diff --git a/Cargo.lock b/Cargo.lock index 950108f..7e5b5f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,9 +295,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "apply" @@ -1470,7 +1470,7 @@ dependencies = [ [[package]] name = "cosmic-files" version = "1.0.5" -source = "git+https://github.com/pop-os/cosmic-files.git#40f19b0d02e308676aa0ae4c05a670bd0b692607" +source = "git+https://github.com/pop-os/cosmic-files.git#055d9e371ca0be7dd2959a670212122c1010221b" dependencies = [ "anyhow", "chrono", @@ -1569,7 +1569,7 @@ dependencies = [ [[package]] name = "cosmic-term" -version = "1.0.5" +version = "1.0.6" dependencies = [ "alacritty_terminal", "clap_lex", @@ -1586,6 +1586,7 @@ dependencies = [ "open", "palette", "paste", + "regex", "ron 0.11.0", "rust-embed", "secret-service", @@ -3949,9 +3950,9 @@ checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" dependencies = [ "jiff-static", "log", @@ -3962,9 +3963,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" dependencies = [ "proc-macro2", "quote", @@ -8872,18 +8873,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.37" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.37" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" dependencies = [ "proc-macro2", "quote", @@ -8967,9 +8968,9 @@ dependencies = [ [[package]] name = "zip" -version = "7.2.0" +version = "7.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" +checksum = "268bf6f9ceb991e07155234071501490bb41fd1e39c6a588106dad10ae2a5804" dependencies = [ "aes", "bzip2", @@ -8977,7 +8978,6 @@ dependencies = [ "crc32fast", "deflate64", "flate2", - "generic-array", "getrandom 0.3.4", "hmac", "indexmap 2.13.0", diff --git a/Cargo.toml b/Cargo.toml index c6c3f33..30fda4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-term" -version = "1.0.5" +version = "1.0.6" authors = ["Jeremy Soller "] edition = "2024" license = "GPL-3.0-only" @@ -14,6 +14,7 @@ log = "0.4" open = "5.3.2" palette = { version = "0.7", features = ["serde"] } paste = "1.0" +regex = "1" ron = "0.11" serde = { version = "1", features = ["serde_derive"] } shlex = "1" diff --git a/README.md b/README.md index 65dc63a..34a88c5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # cosmic-term -WIP COSMIC terminal emulator, built using [alacritty\_terminal](https://docs.rs/alacritty_terminal) that is provided by the [alacritty](https://github.com/alacritty/alacritty) project. `cosmic-term` provides bidirectional rendering and ligatures with a custom renderer based on [cosmic-text](https://github.com/pop-os/cosmic-text). +COSMIC terminal emulator, built using [alacritty\_terminal](https://docs.rs/alacritty_terminal) that is provided by the [alacritty](https://github.com/alacritty/alacritty) project. `cosmic-term` provides bidirectional rendering and ligatures with a custom renderer based on [cosmic-text](https://github.com/pop-os/cosmic-text). The `wgpu` feature, enabled by default, supports GPU rendering using `glyphon` and `wgpu`. If `wgpu` is not enabled or fails to initialize, then rendering falls diff --git a/debian/changelog b/debian/changelog index 71e3402..4a9c877 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +cosmic-term (1.0.6) noble; urgency=medium + + * Epoch 1.0.6 version update + + -- Jeremy Soller Thu, 05 Feb 2026 15:23:41 -0700 + cosmic-term (1.0.5) noble; urgency=medium * Epoch 1.0.5 version update diff --git a/i18n/ar/cosmic_term.ftl b/i18n/ar/cosmic_term.ftl index 00b7605..df282cd 100644 --- a/i18n/ar/cosmic_term.ftl +++ b/i18n/ar/cosmic_term.ftl @@ -117,3 +117,28 @@ open-link = افتح الرابط menu-password-manager = كلمات السر... password-input = كلمة السر password-input-description = الوصف +add-another-keybinding = أضِف ارتباطات مفاتيح أخرى +cancel = ألغِ +close-window = أغلِق النافذة +disable = عطِّل +focus-pane-left = تركيز على اللوحة الشِمالية +focus-pane-right = تركيز على اللوحة اليمنى +focus-pane-up = تركيز على اللوحة الفوقية +keyboard-shortcuts = اختصارات لوحة المفاتيح +menu-keyboard-shortcuts = اختصارات لوحة المفاتيح... +no-shortcuts = لا اختصارات +password-manager = مدير كلمات السر +paste-primary = ألصِق الرئيسي +replace = استبدل +reset-to-default = صفّر إلى المبدئي +shortcut-capture-hint = اضغط على تركيبة المفاتيح +shortcut-group-clipboard = الحافظة +shortcut-group-other = أخرى +shortcut-group-tabs = ألسنة +shortcut-group-window = نافذة +shortcut-group-zoom = كبِّر +shortcut-replace-body = عُيِّن { $binding } بالفعل لـ { $existing }. أتريد استبداله بـ { $new_action }؟ +shortcut-replace-title = استبدل الاختصار؟ +tab-activate = نشّط لسان { $number } +toggle-fullscreen = بدّل ملء الشاشة +type-to-search = اكتب للبحث... diff --git a/i18n/be/cosmic_term.ftl b/i18n/be/cosmic_term.ftl index 9b83864..00b22f6 100644 --- a/i18n/be/cosmic_term.ftl +++ b/i18n/be/cosmic_term.ftl @@ -44,7 +44,7 @@ light = Светлая syntax-dark = Цёмны сінтаксіс syntax-light = Светлы сінтаксіс default-zoom-step = Крок маштабавання -opacity = Непразрыстаць фону +opacity = Непразрыстасць фону ### Font @@ -97,7 +97,7 @@ find = Знайсці ## View view = Выгляд -zoom-in = Большы тэкст +zoom-in = Павялічыць тэкст zoom-reset = Прадв. памер тэксту zoom-out = Меньшы тэкст next-tab = Наступная ўкладка diff --git a/i18n/cs/cosmic_term.ftl b/i18n/cs/cosmic_term.ftl index dd10076..95405b1 100644 --- a/i18n/cs/cosmic_term.ftl +++ b/i18n/cs/cosmic_term.ftl @@ -60,9 +60,9 @@ select-all = Vybrat vše find = Najít clear-scrollback = Vymazat rolování view = Zobrazení -zoom-in = Větší text +zoom-in = Zvětšit text zoom-reset = Výchozí velikost textu -zoom-out = Menší text +zoom-out = Zmenšit text next-tab = Další karta previous-tab = Předchozí karta split-horizontal = Rozdělit horizontálně @@ -77,3 +77,30 @@ add-password = Přidat heslo password-input = Heslo password-input-description = Popis open-link = Otevřít odkaz +add-another-keybinding = Přidat další klávesovou zkratku +cancel = Zrušit +close-window = Zavřít okno +disable = Zakázat +keyboard-shortcuts = Klávesové zkratky +menu-keyboard-shortcuts = Klávesové zkratky... +no-shortcuts = Žádné zkratky +password-manager = Manažer hesel +replace = Nahradit +reset-to-default = Obnovit výchozí +shortcut-group-clipboard = Schránka +shortcut-group-tabs = Karty +shortcut-group-window = Okno +shortcut-replace-title = Nahradit zkratku? +tab-activate = Aktivovat kartu { $number } +shortcut-group-other = Ostatní +shortcut-group-zoom = Přiblížení +toggle-fullscreen = Přepnout režim celé obrazovky +type-to-search = Pište pro vyhledávání... +copy-or-sigint = Kopírovat nebo SIGINT +focus-pane-down = Zaměřit panel dole +focus-pane-left = Zaměřit panel vlevo +focus-pane-right = Zaměřit panel vpravo +focus-pane-up = Zaměřit panel nahoře +shortcut-replace-body = { $binding } je již přiřazena k „{ $existing }“. Nahradit za „{ $new_action }“? +shortcut-capture-hint = Stiskněte kombinaci kláves +paste-primary = Vložit primární diff --git a/i18n/de/cosmic_term.ftl b/i18n/de/cosmic_term.ftl index bf1bca3..4bbb64e 100644 --- a/i18n/de/cosmic_term.ftl +++ b/i18n/de/cosmic_term.ftl @@ -110,3 +110,4 @@ menu-color-schemes = Farbschemen... menu-settings = Einstellungen... menu-about = Über COSMIC Terminal... repository = Repository +cancel = Abbrechen diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index 88ed838..00d892c 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -62,6 +62,35 @@ advanced = Advanced show-headerbar = Show header show-header-description = Reveal the header from the right-click menu. +### Keyboard shortcuts +add-another-keybinding = Add another keybinding +cancel = Cancel +close-window = Close window +copy-or-sigint = Copy or SIGINT +disable = Disable +focus-pane-down = Focus pane down +focus-pane-left = Focus pane left +focus-pane-right = Focus pane right +focus-pane-up = Focus pane up +keyboard-shortcuts = Keyboard shortcuts +menu-keyboard-shortcuts = Keyboard shortcuts... +no-shortcuts = No shortcuts +password-manager = Password manager +paste-primary = Paste primary +replace = Replace +reset-to-default = Reset to default +shortcut-capture-hint = Press the key combination +shortcut-group-clipboard = Clipboard +shortcut-group-other = Other +shortcut-group-tabs = Tabs +shortcut-group-window = Window +shortcut-group-zoom = Zoom +shortcut-replace-body = { $binding } is already assigned to { $existing }. Replace it with { $new_action }? +shortcut-replace-title = Replace shortcut? +tab-activate = Activate tab { $number } +toggle-fullscreen = Toggle fullscreen +type-to-search = Type to search... + # Find find-placeholder = Find... find-previous = Find previous diff --git a/i18n/fr/cosmic_term.ftl b/i18n/fr/cosmic_term.ftl index 2e2e715..febb158 100644 --- a/i18n/fr/cosmic_term.ftl +++ b/i18n/fr/cosmic_term.ftl @@ -117,3 +117,30 @@ add-password = Ajouter un mot de passe password-input = Mot de passe password-input-description = Description open-link = Ouvrir le lien +add-another-keybinding = Ajouter un autre raccourci clavier +cancel = Annuler +close-window = Fermer la fenêtre +copy-or-sigint = Copier ou SIGINT +disable = Désactiver +keyboard-shortcuts = Raccourcis clavier +menu-keyboard-shortcuts = Raccourcis clavier... +no-shortcuts = Aucun raccourci +password-manager = Gestionnaire de mots de passe +paste-primary = Coller primaire +replace = Remplacer +shortcut-capture-hint = Appuyer sur la combinaison de touches +shortcut-group-clipboard = Presse-papier +shortcut-group-other = Autre +shortcut-group-tabs = Onglets +shortcut-group-window = Fenêtre +shortcut-group-zoom = Zoom +shortcut-replace-body = { $binding } est déjà assigné à { $existing }. Le remplacer avec { $new_action } ? +shortcut-replace-title = Remplacer raccourci ? +tab-activate = Activer l'onglet { $number } +type-to-search = Taper pour chercher... +focus-pane-down = Passer au panneau du dessous +focus-pane-left = Passer au panneau de gauche +focus-pane-right = Passer au panneau de droite +focus-pane-up = Passer au panneau du dessus +reset-to-default = Rétablir les paramètres par défaut +toggle-fullscreen = Basculer en plein écran diff --git a/i18n/ga/cosmic_term.ftl b/i18n/ga/cosmic_term.ftl index 029c6e3..e71adfc 100644 --- a/i18n/ga/cosmic_term.ftl +++ b/i18n/ga/cosmic_term.ftl @@ -117,3 +117,30 @@ passwords-title = Pasfhocail add-password = Cuir Pasfhocal leis password-input = Pasfhocal password-input-description = Cur síos +add-another-keybinding = Cuir ceangal eochrach eile leis +cancel = Cealaigh +close-window = Dún an fhuinneog +copy-or-sigint = Cóipeáil nó SIGINT +disable = Díchumasaigh +focus-pane-down = Painéal fócais síos +focus-pane-left = Painéal fócais ar chlé +focus-pane-right = Painéal fócais ar dheis +focus-pane-up = Painéal fócais suas +keyboard-shortcuts = Aicearraí méarchláir +menu-keyboard-shortcuts = Aicearraí méarchláir... +no-shortcuts = Gan aicearraí +password-manager = Bainisteoir pasfhocal +paste-primary = Greamaigh príomhúil +replace = Athsholáthair +reset-to-default = Athshocraigh go réamhshocraithe +shortcut-capture-hint = Brúigh an teaglaim eochrach +shortcut-group-clipboard = Gearrthaisce +shortcut-group-other = Eile +shortcut-group-tabs = Cluaisíní +shortcut-group-window = Fuinneog +shortcut-group-zoom = Súmáil +shortcut-replace-body = Tá { $binding } sannta do { $existing } cheana féin. Cuir { $new_action } ina áit? +shortcut-replace-title = Aicearra a athsholáthar? +tab-activate = Gníomhachtaigh an cluaisín { $number } +toggle-fullscreen = Scoraigh lánscáileán +type-to-search = Clóscríobh le cuardach a dhéanamh... diff --git a/i18n/hu/cosmic_term.ftl b/i18n/hu/cosmic_term.ftl index 7eb0fda..37ce161 100644 --- a/i18n/hu/cosmic_term.ftl +++ b/i18n/hu/cosmic_term.ftl @@ -117,3 +117,30 @@ add-password = Jelszó hozzáadása password-input = Jelszó password-input-description = Leírás open-link = Hivatkozás megnyitása +add-another-keybinding = Új gyorsbillentyű hozzáadása +cancel = Mégse +close-window = Ablak bezárása +copy-or-sigint = Másolás vagy SIGINT +disable = Letiltás +focus-pane-down = Fókusz az alsó panelre +focus-pane-left = Fókusz a bal oldali panelre +focus-pane-right = Fókusz a jobb oldali panelre +keyboard-shortcuts = Gyorsbillentyűk +menu-keyboard-shortcuts = Gyorsbillentyűk… +no-shortcuts = Nincsenek gyorsbillentyűk +password-manager = Jelszókezelő +focus-pane-up = Fókusz a felső panelre +paste-primary = Elsődleges vágólap beillesztése +replace = Csere +reset-to-default = Visszaállítás alapértelmezettre +shortcut-capture-hint = Nyomd meg a billentyűkombinációt +shortcut-group-clipboard = Vágólap +shortcut-group-other = Egyéb +shortcut-group-tabs = Lapok +shortcut-group-window = Ablak +shortcut-group-zoom = Nagyítás +shortcut-replace-body = A(z) { $binding } már hozzá van rendelve ehhez: { $existing }. Lecseréled erre: { $new_action }? +shortcut-replace-title = Gyorsbillentyű cseréje? +tab-activate = { $number }. lap aktiválása +toggle-fullscreen = Teljes képernyő váltása +type-to-search = Gépelj a kereséshez… diff --git a/i18n/id/cosmic_term.ftl b/i18n/id/cosmic_term.ftl index 51a9c43..06e9cf3 100644 --- a/i18n/id/cosmic_term.ftl +++ b/i18n/id/cosmic_term.ftl @@ -68,7 +68,7 @@ next-tab = Tab selanjutnya previous-tab = Tab sebelumnya split-horizontal = Pemisahan horisontal split-vertical = Pemisahan vertikal -pane-toggle-maximize = Alihkan ke maksimal +pane-toggle-maximize = Ubah ke maksimal menu-color-schemes = Skema warna... menu-settings = Pengaturan... menu-about = Tentang Terminal COSMIC... @@ -77,3 +77,30 @@ passwords-title = Kata Sandi add-password = Tambahkan Kata Sandi password-input = Kata Sandi password-input-description = Deskripsi +add-another-keybinding = Tambahkan pintasan tombol lainnya +cancel = Batalkan +close-window = Tutup jendela +copy-or-sigint = Salin atau SIGINT +disable = Nonaktifkan +type-to-search = Ketik untuk mencari... +toggle-fullscreen = Ubah layar penuh +tab-activate = Aktifkan tab { $number } +replace = Ganti +shortcut-replace-title = Ganti pintasan? +shortcut-group-zoom = Zum +shortcut-group-window = Jendela +shortcut-group-tabs = Tab +shortcut-group-other = Lainnya +shortcut-group-clipboard = Papan klip +shortcut-capture-hint = Tekan kombinasi tombol +reset-to-default = Atur ulang ke bawaan +shortcut-replace-body = { $binding } sudah ditetapkan ke { $existing }. Ganti dengan { $new_action }? +paste-primary = Tempel primer +password-manager = Pengelolaan kata sandi +no-shortcuts = Tidak ada pintasan +menu-keyboard-shortcuts = Pintasan papan ketik... +keyboard-shortcuts = Pintasan papan ketik +focus-pane-down = Fokuskan panel ke bawah +focus-pane-left = Fokuskan panel ke kiri +focus-pane-right = Fokuskan panel ke kanan +focus-pane-up = Fokuskan panel ke atas diff --git a/i18n/kk/cosmic_term.ftl b/i18n/kk/cosmic_term.ftl index d5ba93d..c3e8a1e 100644 --- a/i18n/kk/cosmic_term.ftl +++ b/i18n/kk/cosmic_term.ftl @@ -77,3 +77,30 @@ passwords-title = Парольдер add-password = Пароль қосу password-input = Пароль password-input-description = Сипаттама +cancel = Бас тарту +add-another-keybinding = Басқа пернелер тіркесін қосу +close-window = Терезені жабу +copy-or-sigint = Көшіру немесе SIGINT +disable = Сөндіру +focus-pane-down = Төмендегі панельге фокустау +focus-pane-left = Сол жақтағы панельге фокустау +focus-pane-right = Оң жақтағы панельге фокустау +focus-pane-up = Жоғарыдағы панельге фокустау +keyboard-shortcuts = Пернетақта жарлықтары +menu-keyboard-shortcuts = Пернетақта жарлықтары... +no-shortcuts = Жарлықтар жоқ +password-manager = Парольдер менеджері +paste-primary = Негізгіні кірістіру +replace = Алмастыру +reset-to-default = Әдепкі күйге қайтару +shortcut-capture-hint = Пернелер комбинациясын басыңыз +shortcut-group-clipboard = Алмасу буфері +shortcut-group-other = Басқа +shortcut-group-tabs = Беттер +shortcut-group-window = Терезе +shortcut-group-zoom = Масштаб +shortcut-replace-body = { $binding } қазірдің өзінде { $existing } үшін тағайындалған. Оны { $new_action } әрекетімен алмастыру керек пе? +shortcut-replace-title = Жарлықты алмастыру керек пе? +tab-activate = { $number }-бетті белсендіру +toggle-fullscreen = Толық экран режиміне ауысу +type-to-search = Іздеу үшін теріңіз... diff --git a/i18n/ml/cosmic_term.ftl b/i18n/ml/cosmic_term.ftl new file mode 100644 index 0000000..e69de29 diff --git a/i18n/nl/cosmic_term.ftl b/i18n/nl/cosmic_term.ftl index 9a22d0f..10e1a1c 100644 --- a/i18n/nl/cosmic_term.ftl +++ b/i18n/nl/cosmic_term.ftl @@ -11,9 +11,9 @@ new-terminal = Nieuwe terminal color-schemes = Kleurenschema's rename = Hernoem -export = Exporteer +export = Exporteren delete = Verwijder -import = Importeer +import = Importeren import-errors = Importfouten ## Profiles @@ -111,3 +111,6 @@ menu-settings = Instellingen… menu-about = Over COSMIC Terminal… support = Ondersteuning repository = Bibliotheek +cancel = Annuleren +type-to-search = Typ om te zoeken… +replace = Vervangen diff --git a/i18n/oc/cosmic_term.ftl b/i18n/oc/cosmic_term.ftl new file mode 100644 index 0000000..e69de29 diff --git a/i18n/pl/cosmic_term.ftl b/i18n/pl/cosmic_term.ftl index 21329c7..9a01731 100644 --- a/i18n/pl/cosmic_term.ftl +++ b/i18n/pl/cosmic_term.ftl @@ -117,3 +117,30 @@ add-password = Dodaj Hasło password-input = Hasło password-input-description = Opis open-link = Otwórz Odnośnik +add-another-keybinding = Dodaj kolejny skrót klawiszowy +cancel = Anuluj +close-window = Zamknij okno +copy-or-sigint = Kopiuj lub SIGINT +disable = Wyłącz +keyboard-shortcuts = Skróty klawiszowe +menu-keyboard-shortcuts = Skróty klawiszowe… +no-shortcuts = Brak skrótów +password-manager = Menedżer haseł +paste-primary = Wklej główne +replace = Zastąp +reset-to-default = Przywróć domyślne +shortcut-capture-hint = Naciśnij kombinację klawiszy +shortcut-group-clipboard = Schowek +shortcut-group-other = Inne +shortcut-group-tabs = Karty +shortcut-group-window = Okno +shortcut-group-zoom = Przybliżenie +shortcut-replace-body = { $binding } jest już przypisany do { $existing }. Zastąpć je przez { $new_action }? +shortcut-replace-title = Zastąpić skrót klawiszowy? +tab-activate = Aktywuj kartę { $number } +toggle-fullscreen = Przełącznik pełnego ekranu +type-to-search = Zacznij pisać by wyszukać… +focus-pane-down = Aktywuj panel niżej +focus-pane-left = Aktywuj lewy panel +focus-pane-right = Aktywuj prawy panel +focus-pane-up = Aktywuj panel wyżej diff --git a/i18n/pt-BR/cosmic_term.ftl b/i18n/pt-BR/cosmic_term.ftl index 4118512..d9250fd 100644 --- a/i18n/pt-BR/cosmic_term.ftl +++ b/i18n/pt-BR/cosmic_term.ftl @@ -117,3 +117,30 @@ passwords-title = Senhas add-password = Adicionar Senha password-input = Senha password-input-description = Descrição +add-another-keybinding = Adicionar outra combinação de teclas +cancel = Cancelar +close-window = Fechar janela +copy-or-sigint = Copiar ou SIGINT +disable = Desabilitar +focus-pane-down = Focar painel abaixo +focus-pane-left = Focar painel à esquerda +focus-pane-right = Focar painel à direita +focus-pane-up = Focar painel acima +keyboard-shortcuts = Atalhos de teclado +menu-keyboard-shortcuts = Atalhos de teclado... +no-shortcuts = Sem atalhos +password-manager = Gerenciador de senhas +paste-primary = Colar +replace = Substituir +reset-to-default = Restaurar padrão +shortcut-capture-hint = Pressione a combinação de teclas +shortcut-group-clipboard = Área de transferência +shortcut-group-other = Outro +shortcut-group-tabs = Abas +shortcut-group-window = Janela +shortcut-group-zoom = Zoom +shortcut-replace-body = { $binding } já está atribuído a { $existing }. Substituir por { $new_action }? +shortcut-replace-title = Substituir atalho? +tab-activate = Ativar aba { $number } +toggle-fullscreen = Alternar para tela cheia +type-to-search = Digite para pesquisar... diff --git a/i18n/ru/cosmic_term.ftl b/i18n/ru/cosmic_term.ftl index 357d753..dfa6b60 100644 --- a/i18n/ru/cosmic_term.ftl +++ b/i18n/ru/cosmic_term.ftl @@ -13,7 +13,7 @@ color-schemes = Цветовые схемы rename = Переименовать export = Экспортировать delete = Удалить -import = Импортировать +import = Импорт import-errors = Ошибки при импорте ## Profiles @@ -39,8 +39,8 @@ settings = Параметры appearance = Оформление theme = Тема match-desktop = Как в системе -dark = Тёмное -light = Светлое +dark = Тёмная +light = Светлая syntax-dark = Тёмная цветовая схема syntax-light = Светлая цветовая схема default-zoom-step = Шаг масштабирования @@ -111,3 +111,30 @@ menu-about = О Терминале COSMIC... support = Поддержка repository = Репозиторий clear-scrollback = Очистить вывод команд +add-another-keybinding = Добавить сочетание клавиш +cancel = Отмена +close-window = Закрыть окно +copy-or-sigint = Копировать или SIGINT +disable = Отключить +focus-pane-down = Переместить фокус вниз +focus-pane-left = Переместить фокус налево +focus-pane-right = Переместить фокус направо +focus-pane-up = Переместить фокус вверх +keyboard-shortcuts = Сочетания клавиш +menu-keyboard-shortcuts = Сочетания клавиш... +no-shortcuts = Нет сочетаний клавиш +password-manager = Менеджер паролей +paste-primary = Вставить первичное выделение +replace = Заменить +reset-to-default = Вернуть по умолчанию +shortcut-capture-hint = Нажмите комбинацию клавиш +shortcut-group-clipboard = Буфер обмена +shortcut-group-other = Прочие +shortcut-group-tabs = Вкладки +shortcut-group-window = Окно +shortcut-group-zoom = Масштаб +shortcut-replace-body = { $binding } уже присвоено { $existing }. Заменить его на { $new_action }? +shortcut-replace-title = Заменить сочетание клавиш? +tab-activate = Активировать вкладку { $number } +toggle-fullscreen = Вкл./выкл. полноэкранный режим +type-to-search = Введите для поиска... diff --git a/i18n/sv-SE/cosmic_term.ftl b/i18n/sv-SE/cosmic_term.ftl index 8ad0927..cc21887 100644 --- a/i18n/sv-SE/cosmic_term.ftl +++ b/i18n/sv-SE/cosmic_term.ftl @@ -121,3 +121,30 @@ passwords-title = Lösenord add-password = Lägg till lösenord password-input = Lösenord password-input-description = Beskrivning +type-to-search = Skriv för att söka... +cancel = Avbryt +close-window = Stäng fönster +copy-or-sigint = Kopiera eller SIGINT +disable = Avaktivera +password-manager = Lösenordshanterare +replace = Ersätt +shortcut-capture-hint = Tryck tangentkombinationen +shortcut-group-window = Fönster +shortcut-replace-body = { $binding } har redan tilldelats till { $existing }. Ersätt den med { $new_action }? +shortcut-replace-title = Ersätt genväg? +tab-activate = Aktivera flik { $number } +toggle-fullscreen = Växla helskärmsläge +add-another-keybinding = Lägg till ytterligare tangentbindning +focus-pane-down = Fokusera fält nedåt +focus-pane-left = Fokusera fält vänster +focus-pane-right = Fokusera fält höger +focus-pane-up = Fokusera fält uppåt +keyboard-shortcuts = Tangentbordsgenvägar +menu-keyboard-shortcuts = Tangentbordsgenvägar... +no-shortcuts = Inga genvägar +paste-primary = Klistra in primär +reset-to-default = Återställ till standard +shortcut-group-clipboard = Urklipp +shortcut-group-other = Andra +shortcut-group-tabs = Flikar +shortcut-group-zoom = Zooma diff --git a/i18n/uk/cosmic_term.ftl b/i18n/uk/cosmic_term.ftl index c48df88..55756fe 100644 --- a/i18n/uk/cosmic_term.ftl +++ b/i18n/uk/cosmic_term.ftl @@ -12,7 +12,7 @@ new-terminal = Новий термінал color-schemes = Кольорові схеми rename = Перейменувати export = Експорт -delete = Вилучити +delete = Видалити import = Імпортувати import-errors = Помилки імпорту @@ -38,7 +38,7 @@ settings = Налаштування appearance = Зовнішній вигляд theme = Тема -match-desktop = Відповідно системі +match-desktop = Системна dark = Темна light = Світла syntax-dark = Темна колірна схема @@ -96,7 +96,7 @@ find = Знайти ## View -view = Вигляд +view = Вид zoom-in = Збільшити текст zoom-reset = Типовий розмір тексту zoom-out = Зменшити текст @@ -108,12 +108,39 @@ pane-toggle-maximize = Перемкнути розгортання menu-color-schemes = Кольорові схеми... menu-settings = Налаштування... menu-about = Про Термінал COSMIC... -repository = Репозиторій +repository = Сховище support = Підтримка -clear-scrollback = Очистити журнал прокрутки +clear-scrollback = Очистити прокрутку menu-password-manager = Паролі... passwords-title = Паролі add-password = Додати пароль password-input = Пароль password-input-description = Опис open-link = Відкрити ланку +cancel = Скасувати +close-window = Закрити вікно +disable = Вимкнути +keyboard-shortcuts = Сполучення клавіш +menu-keyboard-shortcuts = Сполучення клавіш... +password-manager = Менеджер паролів +replace = Замінити +reset-to-default = Типові значення +shortcut-group-tabs = Вкладки +shortcut-group-window = Вікно +shortcut-replace-body = { $binding } вже призначено { $existing }. Замінити на { $new_action }? +shortcut-replace-title = Замінити сполучення? +type-to-search = Введіть для пошуку… +copy-or-sigint = Копіювати або SIGINT +no-shortcuts = Сполучення не призначено +paste-primary = Вставити основне +shortcut-capture-hint = Натисніть комбінацію клавіш +shortcut-group-clipboard = Буфер обміну +shortcut-group-other = Інше +shortcut-group-zoom = Масштаб +tab-activate = Вибрати вкладку { $number } +toggle-fullscreen = На весь екран +add-another-keybinding = Додати ще сполучення +focus-pane-down = Фокус на нижню панель +focus-pane-left = Фокус на ліву панель +focus-pane-right = Фокус на праву панель +focus-pane-up = Фокус на верхню панель diff --git a/i18n/zh-CN/cosmic_term.ftl b/i18n/zh-CN/cosmic_term.ftl index fd291a4..de0da79 100644 --- a/i18n/zh-CN/cosmic_term.ftl +++ b/i18n/zh-CN/cosmic_term.ftl @@ -117,3 +117,30 @@ passwords-title = 密码 add-password = 添加密码 password-input = 密码 password-input-description = 描述 +cancel = 取消 +close-window = 关闭窗口 +disable = 禁用 +shortcut-group-window = 窗口 +add-another-keybinding = 添加另一个快捷键 +keyboard-shortcuts = 键盘快捷键 +menu-keyboard-shortcuts = 键盘快捷键… +no-shortcuts = 无快捷键 +password-manager = 密码管理器 +replace = 替换 +reset-to-default = 重置为默认值 +shortcut-group-clipboard = 剪切板 +shortcut-group-other = 其他 +shortcut-group-tabs = 标签 +shortcut-group-zoom = 缩放 +type-to-search = 输入即可搜索... +copy-or-sigint = 复制或 SIGINT +focus-pane-down = 聚焦下方窗格 +focus-pane-left = 聚焦左侧窗格 +focus-pane-right = 聚焦右侧窗格 +focus-pane-up = 聚焦上方窗格 +paste-primary = 粘贴主要 +shortcut-capture-hint = 按下组合键 +shortcut-replace-body = { $binding } 已分配给 { $existing }。是否将其替换为 { $new_action }? +shortcut-replace-title = 是否替换快捷键? +tab-activate = 启用标签 { $number } +toggle-fullscreen = 切换全屏 diff --git a/res/icons/edit-undo-symbolic.svg b/res/icons/edit-undo-symbolic.svg new file mode 100644 index 0000000..7400969 --- /dev/null +++ b/res/icons/edit-undo-symbolic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/config.rs b/src/config.rs index 9963994..39e6568 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::sync::OnceLock; -use crate::{fl, localize::LANGUAGE_SORTER}; +use crate::{fl, localize::LANGUAGE_SORTER, shortcuts::Shortcuts}; pub const CONFIG_VERSION: u64 = 1; pub const COSMIC_THEME_DARK: &str = "COSMIC Dark"; @@ -236,6 +236,8 @@ pub struct Config { pub syntax_theme_light: String, pub focus_follow_mouse: bool, pub default_profile: Option, + #[serde(default)] + pub shortcuts_custom: Shortcuts, } impl Default for Config { @@ -259,6 +261,7 @@ impl Default for Config { syntax_theme_light: COSMIC_THEME_LIGHT.to_string(), use_bright_bold: false, default_profile: None, + shortcuts_custom: Shortcuts::default(), } } } diff --git a/src/icon_cache.rs b/src/icon_cache.rs index 59c9e0b..21a7dc9 100644 --- a/src/icon_cache.rs +++ b/src/icon_cache.rs @@ -33,6 +33,7 @@ impl IconCache { bundle!("dialog-error-symbolic", 16); bundle!("edit-clear-symbolic", 16); bundle!("edit-delete-symbolic", 16); + bundle!("edit-undo-symbolic", 16); bundle!("list-add-symbolic", 16); bundle!("go-down-symbolic", 16); bundle!("go-up-symbolic", 16); diff --git a/src/key_bind.rs b/src/key_bind.rs index 900f7c9..86f3d39 100644 --- a/src/key_bind.rs +++ b/src/key_bind.rs @@ -1,87 +1,9 @@ -use cosmic::widget::menu::key_bind::{KeyBind, Modifier}; -use cosmic::{iced::keyboard::Key, iced_core::keyboard::key::Named}; +use cosmic::widget::menu::key_bind::KeyBind; use std::collections::HashMap; use crate::Action; +use crate::shortcuts::ShortcutsConfig; -//TODO: load from config -pub fn key_binds() -> HashMap { - let mut key_binds = HashMap::new(); - - macro_rules! bind { - ([$($modifier:ident),* $(,)?], $key:expr, $action:ident) => {{ - key_binds.insert( - KeyBind { - modifiers: vec![$(Modifier::$modifier),*], - key: $key, - }, - Action::$action, - ); - }}; - } - - // Standard key bindings - bind!([Ctrl, Shift], Key::Character("A".into()), SelectAll); - bind!([Ctrl, Shift], Key::Character("C".into()), Copy); - bind!([], Key::Named(Named::Copy), Copy); - bind!([Ctrl], Key::Character("c".into()), CopyOrSigint); - bind!([Ctrl, Shift], Key::Character("F".into()), Find); - bind!([Ctrl, Shift], Key::Character("N".into()), WindowNew); - bind!([Ctrl, Shift], Key::Character("Q".into()), WindowClose); - bind!([Ctrl, Shift], Key::Character("T".into()), TabNew); - bind!([Ctrl, Shift], Key::Character("V".into()), Paste); - bind!([], Key::Named(Named::Paste), Paste); - bind!([Shift], Key::Named(Named::Insert), PastePrimary); - bind!([Ctrl, Shift], Key::Character("W".into()), TabClose); - bind!([Ctrl], Key::Character(",".into()), Settings); - bind!([], Key::Named(Named::F11), ToggleFullscreen); - - // Ctrl+Alt+D splits horizontally, Ctrl+Alt+R splits vertically, Ctrl+Shift+X maximizes split - //TODO: Adjust bindings as desired by UX - bind!([Ctrl, Alt], Key::Character("d".into()), PaneSplitHorizontal); - bind!([Ctrl, Alt], Key::Character("r".into()), PaneSplitVertical); - bind!( - [Ctrl, Shift], - Key::Character("X".into()), - PaneToggleMaximized - ); - #[cfg(feature = "password_manager")] - bind!([Ctrl, Alt], Key::Character("p".into()), PasswordManager); - - // Ctrl+Tab and Ctrl+Shift+Tab cycle through tabs - // Ctrl+Tab is not a special key for terminals and is free to use - bind!([Ctrl], Key::Named(Named::Tab), TabNext); - bind!([Ctrl, Shift], Key::Named(Named::Tab), TabPrev); - - // Ctrl+Shift+# activates tabs by index - bind!([Ctrl, Shift], Key::Character("1".into()), TabActivate0); - bind!([Ctrl, Shift], Key::Character("2".into()), TabActivate1); - bind!([Ctrl, Shift], Key::Character("3".into()), TabActivate2); - bind!([Ctrl, Shift], Key::Character("4".into()), TabActivate3); - bind!([Ctrl, Shift], Key::Character("5".into()), TabActivate4); - bind!([Ctrl, Shift], Key::Character("6".into()), TabActivate5); - bind!([Ctrl, Shift], Key::Character("7".into()), TabActivate6); - bind!([Ctrl, Shift], Key::Character("8".into()), TabActivate7); - bind!([Ctrl, Shift], Key::Character("9".into()), TabActivate8); - - // Ctrl+0, Ctrl+-, and Ctrl+= are not special keys for terminals and are free to use - bind!([Ctrl], Key::Character("0".into()), ZoomReset); - bind!([Ctrl], Key::Character("-".into()), ZoomOut); - bind!([Ctrl], Key::Character("=".into()), ZoomIn); - bind!([Ctrl], Key::Character("+".into()), ZoomIn); - - // Ctrl+Arrows and Ctrl+HJKL move between splits - bind!([Ctrl, Shift], Key::Named(Named::ArrowLeft), PaneFocusLeft); - bind!([Ctrl, Shift], Key::Character("H".into()), PaneFocusLeft); - bind!([Ctrl, Shift], Key::Named(Named::ArrowDown), PaneFocusDown); - bind!([Ctrl, Shift], Key::Character("J".into()), PaneFocusDown); - bind!([Ctrl, Shift], Key::Named(Named::ArrowUp), PaneFocusUp); - bind!([Ctrl, Shift], Key::Character("K".into()), PaneFocusUp); - bind!([Ctrl, Shift], Key::Named(Named::ArrowRight), PaneFocusRight); - bind!([Ctrl, Shift], Key::Character("L".into()), PaneFocusRight); - - // CTRL+Alt+L clears the scrollback. - bind!([Ctrl, Alt], Key::Character("L".into()), ClearScrollback); - - key_binds +pub fn key_binds(shortcuts: &ShortcutsConfig) -> HashMap { + shortcuts.key_binds() } diff --git a/src/main.rs b/src/main.rs index 0ff88c3..0984566 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use alacritty_terminal::tty::Options; use alacritty_terminal::{event::Event as TermEvent, term, term::color::Colors as TermColors, tty}; use cosmic::iced::clipboard::dnd::DndAction; +use cosmic::iced_core::keyboard::key::Named; use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::pane_grid::Pane; @@ -31,6 +32,7 @@ use cosmic_text::{Family, Stretch, Weight, fontdb::FaceInfo}; use localize::LANGUAGE_SORTER; use std::{ any::TypeId, + cell::Cell, cmp, collections::{BTreeMap, BTreeSet, HashMap}, env, @@ -54,6 +56,8 @@ mod icon_cache; use key_bind::key_binds; mod key_bind; +mod shortcuts; + mod localize; use menu::menu_bar; @@ -161,6 +165,8 @@ fn main() -> Result<(), Box> { } }; + let shortcuts_config = shortcuts::ShortcutsConfig::new(config.shortcuts_custom.clone()); + let startup_options = if let Some(shell_program) = shell_program_opt { let options = tty::Options { shell: Some(tty::Shell::new(shell_program, shell_args)), @@ -189,6 +195,7 @@ fn main() -> Result<(), Box> { let flags = Flags { config_handler, config, + shortcuts_config, startup_options, term_config, }; @@ -215,6 +222,7 @@ Options: pub struct Flags { config_handler: Option, config: Config, + shortcuts_config: shortcuts::ShortcutsConfig, startup_options: Option, term_config: term::Config, } @@ -228,6 +236,7 @@ pub enum Action { CopyOrSigint, CopyPrimary, Find, + KeyboardShortcuts, LaunchUrlByMenu, PaneFocusDown, PaneFocusLeft, @@ -279,6 +288,7 @@ impl Action { Self::CopyOrSigint => Message::CopyOrSigint(entity_opt), Self::CopyPrimary => Message::CopyPrimary(entity_opt), Self::Find => Message::Find(true), + Self::KeyboardShortcuts => Message::ToggleContextPage(ContextPage::KeyboardShortcuts), Self::LaunchUrlByMenu => Message::LaunchUrlByMenu, Self::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down), Self::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left), @@ -366,6 +376,13 @@ pub enum Message { LaunchUrl(String), LaunchUrlByMenu, Modifiers(Modifiers), + ShortcutCaptureCancel, + ShortcutCaptureStart(shortcuts::KeyBindAction), + ShortcutConflictCancel, + ShortcutConflictReplace, + ShortcutRemove(shortcuts::Binding, shortcuts::BindingSource), + ShortcutReset(shortcuts::KeyBindAction), + ShortcutSearch(String), MouseEnter(pane_grid::Pane), Opacity(u8), PaneClicked(pane_grid::Pane), @@ -427,12 +444,20 @@ pub enum Message { pub enum ContextPage { About, ColorSchemes(ColorSchemeKind), + KeyboardShortcuts, Profiles, Settings, #[cfg(feature = "password_manager")] PasswordManager, } +#[derive(Clone, Debug)] +struct ShortcutConflict { + binding: shortcuts::Binding, + existing_action: shortcuts::KeyBindAction, + new_action: shortcuts::KeyBindAction, +} + /// The [`App`] stores application-specific state. pub struct App { core: Core, @@ -440,6 +465,7 @@ pub struct App { pane_model: TerminalPaneGrid, config_handler: Option, config: Config, + shortcuts_config: shortcuts::ShortcutsConfig, key_binds: HashMap, app_themes: Vec, font_names: Vec, @@ -474,6 +500,13 @@ pub struct App { color_scheme_tab_model: widget::segmented_button::SingleSelectModel, profile_expanded: Option, show_advanced_font_settings: bool, + shortcut_capture: Option, + shortcut_conflict: Option, + shortcut_conflict_overlay_restore: Option, + shortcut_search_focus: Cell, + shortcut_search_id: widget::Id, + shortcut_search_regex: Option, + shortcut_search_value: String, modifiers: Modifiers, #[cfg(feature = "password_manager")] password_mgr: password_manager::PasswordManager, @@ -545,6 +578,63 @@ impl App { } } + fn save_shortcuts_custom(&mut self) { + self.config.shortcuts_custom = self.shortcuts_config.custom.clone(); + match &self.config_handler { + Some(config_handler) => { + if let Err(err) = + config_handler.set("shortcuts_custom", &self.config.shortcuts_custom) + { + log::warn!("failed to save shortcuts custom config: {}", err); + } + } + None => { + log::warn!("failed to save shortcuts custom config: no config handler"); + } + } + self.key_binds = key_binds(&self.shortcuts_config); + } + + fn apply_shortcut_binding( + &mut self, + binding: shortcuts::Binding, + action: shortcuts::KeyBindAction, + ) { + self.shortcuts_config.custom.0.insert(binding, action); + self.save_shortcuts_custom(); + } + + fn set_context_overlay(&mut self, overlay: bool) { + if self.core.window.context_is_overlay != overlay { + self.core.window.context_is_overlay = overlay; + self.core.set_show_context(self.core.window.show_context); + } + } + + fn begin_shortcut_conflict(&mut self, conflict: ShortcutConflict) { + if self.shortcut_conflict.is_none() { + self.shortcut_conflict_overlay_restore = Some(self.core.window.context_is_overlay); + self.set_context_overlay(false); + } + self.shortcut_conflict = Some(conflict); + } + + fn clear_shortcut_conflict(&mut self) { + self.shortcut_conflict = None; + if let Some(overlay) = self.shortcut_conflict_overlay_restore.take() { + self.set_context_overlay(overlay); + } + } + + fn shortcut_page_toggle(&mut self) { + self.shortcut_capture = None; + self.clear_shortcut_conflict(); + self.shortcut_search_focus + .set(self.core.window.show_context); + self.shortcut_search_regex = None; + self.shortcut_search_value.clear(); + } + fn update_config(&mut self) -> Task { let theme = self.config.app_theme.theme(); @@ -640,7 +730,16 @@ impl App { if self.find { widget::text_input::focus(self.find_search_id.clone()) } else if self.core.window.show_context { - // TODO focus the context page? + match self.context_page { + ContextPage::KeyboardShortcuts => { + if self.shortcut_search_focus.get() { + self.shortcut_search_focus.set(false); + return widget::text_input::focus(self.shortcut_search_id.clone()); + } + } + // TODO focus for other context pages? + _ => {} + } Task::none() } else if let Some(terminal_id) = self.terminal_ids.get(&self.pane_model.focused()).cloned() { @@ -872,6 +971,126 @@ impl App { widget::settings::view_column(sections).into() } + fn keyboard_shortcuts(&self) -> Element<'_, Message> { + let cosmic_theme::Spacing { + space_xxs, + space_s, + space_m, + space_l, + space_xl, + .. + } = self.core().system_theme().cosmic().spacing; + + let pad_action = [space_xxs, space_m]; + let div_action = space_s; + let pad_binding = [space_xxs, space_xl]; + let div_binding = space_l; + + let mut groups = Vec::new(); + //TODO: fix text input focus going outside bounds + groups.push(widget::horizontal_space().into()); + groups.push( + widget::text_input::search_input(fl!("type-to-search"), &self.shortcut_search_value) + .id(self.shortcut_search_id.clone()) + .on_input(Message::ShortcutSearch) + .into(), + ); + + for group in shortcuts::shortcut_groups() { + let mut list = widget::list::list_column(); + + let mut found_actions = false; + for action in group.actions { + let action_label = shortcuts::action_label(action); + if let Some(regex) = &self.shortcut_search_regex { + if regex.find(&action_label).is_none() { + continue; + } + } + found_actions = true; + + let (bindings, changed) = self.shortcuts_config.bindings_for_action(action); + + let mut buttons = widget::row::with_capacity(2); + if changed { + buttons = buttons.push(widget::tooltip( + widget::button::custom(icon_cache_get("edit-undo-symbolic", 16)) + .class(style::Button::Icon) + .on_press(Message::ShortcutReset(action)), + widget::text::body(fl!("reset-to-default")), + widget::tooltip::Position::Top, + )); + } + buttons = buttons.push(widget::tooltip( + widget::button::custom(icon_cache_get("list-add-symbolic", 16)) + .class(style::Button::Icon) + .on_press(Message::ShortcutCaptureStart(action)), + widget::text::body(fl!("add-another-keybinding")), + widget::tooltip::Position::Top, + )); + + list = list.list_item_padding(pad_action); + list = list.divider_padding(div_action); + list = list.add(widget::settings::item_row(vec![ + widget::text::heading(action_label) + .width(Length::Fill) + .into(), + buttons.into(), + ])); + + if bindings.is_empty() { + list = list.list_item_padding(pad_binding); + list = list.add(widget::text::body(fl!("no-shortcuts"))); + list = list.divider_padding(div_binding); + } else { + for resolved in bindings { + list = list.list_item_padding(pad_binding); + list = list.add( + widget::settings::item::builder(shortcuts::binding_display( + &resolved.binding, + )) + .control( + widget::button::custom(icon_cache_get("edit-delete-symbolic", 16)) + .class(style::Button::Icon) + .on_press(Message::ShortcutRemove( + resolved.binding.clone(), + resolved.source, + )), + ), + ); + list = list.divider_padding(div_binding); + } + } + + if self.shortcut_capture == Some(action) { + list = list.list_item_padding(pad_binding); + list = list.add( + widget::settings::item_row(vec![ + widget::text::body(fl!("shortcut-capture-hint")) + .width(Length::Fill) + .into(), + widget::button::text(fl!("cancel")) + .on_press(Message::ShortcutCaptureCancel) + .into(), + ]) + .spacing(space_xxs), + ); + list = list.divider_padding(div_binding); + } + } + + if found_actions { + groups.push( + widget::settings::section::with_column(list) + .title(group.title) + .into(), + ); + } + } + + widget::settings::view_column(groups).into() + } + fn profiles(&self) -> Element<'_, Message> { let cosmic_theme::Spacing { space_s, @@ -1553,13 +1772,15 @@ impl Application for App { ), ]); + let key_binds = key_binds(&flags.shortcuts_config); let mut app = Self { core, about, pane_model, config_handler: flags.config_handler, config: flags.config, - key_binds: key_binds(), + shortcuts_config: flags.shortcuts_config, + key_binds, app_themes, font_names, font_size_names, @@ -1592,6 +1813,13 @@ impl Application for App { color_scheme_tab_model: widget::segmented_button::Model::default(), profile_expanded: None, show_advanced_font_settings: false, + shortcut_capture: None, + shortcut_conflict: None, + shortcut_conflict_overlay_restore: None, + shortcut_search_focus: Cell::new(true), + shortcut_search_id: widget::Id::unique(), + shortcut_search_regex: None, + shortcut_search_value: String::new(), modifiers: Modifiers::empty(), #[cfg(feature = "password_manager")] password_mgr: Default::default(), @@ -1606,12 +1834,20 @@ impl Application for App { //TODO: currently the first escape unfocuses, and the second calls this function fn on_escape(&mut self) -> Task { if self.core.window.show_context { - // Close context drawer if open - self.core.window.show_context = false; - #[cfg(feature = "password_manager")] - if self.context_page == ContextPage::PasswordManager { - self.password_mgr.clear(); + // Handle keyboard shortcut page escape + if let ContextPage::KeyboardShortcuts = self.context_page { + // Cancel shortcut capture + if self.shortcut_capture.take().is_some() { + return Task::none(); + } + + // Cancel shortcut conflict dialog + if self.shortcut_conflict.take().is_some() { + return Task::none(); + } } + + return self.update(Message::ToggleContextPage(self.context_page)); } else if self.find { // Close find if open self.find = false; @@ -1873,9 +2109,15 @@ impl Application for App { } Message::Config(config) => { if config != self.config { + let shortcuts_changed = config.shortcuts_custom != self.config.shortcuts_custom; log::info!("update config"); //TODO: update syntax theme by clearing tabs, only if needed self.config = config; + if shortcuts_changed { + self.shortcuts_config = + shortcuts::ShortcutsConfig::new(self.config.shortcuts_custom.clone()); + self.key_binds = key_binds(&self.shortcuts_config); + } return self.update_config(); } } @@ -2117,6 +2359,44 @@ impl Application for App { config_set!(focus_follow_mouse, focus_follow_mouse); } Message::Key(modifiers, key) => { + // Hard-coded keys + match key { + Key::Named(Named::Copy) => { + return self.update(Message::Copy(None)); + } + Key::Named(Named::Paste) => { + return self.update(Message::Paste(None)); + } + Key::Named(Named::Escape) => { + // Handled by on_escape + return Task::none(); + } + _ => {} + } + + // Handle shortcut capture + if let Some(action) = self.shortcut_capture { + if let Some(binding) = shortcuts::binding_from_key(modifiers, key) { + self.shortcut_capture = None; + if let Some(existing_action) = + self.shortcuts_config.action_for_binding(&binding) + { + if existing_action != action { + self.begin_shortcut_conflict(ShortcutConflict { + binding, + existing_action, + new_action: action, + }); + return Task::none(); + } + return Task::none(); + } + self.apply_shortcut_binding(binding, action); + } + return Task::none(); + } + + // Handle configurable keys for (key_bind, action) in &self.key_binds { if key_bind.matches(modifiers, &key) { return self.update(action.message(None)); @@ -2152,6 +2432,59 @@ impl Application for App { self.pane_model.set_focus(pane); return self.update_focus(); } + Message::ShortcutCaptureCancel => { + self.shortcut_capture = None; + } + Message::ShortcutCaptureStart(action) => { + self.shortcut_capture = Some(action); + } + Message::ShortcutConflictCancel => { + self.clear_shortcut_conflict(); + } + Message::ShortcutConflictReplace => { + if let Some(conflict) = self.shortcut_conflict.clone() { + self.apply_shortcut_binding(conflict.binding, conflict.new_action); + } + self.clear_shortcut_conflict(); + } + Message::ShortcutRemove(binding, source) => { + match source { + shortcuts::BindingSource::Default => { + self.shortcuts_config + .custom + .0 + .insert(binding, shortcuts::KeyBindAction::Disable); + } + shortcuts::BindingSource::Custom => { + self.shortcuts_config.custom.0.remove(&binding); + } + } + self.save_shortcuts_custom(); + } + Message::ShortcutReset(reset_action) => { + self.shortcuts_config.reset_action(reset_action); + self.save_shortcuts_custom(); + } + Message::ShortcutSearch(search) => { + self.shortcut_search_focus.set(true); + self.shortcut_search_regex = None; + if !search.is_empty() { + let pattern = regex::escape(&search); + match regex::RegexBuilder::new(&pattern) + .case_insensitive(true) + .build() + { + Ok(regex) => { + self.shortcut_search_regex = Some(regex); + } + Err(err) => { + log::warn!("failed to parse regex {:?}: {}", pattern, err); + } + }; + } + self.shortcut_search_value = search; + return self.update_focus(); + } Message::Opacity(opacity) => { config_set!(opacity, cmp::min(100, opacity)); } @@ -2664,6 +2997,10 @@ impl Application for App { self.core.window.show_context = !self.core.window.show_context; self.pane_model.update_terminal_focus(); + if let ContextPage::KeyboardShortcuts = context_page { + self.shortcut_page_toggle(); + } + #[cfg(feature = "password_manager")] if ContextPage::PasswordManager == context_page { if self.core.window.show_context { @@ -2705,6 +3042,11 @@ impl Application for App { }); } + if let ContextPage::KeyboardShortcuts = context_page { + self.shortcut_page_toggle(); + return self.update_focus(); + } + #[cfg(feature = "password_manager")] if ContextPage::PasswordManager == context_page { self.password_mgr.pane = Some(self.pane_model.focused()); @@ -2789,6 +3131,11 @@ impl Application for App { Message::ToggleContextPage(ContextPage::ColorSchemes(color_scheme_kind)), ) .title(fl!("color-schemes")), + ContextPage::KeyboardShortcuts => context_drawer::context_drawer( + self.keyboard_shortcuts(), + Message::ToggleContextPage(ContextPage::KeyboardShortcuts), + ) + .title(fl!("keyboard-shortcuts")), ContextPage::Profiles => context_drawer::context_drawer( self.profiles(), Message::ToggleContextPage(ContextPage::Profiles), @@ -2808,6 +3155,34 @@ impl Application for App { }) } + fn dialog(&self) -> Option> { + let conflict = self.shortcut_conflict.as_ref()?; + let binding = shortcuts::binding_display(&conflict.binding); + let existing = shortcuts::action_label(conflict.existing_action); + let new_action = shortcuts::action_label(conflict.new_action); + let body = fl!( + "shortcut-replace-body", + binding = binding.as_str(), + existing = existing.as_str(), + new_action = new_action.as_str() + ); + + Some( + widget::dialog() + .title(fl!("shortcut-replace-title")) + .body(body) + .primary_action( + widget::button::suggested(fl!("replace")) + .on_press(Message::ShortcutConflictReplace), + ) + .secondary_action( + widget::button::standard(fl!("cancel")) + .on_press(Message::ShortcutConflictCancel), + ) + .into(), + ) + } + fn header_start(&self) -> Vec> { vec![menu_bar(&self.core, &self.config, &self.key_binds)] } @@ -2861,7 +3236,7 @@ impl Application for App { .cloned() .unwrap_or_else(widget::Id::unique); if let Some(terminal) = tab_model.data::>(entity) { - let mut terminal_box = terminal_box(terminal) + let mut terminal_box = terminal_box(terminal, &self.key_binds) .id(terminal_id) .disabled(self.core.window.show_context) .on_context_menu(move |menu_state| Message::TabContextMenu(pane, menu_state)) diff --git a/src/menu.rs b/src/menu.rs index 0e11dd2..520a608 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -265,6 +265,11 @@ pub fn menu_bar<'a>( None, Action::ColorSchemes(config.color_scheme_kind()), ), + MenuItem::Button( + fl!("menu-keyboard-shortcuts"), + None, + Action::KeyboardShortcuts, + ), MenuItem::Button(fl!("menu-settings"), None, Action::Settings), #[cfg(feature = "password_manager")] MenuItem::Button( diff --git a/src/shortcuts.rs b/src/shortcuts.rs new file mode 100644 index 0000000..427ed19 --- /dev/null +++ b/src/shortcuts.rs @@ -0,0 +1,557 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic::widget::menu::key_bind::{KeyBind, Modifier}; +use cosmic::{ + iced::keyboard::{Key, Modifiers}, + iced_core::keyboard::key::Named, +}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; + +use crate::{Action, fl}; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub enum ModifierName { + Ctrl, + Shift, + Alt, + Super, +} + +impl ModifierName { + fn to_modifier(self) -> Modifier { + match self { + Self::Ctrl => Modifier::Ctrl, + Self::Shift => Modifier::Shift, + Self::Alt => Modifier::Alt, + Self::Super => Modifier::Super, + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub struct Binding { + pub modifiers: Vec, + pub key: String, +} + +impl Binding { + fn to_key_bind(&self) -> Option { + let key = key_from_string(&self.key)?; + let mut modifiers = Vec::new(); + for modifier in [ + ModifierName::Ctrl, + ModifierName::Shift, + ModifierName::Alt, + ModifierName::Super, + ] { + if self.modifiers.contains(&modifier) { + modifiers.push(modifier.to_modifier()); + } + } + + Some(KeyBind { modifiers, key }) + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum KeyBindAction { + Disable, + ClearScrollback, + Copy, + CopyOrSigint, + Find, + PaneFocusDown, + PaneFocusLeft, + PaneFocusRight, + PaneFocusUp, + PaneSplitHorizontal, + PaneSplitVertical, + PaneToggleMaximized, + Paste, + PastePrimary, + #[cfg_attr(not(feature = "password_manager"), allow(dead_code))] + PasswordManager, + SelectAll, + Settings, + TabActivate0, + TabActivate1, + TabActivate2, + TabActivate3, + TabActivate4, + TabActivate5, + TabActivate6, + TabActivate7, + TabActivate8, + TabClose, + TabNew, + TabNext, + TabPrev, + ToggleFullscreen, + WindowClose, + WindowNew, + ZoomIn, + ZoomOut, + ZoomReset, +} + +impl KeyBindAction { + fn to_action(self) -> Option { + match self { + Self::Disable => None, + Self::ClearScrollback => Some(Action::ClearScrollback), + Self::Copy => Some(Action::Copy), + Self::CopyOrSigint => Some(Action::CopyOrSigint), + Self::Find => Some(Action::Find), + Self::PaneFocusDown => Some(Action::PaneFocusDown), + Self::PaneFocusLeft => Some(Action::PaneFocusLeft), + Self::PaneFocusRight => Some(Action::PaneFocusRight), + Self::PaneFocusUp => Some(Action::PaneFocusUp), + Self::PaneSplitHorizontal => Some(Action::PaneSplitHorizontal), + Self::PaneSplitVertical => Some(Action::PaneSplitVertical), + Self::PaneToggleMaximized => Some(Action::PaneToggleMaximized), + Self::Paste => Some(Action::Paste), + Self::PastePrimary => Some(Action::PastePrimary), + Self::SelectAll => Some(Action::SelectAll), + Self::Settings => Some(Action::Settings), + Self::TabActivate0 => Some(Action::TabActivate0), + Self::TabActivate1 => Some(Action::TabActivate1), + Self::TabActivate2 => Some(Action::TabActivate2), + Self::TabActivate3 => Some(Action::TabActivate3), + Self::TabActivate4 => Some(Action::TabActivate4), + Self::TabActivate5 => Some(Action::TabActivate5), + Self::TabActivate6 => Some(Action::TabActivate6), + Self::TabActivate7 => Some(Action::TabActivate7), + Self::TabActivate8 => Some(Action::TabActivate8), + Self::TabClose => Some(Action::TabClose), + Self::TabNew => Some(Action::TabNew), + Self::TabNext => Some(Action::TabNext), + Self::TabPrev => Some(Action::TabPrev), + Self::ToggleFullscreen => Some(Action::ToggleFullscreen), + Self::WindowClose => Some(Action::WindowClose), + Self::WindowNew => Some(Action::WindowNew), + Self::ZoomIn => Some(Action::ZoomIn), + Self::ZoomOut => Some(Action::ZoomOut), + Self::ZoomReset => Some(Action::ZoomReset), + Self::PasswordManager => { + #[cfg(feature = "password_manager")] + { + Some(Action::PasswordManager) + } + #[cfg(not(feature = "password_manager"))] + { + None + } + } + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(transparent)] +pub struct Shortcuts(pub BTreeMap); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BindingSource { + Default, + Custom, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ResolvedBinding { + pub binding: Binding, + pub source: BindingSource, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ShortcutsConfig { + defaults: Shortcuts, + pub custom: Shortcuts, +} + +impl ShortcutsConfig { + pub fn new(custom: Shortcuts) -> Self { + Self { + defaults: fallback_shortcuts(), + custom, + } + } + + pub fn key_binds(&self) -> HashMap { + let mut binds = HashMap::new(); + insert_shortcuts(&self.defaults, &mut binds, false); + insert_shortcuts(&self.custom, &mut binds, true); + binds + } + + pub fn bindings_for_action(&self, action: KeyBindAction) -> (Vec, bool) { + let mut bindings = Vec::new(); + + let mut changed = false; + for (binding, default_action) in &self.defaults.0 { + if *default_action != action { + continue; + } + + match self.custom.0.get(binding) { + Some(KeyBindAction::Disable) => { + changed = true; + } + Some(custom_action) => { + if *custom_action == action { + bindings.push(ResolvedBinding { + binding: binding.clone(), + source: BindingSource::Custom, + }); + changed = true; + } + } + None => bindings.push(ResolvedBinding { + binding: binding.clone(), + source: BindingSource::Default, + }), + } + } + + for (binding, custom_action) in &self.custom.0 { + if *custom_action == action + && !bindings.iter().any(|resolved| resolved.binding == *binding) + { + bindings.push(ResolvedBinding { + binding: binding.clone(), + source: BindingSource::Custom, + }); + changed = true; + } + } + + (bindings, changed) + } + + pub fn action_for_binding(&self, binding: &Binding) -> Option { + if let Some(action) = self.custom.0.get(binding) { + if *action == KeyBindAction::Disable { + return None; + } + return Some(*action); + } + + self.defaults.0.get(binding).copied() + } + + pub fn reset_action(&mut self, reset_action: KeyBindAction) { + self.custom.0.retain(|binding, action| { + if *action == reset_action { + // Remove any matching bindings + return false; + } + if let Some(default_action) = self.defaults.0.get(binding) { + if *default_action == reset_action { + // Remove binding that overrode a default + return false; + } + } + true + }); + } +} + +pub fn action_label(action: KeyBindAction) -> String { + match action { + KeyBindAction::Disable => fl!("disable"), + KeyBindAction::ClearScrollback => fl!("clear-scrollback"), + KeyBindAction::Copy => fl!("copy"), + KeyBindAction::CopyOrSigint => fl!("copy-or-sigint"), + KeyBindAction::Find => fl!("find"), + KeyBindAction::PaneFocusDown => fl!("focus-pane-down"), + KeyBindAction::PaneFocusLeft => fl!("focus-pane-left"), + KeyBindAction::PaneFocusRight => fl!("focus-pane-right"), + KeyBindAction::PaneFocusUp => fl!("focus-pane-up"), + KeyBindAction::PaneSplitHorizontal => fl!("split-horizontal"), + KeyBindAction::PaneSplitVertical => fl!("split-vertical"), + KeyBindAction::PaneToggleMaximized => fl!("pane-toggle-maximize"), + KeyBindAction::Paste => fl!("paste"), + KeyBindAction::PastePrimary => fl!("paste-primary"), + KeyBindAction::PasswordManager => fl!("password-manager"), + KeyBindAction::SelectAll => fl!("select-all"), + KeyBindAction::Settings => fl!("settings"), + KeyBindAction::TabActivate0 => fl!("tab-activate", number = 1), + KeyBindAction::TabActivate1 => fl!("tab-activate", number = 2), + KeyBindAction::TabActivate2 => fl!("tab-activate", number = 3), + KeyBindAction::TabActivate3 => fl!("tab-activate", number = 4), + KeyBindAction::TabActivate4 => fl!("tab-activate", number = 5), + KeyBindAction::TabActivate5 => fl!("tab-activate", number = 6), + KeyBindAction::TabActivate6 => fl!("tab-activate", number = 7), + KeyBindAction::TabActivate7 => fl!("tab-activate", number = 8), + KeyBindAction::TabActivate8 => fl!("tab-activate", number = 9), + KeyBindAction::TabClose => fl!("close-tab"), + KeyBindAction::TabNew => fl!("new-tab"), + KeyBindAction::TabNext => fl!("next-tab"), + KeyBindAction::TabPrev => fl!("previous-tab"), + KeyBindAction::ToggleFullscreen => fl!("toggle-fullscreen"), + KeyBindAction::WindowClose => fl!("close-window"), + KeyBindAction::WindowNew => fl!("new-window"), + KeyBindAction::ZoomIn => fl!("zoom-in"), + KeyBindAction::ZoomOut => fl!("zoom-out"), + KeyBindAction::ZoomReset => fl!("zoom-reset"), + } +} + +pub struct ShortcutGroup { + pub title: String, + pub actions: Vec, +} + +pub fn shortcut_groups() -> Vec { + let mut groups = Vec::new(); + groups.push(ShortcutGroup { + title: fl!("shortcut-group-clipboard"), + actions: vec![ + KeyBindAction::SelectAll, + KeyBindAction::Copy, + KeyBindAction::CopyOrSigint, + KeyBindAction::Paste, + KeyBindAction::PastePrimary, + KeyBindAction::Find, + ], + }); + groups.push(ShortcutGroup { + title: fl!("shortcut-group-tabs"), + actions: vec![ + KeyBindAction::TabNew, + KeyBindAction::TabClose, + KeyBindAction::TabNext, + KeyBindAction::TabPrev, + KeyBindAction::TabActivate0, + KeyBindAction::TabActivate1, + KeyBindAction::TabActivate2, + KeyBindAction::TabActivate3, + KeyBindAction::TabActivate4, + KeyBindAction::TabActivate5, + KeyBindAction::TabActivate6, + KeyBindAction::TabActivate7, + KeyBindAction::TabActivate8, + ], + }); + groups.push(ShortcutGroup { + title: fl!("splits"), + actions: vec![ + KeyBindAction::PaneSplitHorizontal, + KeyBindAction::PaneSplitVertical, + KeyBindAction::PaneToggleMaximized, + KeyBindAction::PaneFocusLeft, + KeyBindAction::PaneFocusRight, + KeyBindAction::PaneFocusUp, + KeyBindAction::PaneFocusDown, + ], + }); + groups.push(ShortcutGroup { + title: fl!("shortcut-group-window"), + actions: vec![ + KeyBindAction::WindowNew, + KeyBindAction::WindowClose, + KeyBindAction::ToggleFullscreen, + KeyBindAction::Settings, + ], + }); + groups.push(ShortcutGroup { + title: fl!("shortcut-group-zoom"), + actions: vec![ + KeyBindAction::ZoomIn, + KeyBindAction::ZoomOut, + KeyBindAction::ZoomReset, + ], + }); + let mut other_actions = vec![KeyBindAction::ClearScrollback]; + #[cfg(feature = "password_manager")] + other_actions.push(KeyBindAction::PasswordManager); + groups.push(ShortcutGroup { + title: fl!("shortcut-group-other"), + actions: other_actions, + }); + groups +} + +pub fn binding_display(binding: &Binding) -> String { + binding + .to_key_bind() + .map(|key_bind| key_bind.to_string()) + .unwrap_or_else(|| binding.key.clone()) +} + +pub fn binding_from_key(modifiers: Modifiers, key: Key) -> Option { + if is_modifier_only_key(&key) { + return None; + } + let key = key_to_string(&key)?; + let mut binding_modifiers = Vec::new(); + if modifiers.control() { + binding_modifiers.push(ModifierName::Ctrl); + } + if modifiers.shift() { + binding_modifiers.push(ModifierName::Shift); + } + if modifiers.alt() { + binding_modifiers.push(ModifierName::Alt); + } + if modifiers.logo() { + binding_modifiers.push(ModifierName::Super); + } + Some(Binding { + modifiers: binding_modifiers, + key, + }) +} + +fn insert_shortcuts( + shortcuts: &Shortcuts, + binds: &mut HashMap, + allow_disable: bool, +) { + for (binding, action) in &shortcuts.0 { + let key_bind = match binding.to_key_bind() { + Some(key_bind) => key_bind, + None => { + log::warn!("invalid key binding: {:?}", binding); + continue; + } + }; + if allow_disable && *action == KeyBindAction::Disable { + binds.remove(&key_bind); + continue; + } + let Some(action) = action.to_action() else { + log::warn!("unsupported shortcut action: {:?}", action); + continue; + }; + binds.insert(key_bind, action); + } +} + +fn fallback_shortcuts() -> Shortcuts { + let mut shortcuts = BTreeMap::new(); + + macro_rules! bind { + ([$($modifier:ident),* $(,)?], $key:expr, $action:ident) => {{ + shortcuts.insert( + Binding { + modifiers: vec![$(ModifierName::$modifier),*], + key: $key.to_string(), + }, + KeyBindAction::$action, + ); + }}; + } + + // Standard key bindings + bind!([Ctrl, Shift], "A", SelectAll); + bind!([Ctrl, Shift], "C", Copy); + bind!([Ctrl], "c", CopyOrSigint); + bind!([Ctrl, Shift], "F", Find); + bind!([Ctrl, Shift], "N", WindowNew); + bind!([Ctrl, Shift], "Q", WindowClose); + bind!([Ctrl, Shift], "T", TabNew); + bind!([Ctrl, Shift], "V", Paste); + bind!([Shift], "Insert", PastePrimary); + bind!([Ctrl, Shift], "W", TabClose); + bind!([Ctrl], ",", Settings); + bind!([], "F11", ToggleFullscreen); + + // Ctrl+Alt+D splits horizontally, Ctrl+Alt+R splits vertically, Ctrl+Shift+X maximizes split + //TODO: Adjust bindings as desired by UX + bind!([Ctrl, Alt], "d", PaneSplitHorizontal); + bind!([Ctrl, Alt], "r", PaneSplitVertical); + bind!([Ctrl, Shift], "X", PaneToggleMaximized); + #[cfg(feature = "password_manager")] + bind!([Ctrl, Alt], "p", PasswordManager); + + // Ctrl+Tab and Ctrl+Shift+Tab cycle through tabs + // Ctrl+Tab is not a special key for terminals and is free to use + bind!([Ctrl], "Tab", TabNext); + bind!([Ctrl, Shift], "Tab", TabPrev); + + // Ctrl+Shift+# activates tabs by index + bind!([Ctrl, Shift], "1", TabActivate0); + bind!([Ctrl, Shift], "2", TabActivate1); + bind!([Ctrl, Shift], "3", TabActivate2); + bind!([Ctrl, Shift], "4", TabActivate3); + bind!([Ctrl, Shift], "5", TabActivate4); + bind!([Ctrl, Shift], "6", TabActivate5); + bind!([Ctrl, Shift], "7", TabActivate6); + bind!([Ctrl, Shift], "8", TabActivate7); + bind!([Ctrl, Shift], "9", TabActivate8); + + // Ctrl+0, Ctrl+-, and Ctrl+= are not special keys for terminals and are free to use + bind!([Ctrl], "0", ZoomReset); + bind!([Ctrl], "-", ZoomOut); + bind!([Ctrl], "=", ZoomIn); + bind!([Ctrl], "+", ZoomIn); + + // Ctrl+Arrows and Ctrl+HJKL move between splits + bind!([Ctrl, Shift], "ArrowLeft", PaneFocusLeft); + bind!([Ctrl, Shift], "H", PaneFocusLeft); + bind!([Ctrl, Shift], "ArrowDown", PaneFocusDown); + bind!([Ctrl, Shift], "J", PaneFocusDown); + bind!([Ctrl, Shift], "ArrowUp", PaneFocusUp); + bind!([Ctrl, Shift], "K", PaneFocusUp); + bind!([Ctrl, Shift], "ArrowRight", PaneFocusRight); + bind!([Ctrl, Shift], "L", PaneFocusRight); + + // CTRL+Alt+L clears the scrollback. + bind!([Ctrl, Alt], "L", ClearScrollback); + + Shortcuts(shortcuts) +} + +fn key_from_string(value: &str) -> Option { + match value { + "Insert" => Some(Key::Named(Named::Insert)), + "Tab" => Some(Key::Named(Named::Tab)), + "F11" => Some(Key::Named(Named::F11)), + "ArrowLeft" | "Left" => Some(Key::Named(Named::ArrowLeft)), + "ArrowRight" | "Right" => Some(Key::Named(Named::ArrowRight)), + "ArrowUp" | "Up" => Some(Key::Named(Named::ArrowUp)), + "ArrowDown" | "Down" => Some(Key::Named(Named::ArrowDown)), + "Space" | "space" => Some(Key::Character(" ".into())), + _ if !value.is_empty() => Some(Key::Character(value.into())), + _ => None, + } +} + +fn key_to_string(key: &Key) -> Option { + match key { + Key::Character(c) => { + if c == " " { + Some("Space".to_string()) + } else if c.len() == 1 && c.chars().all(|ch| ch.is_ascii_alphabetic()) { + Some(c.to_uppercase()) + } else { + Some(c.to_string()) + } + } + Key::Named(named) => Some(format!("{named:?}")), + _ => None, + } +} + +fn is_modifier_only_key(key: &Key) -> bool { + matches!( + key, + Key::Named( + Named::Alt + | Named::AltGraph + | Named::CapsLock + | Named::Control + | Named::Fn + | Named::FnLock + | Named::NumLock + | Named::ScrollLock + | Named::Shift + | Named::Symbol + | Named::SymbolLock + | Named::Meta + | Named::Hyper + | Named::Super + ) + ) +} diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 817e307..3dc1b8d 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -46,8 +46,8 @@ use std::{ }; use crate::{ - Action, Terminal, TerminalScroll, key_bind::key_binds, menu::MenuState, - mouse_reporter::MouseReporter, terminal::Metadata, + Action, Terminal, TerminalScroll, menu::MenuState, mouse_reporter::MouseReporter, + terminal::Metadata, }; const AUTOSCROLL_INTERVAL: Duration = Duration::from_millis(100); @@ -122,7 +122,7 @@ pub struct TerminalBox<'a, Message> { on_open_hyperlink: Option Message + 'a>>, on_window_focused: Option Message + 'a>>, on_window_unfocused: Option Message + 'a>>, - key_binds: HashMap, + key_binds: &'a HashMap, sharp_corners: bool, disabled: bool, } @@ -131,7 +131,7 @@ impl<'a, Message> TerminalBox<'a, Message> where Message: Clone, { - pub fn new(terminal: &'a Mutex) -> Self { + pub fn new(terminal: &'a Mutex, key_binds: &'a HashMap) -> Self { Self { terminal, id: None, @@ -145,7 +145,7 @@ where opacity: None, mouse_inside_boundary: None, on_middle_click: None, - key_binds: key_binds(), + key_binds, on_open_hyperlink: None, on_window_focused: None, on_window_unfocused: None, @@ -236,11 +236,14 @@ where } } -pub fn terminal_box(terminal: &Mutex) -> TerminalBox<'_, Message> +pub fn terminal_box<'a, Message>( + terminal: &'a Mutex, + key_binds: &'a HashMap, +) -> TerminalBox<'a, Message> where Message: Clone, { - TerminalBox::new(terminal) + TerminalBox::new(terminal, key_binds) } impl<'a, Message> Widget for TerminalBox<'a, Message> @@ -720,66 +723,75 @@ where state.scrollbar_rect.set(Rectangle::default()) } - // Draw cursor + // Draw cursor (only when not scrolled, as cursor is at bottom of active area) { - let cursor = terminal.term.lock().renderable_content().cursor; - let col = cursor.point.column.0; - let line = cursor.point.line.0; - let color = terminal.term.lock().colors()[NamedColor::Cursor] - .or(terminal.colors()[NamedColor::Cursor]) - .map(|rgb| Color::from_rgb8(rgb.r, rgb.g, rgb.b)) - .unwrap_or(Color::WHITE); // TODO default color from theme? - let width = terminal.size().cell_width; - let height = terminal.size().cell_height; - let top_left = view_position - + Vector::new((col as f32 * width).floor(), (line as f32 * height).floor()); - match cursor.shape { - CursorShape::Beam => { - let quad = Quad { - bounds: Rectangle::new(top_left, Size::new(1.0, height)), - ..Default::default() - }; - renderer.fill_quad(quad, color); - } - CursorShape::Underline => { - let quad = Quad { - bounds: Rectangle::new( - view_position - + Vector::new( - (col as f32 * width).floor(), - ((line + 1) as f32 * height).floor(), - ), - Size::new(width, 1.0), - ), - ..Default::default() - }; - renderer.fill_quad(quad, color); - } - CursorShape::Block if !state.is_focused => { - let quad = Quad { - bounds: Rectangle::new(top_left, Size::new(width, height)), - border: Border { - width: 1.0, - color, + let term = terminal.term.lock(); + let display_offset = term.grid().display_offset(); + let cursor = term.renderable_content().cursor; + drop(term); + + // Skip drawing cursor when scrolled - the cursor is below the visible viewport + if display_offset > 0 { + // Cursor is off-screen when scrolled up + } else { + let col = cursor.point.column.0; + let line = cursor.point.line.0; + let color = terminal.term.lock().colors()[NamedColor::Cursor] + .or(terminal.colors()[NamedColor::Cursor]) + .map(|rgb| Color::from_rgb8(rgb.r, rgb.g, rgb.b)) + .unwrap_or(Color::WHITE); // TODO default color from theme? + let width = terminal.size().cell_width; + let height = terminal.size().cell_height; + let top_left = view_position + + Vector::new((col as f32 * width).floor(), (line as f32 * height).floor()); + match cursor.shape { + CursorShape::Beam => { + let quad = Quad { + bounds: Rectangle::new(top_left, Size::new(1.0, height)), ..Default::default() - }, - ..Default::default() - }; - renderer.fill_quad(quad, Color::TRANSPARENT); - } - CursorShape::HollowBlock => { - let quad = Quad { - bounds: Rectangle::new(top_left, Size::new(width, height)), - border: Border { - width: 1.0, - color, + }; + renderer.fill_quad(quad, color); + } + CursorShape::Underline => { + let quad = Quad { + bounds: Rectangle::new( + view_position + + Vector::new( + (col as f32 * width).floor(), + ((line + 1) as f32 * height).floor(), + ), + Size::new(width, 1.0), + ), ..Default::default() - }, - ..Default::default() - }; - renderer.fill_quad(quad, Color::TRANSPARENT); + }; + renderer.fill_quad(quad, color); + } + CursorShape::Block if !state.is_focused => { + let quad = Quad { + bounds: Rectangle::new(top_left, Size::new(width, height)), + border: Border { + width: 1.0, + color, + ..Default::default() + }, + ..Default::default() + }; + renderer.fill_quad(quad, Color::TRANSPARENT); + } + CursorShape::HollowBlock => { + let quad = Quad { + bounds: Rectangle::new(top_left, Size::new(width, height)), + border: Border { + width: 1.0, + color, + ..Default::default() + }, + ..Default::default() + }; + renderer.fill_quad(quad, Color::TRANSPARENT); + } + CursorShape::Block | CursorShape::Hidden => {} // Block is handled seperately } - CursorShape::Block | CursorShape::Hidden => {} // Block is handled seperately } }