# vim:et sts=4 sw=4 # # ibus-typing-booster - A completion input method for IBus # # Copyright (c) 2025 Mike FABIAN # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see ''' dictionary_download.py Uses Gio.File to download dictionary files with a Gtk3 progress dialog. ''' from typing import Dict from typing import Set from typing import List from typing import Optional from typing import Callable # pylint: disable=wrong-import-position import sys if sys.version_info >= (3, 8): from typing import Literal else: from typing_extensions import Literal # pylint: enable=wrong-import-position InstallStatus = Literal['success', 'cancelled', 'failure'] OutputCallback = Callable[[str], None] CompleteCallback = Callable[[InstallStatus], None] import pathlib import os import locale import logging from gi import require_version # pylint: disable=wrong-import-position require_version('GLib', '2.0') require_version('Gio', '2.0') from gi.repository import GLib # type: ignore from gi.repository import Gio # type: ignore try: require_version('Soup', '3.0') from gi.repository import Soup # type: ignore except Exception: # pylint: disable=broad-exception-caught # no Soup 3 available, try Soup 2.4 require_version('Soup', '2.4') from gi.repository import Soup # type: ignore # pylint: enable=wrong-import-position from i18n import _, init as i18n_init sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'engine')) # pylint: disable=import-error, wrong-import-order import itb_util_core from itb_gtk import Gtk from g_compat_helpers import ( add_child, show_all, ) # pylint: enable=import-error, wrong-import-order LOGGER = logging.getLogger('ibus-typing-booster') # pylint: disable=line-too-long # https://github.com/LibreOffice/dictionaries/ DICTIONARY_SOURCES = { 'af': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/af_ZA/af_ZA', 'af_ZA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/af_ZA/af_ZA', 'an': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/an_ES/an_ES', 'an_ES': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/an_ES/an_ES', 'ar': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_AE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_BH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_DJ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_DZ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_EG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_IL': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_IQ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_JO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_KM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_KW': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_LB': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_LY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_MA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_MR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_OM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_PS': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_QA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_SA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_SD': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_SO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_SY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_TD': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_TN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'ar_YE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ar/ar', 'as': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/as_IN/as_IN', 'as_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/as_IN/as_IN', # Fedora package: hunspell-ast-2.0-17.fc43.noarch # ast: (This is ZIP archive, not sure whether I should support that. # https://extensions.libreoffice.org/assets/downloads/340/1717185915/ort-ast-20240531.oxt 'be': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/be_BY/be-official', 'be_BY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/be_BY/be-official', 'bg': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/bg_BG/bg_BG', 'bg_BG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/bg_BG/bg_BG', 'bn': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/bn_BD/bn_BD', 'bn_BD': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/bn_BD/bn_BD', 'bn_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/bn_BD/bn_BD', 'bo': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/bo/bo', 'bo_CN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/bo/bo', 'bo_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/bo/bo', 'br': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/br_FR/br_FR', 'br_FR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/br_FR/br_FR', 'bs': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/bs_BA/bs_BA', 'bs_BA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/bs_BA/bs_BA', 'ca': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ca/dictionaries/ca', 'ca_AD': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ca/dictionaries/ca', 'ca_ES': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ca/dictionaries/ca', 'ca_ES_VALENCIA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ca/dictionaries/ca-valencia', 'ca_FR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ca/dictionaries/ca', 'ca_IT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ca/dictionaries/ca', 'ckb': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ckb/dictionaries/ckb', 'ckb_IQ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ckb/dictionaries/ckb', 'cs': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/cs_CZ/cs_CZ', 'cs_CZ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/cs_CZ/cs_CZ', 'da': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/da_DK/da_DK', 'da_DK': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/da_DK/da_DK', 'de': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/de/de_DE_frami', 'de_AT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/de/de_AT_frami', 'de_BE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/de/de_DE_frami', 'de_CH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/de/de_CH_frami', 'de_DE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/de/de_DE_frami', 'de_IT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/de/de_AT_frami', 'de_LI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/de/de_CH_frami', 'de_LU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/de/de_DE_frami', 'el': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/el_GR/el_GR', 'el_CY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/el_GR/el_GR', 'el_GR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/el_GR/el_GR', 'en': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_US', 'en_001': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_US', 'en_150': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_AG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_AI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_AS': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_AT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_AU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_AU', 'en_BB': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_BE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_BI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_BM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_BS': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_BW': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_BZ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_CA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_CA', 'en_CC': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_CH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_CK': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_CM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_CX': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_CY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_DE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_DG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_DK': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_DM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_GB': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_ER': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_FI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_FJ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_FK': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_FM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_GD': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_GG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_GH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_GI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_GM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_GU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_GY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_HK': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_IE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_IL': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_IM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_IO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_JE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_JM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_KE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_KI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_KN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_KY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_LC': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_LR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_LS': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_MG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_MH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_MO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_MP': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_MS': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_MT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_MU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_MW': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_MY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_NA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_NF': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_NG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_NL': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_NR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_NU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_NZ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_PG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_PH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_PK': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_PN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_PR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_PW': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_RW': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SB': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SC': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SD': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SL': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SS': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SX': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_SZ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_TC': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_TK': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_TO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_TT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_TV': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_TZ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_UG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_UM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_US', 'en_US': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_US', 'en_US_POSIX': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_US', 'en_VC': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_VG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_VI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_US', 'en_VU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_WS': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_ZA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_ZA', 'en_ZM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'en_ZW': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/en_GB', 'eo': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/eo/eo', 'es': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_ES', 'es_419': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_MX', 'es_AR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_AR', 'es_BO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_BO', 'es_BR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_UY', 'es_BZ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_MX', 'es_CL': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_CL', 'es_CO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_CO', 'es_CR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_CR', 'es_CU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_CU', 'es_DO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_DO', 'es_EA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_ES', 'es_EC': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_EC', 'es_ES': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_ES', 'es_GQ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_GQ', 'es_GT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_GT', 'es_HN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_HN', 'es_MX': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_MX', 'es_NI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_NI', 'es_PA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_PA', 'es_PE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_PE', 'es_PH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_PH', 'es_PR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_PR', 'es_PY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_PY', 'es_SV': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_SV', 'es_US': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_US', 'es_UY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_UY', 'es_VE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/es/es_VE', 'et': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/et_EE/et_EE', 'et_EE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/et_EE/et_EE', 'fa': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fa_IR/fa-IR', 'fa_IR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fa_IR/fa-IR', 'fr': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_BE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_BF': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_BI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_BJ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_BL': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_CA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_CD': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_CF': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_CG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_CH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_CI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_CM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_DJ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_DZ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_FR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_GA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_GF': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_GN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_GP': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_GQ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_HT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_KM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_LU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_MA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_MC': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_MF': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_MG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_ML': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_MQ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_MR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_MU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_NC': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_NE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_PF': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_PM': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_RE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_RW': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_SC': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_SN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_SY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_TD': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_TG': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_TN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_VU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_WF': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'fr_YT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/fr_FR/fr', 'gd': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/gd_GB/gd_GB', 'gd_GB': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/gd_GB/gd_GB', 'gl': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/gl/gl_ES', 'gl_ES': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/gl/gl_ES', 'gu': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/gu_IN/gu_IN', 'gu_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/gu_IN/gu_IN', 'gug': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/gug/gug', 'he': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/he_IL/he_IL', 'he_IL': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/he_IL/he_IL', 'hi': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/hi_IN/hi_IN', 'hi_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/hi_IN/hi_IN', 'hr': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/hr_HR/hr_HR', 'hr_HR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/hr_HR/hr_HR', 'hu': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/hu_HU/hu_HU', 'hu_HU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/hu_HU/hu_HU', 'id': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/id/id_ID', 'id_ID': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/id/id_ID', 'is': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/is/is', 'is_IS': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/is/is', 'it': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/it_IT/it_IT', 'it_CH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/it_IT/it_IT', 'it_IT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/it_IT/it_IT', 'ku_SY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/kmr_Latn/kmr_Latn', 'ku_TR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/kmr_Latn/kmr_Latn', 'kmr': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/kmr_Latn/kmr_Latn', 'kmr_SY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/kmr_Latn/kmr_Latn', 'kmr_TR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/kmr_Latn/kmr_Latn', 'kmr_Latn': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/kmr_Latn/kmr_Latn', 'kmr_Latn_TR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/kmr_Latn/kmr_Latn', 'kmr_Latn_SY': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/kmr_Latn/kmr_Latn', 'kn': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/kn_IN/kn_IN', 'kn_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/kn_IN/kn_IN', 'ko': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ko_KR/ko_KR', 'ko_KR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ko_KR/ko_KR', 'lo': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/lo_LA/lo_LA', 'lo_LA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/lo_LA/lo_LA', 'lt': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/lt_LT/lt', 'lt_LT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/lt_LT/lt', 'lv': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/lv_LV/lv_LV', 'lv_LV': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/lv_LV/lv_LV', # 'mg_MG' Fedora package: hunspell-mg-0.20050109-36.fc43.noarch # http://download.services.openoffice.org/contrib/dictionaries/mg_MG.zip 'mn': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/mn_MN/mn_MN', 'mn_MN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/mn_MN/mn_MN', 'mr': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/mr_IN/mr_IN', 'mr_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/mr_IN/mr_IN', 'ne': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ne_NP/ne_NP', 'ne_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ne_NP/ne_NP', 'ne_NP': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ne_NP/ne_NP', 'nl': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/nl_NL/nl_NL', 'nl_AW': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/nl_NL/nl_NL', 'nl_BE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/nl_NL/nl_NL', 'nl_NL': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/nl_NL/nl_NL', 'nb': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/no/nb_NO', 'nb_NO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/no/nb_NO', 'nb_SJ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/no/nb_NO', 'nn': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/no/nn_NO', 'nn_NO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/no/nn_NO', 'oc': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/oc_FR/oc_FR', 'oc_FR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/oc_FR/oc_FR', 'or': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/or_IN/or_IN', 'or_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/or_IN/or_IN', 'pa': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/pa_IN/pa_IN', 'pa_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/pa_IN/pa_IN', 'pl': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/pl_PL/pl_PL', 'pl_PL': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/pl_PL/pl_PL', 'pt': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/pt_BR/pt_BR', 'pt_BR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/pt_BR/pt_BR', 'pt_PT': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/pt_PT/pt_PT', 'ro': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ro/ro_RO', 'ro_RO': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ro/ro_RO', 'ru': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ru_RU/ru_RU', 'ru_RU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ru_RU/ru_RU', 'ru_UA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ru_RU/ru_RU', 'sa': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sa_IN/sa_IN', 'sa_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sa_IN/sa_IN', 'si': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/si_LK/si_LK', 'si_LK': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/si_LK/si_LK', 'sk': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sk_SK/sk_SK', 'sk_SK': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sk_SK/sk_SK', 'sl': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sl_SI/sl_SI', 'sl_SI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sl_SI/sl_SI', 'sq': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sq_AL/sq_AL', 'sq_AL': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sq_AL/sq_AL', 'sr_Latn': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sr/sr-Latn', 'sr': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sr/sr', 'sr_Cyrl': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sr/sr', 'sr_ME': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sr/sr', 'sr_RS': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sr/sr', 'sr_YU': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sr/sr', 'sv': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sv_SE/sv_SE', 'sv_FI': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sv_SE/sv_FI', 'sv_SE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sv_SE/sv_SE', 'sw': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sw_TZ/sw_TZ', 'sw_KE': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sw_TZ/sw_TZ', 'sw_TZ': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/sw_TZ/sw_TZ', 'ta': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ta_IN/ta_IN', 'ta_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/ta_IN/ta_IN', 'te': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/te_IN/te_IN', 'te_IN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/te_IN/te_IN', 'th': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/th_TH/th_TH', 'th_TH': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/th_TH/th_TH', 'tr': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/tr_TR/tr_TR', 'tr_TR': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/tr_TR/tr_TR', 'uk': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/uk_UA/uk_UA', 'uk_UA': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/uk_UA/uk_UA', 'vi': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/vi/vi_VN', 'vi_VN': 'https://raw.githubusercontent.com/LibreOffice/dictionaries/master/vi/vi_VN', } # pylint: enable=line-too-long def download_file_async( url: str, dest_path: str, on_output: Optional[Callable[[str], None]], on_progress: Optional[Callable[[float], None]], on_complete: Optional[Callable[[InstallStatus], None]], cancellable: Optional[Gio.Cancellable] = None, ) -> None: '''Download a file from url to dest_path asynchronously. Using libsoup async instead of Gio.File.copy_async, which is broken on Ubuntu for HTTP URIs. on_output(line): textual log on_progress(fraction): 0.0..1.0 progress fraction for the current file(may be coarse) on_complete(status): 'success' | 'failure' | 'cancelled' ''' try: os.makedirs(os.path.dirname(dest_path), exist_ok=True) except Exception as error: # pylint: disable=broad-except LOGGER.exception('Failed to make directory: %s', error) if on_output: on_output(f'Failed to make directory: {error}') if on_complete and not (cancellable and cancellable.is_cancelled()): on_complete('failure') try: dest_file = Gio.File.new_for_path(dest_path) out_stream = dest_file.replace( None, False, Gio.FileCreateFlags.NONE, # pylint: disable=no-member cancellable) except GLib.Error as err: LOGGER.exception('Failed to open destination: %s', err) if on_output: on_output(f'Failed to open destination: {err}') if on_complete: on_complete('failure') return if Soup.MAJOR_VERSION == 3: session: 'Soup.Session' = Soup.Session() message: 'Soup.Message' = Soup.Message.new('GET', url) def on_read_finished( session: 'Soup.Session', result: 'Gio.AsyncResult', msg: 'Soup.Message', ) -> None: try: # This returns a GLib.Bytes (GBytes) or similar; in any case it's # the whole response body already read by libsoup. body = session.send_and_read_finish(result) data_bytes: Optional[bytes] = None try: # GLib.Bytes.get_data() data_bytes = body.get_data() except Exception: # pylint: disable=broad-exception-caught try: # Maybe it’s already a Python bytes object data_bytes = bytes(body) except Exception: # pylint: disable=broad-exception-caught if on_output: on_output('Failed: could not obtain response bytes') if on_complete: on_complete('failure') return gbytes = GLib.Bytes.new(data_bytes) try: out_stream.write_bytes(gbytes, cancellable) except GLib.Error as err: LOGGER.exception('Write failed: %s', err) if on_output: on_output(f'Write failed: {err}') if on_complete: on_complete('failure') return try: out_stream.close(cancellable) except GLib.Error as err: LOGGER.exception('Failed to close output: %s', err) if on_output: on_output(f'Failed to close output: {err}') if on_complete: on_complete('failure') return if on_progress: on_progress(1.0) # We’ve read the whole body if on_output: on_output(f'{len(data_bytes)/1024.0:.1f} KiB') status = msg.get_status() if status == Soup.Status.OK: if on_output: on_output('Finished.') if on_complete: on_complete('success') else: if on_output: on_output(f'Failed: HTTP {status}') if on_complete: on_complete('failure') except GLib.Error as err: if cancellable and cancellable.is_cancelled(): if on_output: on_output('Cancelled.') if on_complete: on_complete('cancelled') else: LOGGER.exception('Failed to send request: %s', err) if on_output: on_output(f'Failed to send request: {err}') if on_complete: on_complete('failure') session.send_and_read_async( message, GLib.PRIORITY_DEFAULT, cancellable, on_read_finished, message) return # Soup3 could not be imported, try Soup2: session_soup2: 'Soup.SessionAsync' = ( Soup.SessionAsync()) # pylint: disable=c-extension-no-member message_soup2: 'Soup.Message' = Soup.Message.new('GET', url) total_bytes: int = -1 received_bytes: int = 0 def on_headers(msg: 'Soup.Message') -> None: nonlocal total_bytes total_bytes = msg.response_headers.get_content_length() def on_chunk(_msg: 'Soup.Message', chunk: 'Soup.Buffer') -> None: nonlocal received_bytes data_bytes = chunk.get_data() received_bytes += len(data_bytes) try: out_stream.write(data_bytes, cancellable) except GLib.Error as err: LOGGER.exception('Write failed: %s', err) if on_output: on_output(f'Write failed: {err}') if on_complete: on_complete('failure') return if on_progress and total_bytes > 0: frac = min(1.0, received_bytes / total_bytes) on_progress(frac) if on_output: on_output(f'{received_bytes/1024.0:.1f} KiB') def on_finished(msg: 'Soup.Message') -> None: try: out_stream.close(cancellable) except GLib.Error: pass if msg.status_code == 200: if on_output: on_output('Finished.') if on_complete: on_complete('success') return if on_output: on_output(f'Failed: HTTP {msg.status_code}') if on_complete: on_complete('failure') message_soup2.connect('got_headers', on_headers) message_soup2.connect('got_chunk', on_chunk) message_soup2.connect('finished', on_finished) session_soup2.queue_message(message_soup2, None) def download_dictionaries_sequentially_async( languages: Set[str], on_output: Optional[Callable[[str], None]], on_progress: Optional[Callable[[float], None]], on_complete: Optional[Callable[[InstallStatus], None]], cancellable: Optional[Gio.Cancellable] = None, ) -> None: '''Download dictionaries sequentially, one by one.''' url_list: List[Dict[str, str]] = [] for language in sorted(languages): url_base = DICTIONARY_SOURCES.get(language, '') if not url_base: continue for suffix in ('.dic', '.aff'): url_list.append({ 'url': f'{url_base}{suffix}', 'dest': itb_util_core.xdg_save_data_path('ibus-typing-booster/data/') + language + suffix}) results: Dict[str, bool] = {} def download_next(index: int = 0) -> None: '''Download the next url''' if cancellable and cancellable.is_cancelled(): LOGGER.info('Download cancelled, stopping sequence.') if on_complete: on_complete('cancelled') return if index >= len(url_list): success = all(results.values()) LOGGER.info('All downloads done: success=%s', success) if on_complete: on_complete('success' if success else 'failure') return url = url_list[index]['url'] destination = url_list[index]['dest'] LOGGER.info('Downloading url %s (%d/%d)', url, index + 1, len(url_list)) if on_output: on_output(f'📥 {url}...') def handle_output(line: str) -> None: '''Call on_output''' if on_output: on_output(line) def handle_progress(fraction: float) -> None: '''Call on_progress''' if on_progress: on_progress(fraction) def handle_complete(status: InstallStatus) -> None: '''Called when one url is completed Puts easily parsable lines into the output to make it easy for dialog to update the progress. ''' results[url] = status == 'success' if on_output: on_output(f'{url} ➡️ {destination} {"✔️" if status == "success" else "⚠️"}') if status == 'cancelled': LOGGER.info('Installation cancelled before url #%d', index) return download_next(index + 1) download_file_async( url, destination, on_output=handle_output, on_progress=handle_progress, on_complete=handle_complete, cancellable=cancellable) download_next() def ensure_enchant_symlinks(data_dir: str) -> None: '''Ensure Enchant hunspell/nuspell directories link to downloaded dictionaries. Creates or replaces symlinks for *.dic/*.aff in data_dir under ~/.config/enchant/{hunspell,nuspell}/, and cleans up broken symlinks. ''' config_root = pathlib.Path.home() / '.config' / 'enchant' data_path = pathlib.Path(data_dir) if not data_path.exists(): LOGGER.warning('Data directory does not exist: %s', data_dir) return for backend in ('hunspell', 'nuspell'): backend_dir = config_root / backend backend_dir.mkdir(parents=True, exist_ok=True) # Clean up broken symlinks first: for link in backend_dir.glob('*'): if link.is_symlink() and not link.exists(): LOGGER.info('Removing broken symlink: %s', link) try: link.unlink() except OSError as err: LOGGER.error( 'Failed to remove broken symlink %s: %s', link, err) # Create or update valid symlinks: for dict_file in pathlib.Path(data_dir).glob('*.dic'): for suffix in ('dic', 'aff'): source = data_path / f'{dict_file.stem}.{suffix}' if not source.exists(): continue dest = backend_dir / source.name try: # Replace only missing or symlinked destinations if dest.exists(): if dest.is_symlink(): dest.unlink() else: LOGGER.info( 'Preserving existing user dictionary: %s', dest) continue os.symlink(source, dest) LOGGER.info('Linked %s → %s', dest, source) except OSError as err: LOGGER.error( 'Failed to create symlink %s → %s: %s', dest, source, err) def download_dictionaries_with_dialog( parent: Optional[Gtk.Window], languages: Set[str], on_complete: Optional[CompleteCallback] = None, ) -> None: '''Show a transient GTK dialog to download dictionaries asynchronously.''' dialog = Gtk.Dialog( title='📥 ' + _('Download dictionaries'), transient_for=parent, modal=True, destroy_with_parent=True) dialog.set_default_size(480, 650) vbox = dialog.get_content_area() top_label = Gtk.Label(label='') top_label.set_xalign(0) add_child(vbox, top_label) textview = Gtk.TextView() textview.set_editable(False) textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) textbuffer = textview.get_buffer() scrolled = Gtk.ScrolledWindow() scrolled.set_vexpand(True) scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) add_child(scrolled, textview) add_child(vbox, scrolled) progressbar = Gtk.ProgressBar() progressbar.set_show_text(True) progressbar.set_text('0%') add_child(vbox, progressbar) action_area = Gtk.Box() action_area.set_orientation(Gtk.Orientation.HORIZONTAL) action_area.set_halign(Gtk.Align.END) action_area.set_valign(Gtk.Align.CENTER) action_area.set_can_focus(False) action_area.set_hexpand(True) action_area.set_vexpand(False) action_area.set_spacing(0) add_child(vbox, action_area) cancel_button = Gtk.Button.new_with_mnemonic(_('_Cancel')) add_child(action_area, cancel_button) close_button = Gtk.Button.new_with_mnemonic(_('_Close')) add_child(action_area, close_button) show_all(dialog) close_button.set_visible(False) url_index: int = 0 url_count: int = 2 * len(languages) cancellable: Gio.Cancellable = Gio.Cancellable() def append_line(line: str) -> None: '''Append a line to the textbuffer showing detailed progress''' nonlocal url_index end = textbuffer.get_end_iter() textbuffer.insert(end, line + '\n') textview.scroll_mark_onscreen(textbuffer.get_insert()) # Detect per-url completion (From messages I emit myself) if '✔️' in line or '⚠️' in line: # Count this url as fully processed url_index += 1 text = top_label.get_text() # In Python < 3.12, f-string expression part cannot include a backslash newline = '\n' top_label.set_text(f'{text + newline if text else ""}{line}') def update_progressbar(fraction: float) -> None: '''Update the progressbar showing the total progress''' # Make sure UI never jumps backward if fraction temporarily # drops (some async backends report non-monotonic progress current_fraction = max( progressbar.get_fraction(), (url_index + fraction) / url_count) progressbar.set_fraction(current_fraction) progressbar.set_text( f'{url_index}/{url_count} ({int(current_fraction * 100)}%)') def finish(status: InstallStatus) -> None: '''Installation of all urls finished or cancelled.''' if status == 'success': progressbar.set_fraction(1.0) progressbar.set_text(f'{url_count}/{url_count} (100%)') ensure_enchant_symlinks( itb_util_core.xdg_save_data_path('ibus-typing-booster/data/')) elif status == 'cancelled': end = textbuffer.get_end_iter() textbuffer.insert(end, '⚠️ Download cancelled by user.\n') else: # 'failed' end = textbuffer.get_end_iter() textbuffer.insert(end, '❌ There were some failures.\n') cancel_button.set_visible(False) close_button.set_visible(True) if on_complete: on_complete(status) def on_cancel(_button: Gtk.Button) -> None: '''Cancel button clicked''' cancellable.cancel() cancel_button.set_visible(False) close_button.set_visible(True) def on_close(_button: Gtk.Button) -> None: '''Close button clicked''' if glib_main_loop is not None and glib_main_loop.is_running(): glib_main_loop.quit() dialog.destroy() cancel_button.connect('clicked', on_cancel) close_button.connect('clicked', on_close) download_dictionaries_sequentially_async( languages, on_output=append_line, on_progress=update_progressbar, on_complete=finish, cancellable=cancellable) glib_main_loop: Optional[GLib.MainLoop] = None # If a GLib main loop is already running (e.g. from setup UI), # don’t start another one; just return control to it. if GLib.main_depth() > 0: # pylint: disable=no-value-for-parameter # There’s already a running loop return # Otherwise (standalone testing), run our own main loop glib_main_loop = GLib.MainLoop() glib_main_loop.run() dialog.destroy() if __name__ == '__main__': LOG_HANDLER = logging.StreamHandler(stream=sys.stderr) LOGGER.setLevel(logging.DEBUG) LOGGER.addHandler(LOG_HANDLER) try: locale.setlocale(locale.LC_ALL, '') except locale.Error: LOGGER.exception("IBUS-WARNING **: Using the fallback 'C' locale") locale.setlocale(locale.LC_ALL, 'C') i18n_init() def finish_test(status: InstallStatus) -> None: '''Called when downloads have completed''' if status == 'success': LOGGER.info('Install completed without errors.') elif status == 'cancelled': LOGGER.info('⚠️ Installation cancelled by user.') else: # 'failure' LOGGER.info('❌ There were some failures.') download_dictionaries_with_dialog( None, set(sorted(DICTIONARY_SOURCES.keys())[:4]), on_complete=finish_test, )