Compare commits

..

61 commits

Author SHA1 Message Date
0a614b42a7 chore: fige Cargo.lock (epoch-1.10.0, [patch] locaux)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:02:21 +02:00
d4ea340be0 merge: epoch-1.10.0 d'upstream
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:00:06 +02:00
Michael Murphy
3bfca1eb80
i18n: translation update from Hosted Weblate (#920)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Term](https://hosted.weblate.org/projects/pop-os/cosmic-term/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-term/matrix-auto.svg)
2026-10-06 21:50:59 +02:00
Michael Aaron Murphy
3bf0b8e469 Epoch 1.10.0 version update
[x] Generated by cosmic-epoch scripts/version-update.sh
2026-10-06 21:50:27 +02:00
Hosted Weblate
16a9ef5a23
i18n: translation updates from weblate
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Alex <aleha@users.noreply.hosted.weblate.org>
Co-authored-by: Andi Chandler <andi@gowling.com>
Co-authored-by: Arve Eriksson <arve.eriksson@startmail.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Martin Meizoso <martinmeizoso@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/en_GB/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/gl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/zh_Hant/
Translation: Pop OS/COSMIC Term
2026-10-06 06:51:29 +00:00
Jeremy Soller
3ef31d750d
Add moving tabs left and right with key shortcuts (#700)
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.


https://github.com/pop-os/cosmic-term/issues/441 This PR adds option to
move tabs with left and right with Ctrl+Shift+Left/Right Arrow or by
using buttons in view menu.
2026-10-01 14:15:27 -06:00
9b10e98ab1 yoda: copie a la selection vers le presse-papiers, a la Ghostty
La selection n'allait que dans la selection primaire (clic du milieu) ;
pour la coller avec Ctrl+V ailleurs, il fallait Ctrl+Shift+C. Reprise de
copy-on-select = clipboard de Ghostty, le reglage de l'ancienne config
Ghostty de Lionel.

- Config copy_on_select_clipboard (desactive par defaut, comme l'amont) ;
  case dans Reglages > Avance.
- terminal_box::on_selection_done : publie a la fin d'un geste de
  selection fait dans le panneau (glisser, double/triple clic, Shift+
  clic), pas a chaque relachement du bouton gauche. La copie primaire
  existante part elle a tout relachement dans la fenetre : la recopier
  dans le presse-papiers ecraserait, au simple clic sur un onglet, ce
  qu'on vient de copier ailleurs avec une vieille selection.
- Message::SelectionDone : copie la selection de cet onglet ; une
  selection vide (simple clic) ne donne pas de texte (range_simple).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:38:17 +02:00
a035b47e98 yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.

- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
  la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
  plus la vue en bas. Couvre touches legacy et kitty, collage, clic
  milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
  lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
  application attendrait une reponse qui ne vient pas (CSI ? u de Claude
  Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
  (selection, copie et defilement locaux, meme dans une application qui
  la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
  action ToggleReadOnly dans les raccourcis (groupe Autre, sans
  raccourci par defaut, comme Ghostty et Konsole) ; cadenas
  changes-prevent-symbolic sur l'onglet, apres les alertes.

Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
db9ee02d8d yoda: attenuation des panneaux non focalises, a la Ghostty
Avec Claude Code et Modjo cote a cote, rien ne distinguait le panneau ou
l'on tape, hors le curseur creux. L'amont assombrissait le fond de 8 %
(aussi quand la fenetre perdait le focus) et l'a retire en juillet
(a43b5ae).

Reprise de unfocused-split-opacity de Ghostty : un voile de la couleur du
fond, texte compris, sur chaque panneau non focalise, seulement quand il y
en a plusieurs ; la perte de focus de la fenetre ne change rien.

- Config unfocused_pane_dim (pourcentage, defaut 30 = opacite 0,7 de
  Ghostty, 0 desactive, plafond 85 = opacite minimale 0,15 de Ghostty).
- Reglages > Panneaux : curseur « Attenuation des panneaux inactifs ».
- terminal_box : voile trace dans un calque a part en fin de rendu (dans
  le meme calque, la primitive de texte passerait par-dessus), aux coins
  du fond du panneau.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:40:47 +02:00
52b37cc2f8 yoda: protocole clavier kitty, Shift+Entree pour Claude Code et Modjo
Claude Code (>= 2.1.269) n'active Shift+Entree que si le terminal repond
a CSI ? u. alacritty_terminal sait tenir la pile des modes et repondre,
mais seulement avec kitty_keyboard, et n'encode aucune touche : Shift+
Entree partait en \r, identique a Entree.

- term::Config : kitty_keyboard active.
- kitty_keyboard : encodeur du frontal (spec kitty, decoupe de l'encodeur
  d'Alacritty), utilise seulement quand l'application au premier plan a
  pousse le drapeau 1 ou 8 ; sinon l'encodage legacy reste seul. Gere
  desambiguisation, types d'evenements (repetition, relachement),
  touches alternatives, toutes les touches, texte associe, pave
  numerique, F1-F24, touches de modification. Shift+Entree -> CSI 13;2u.
- terminal_box : raccourcis de l'appli toujours prioritaires ;
  Shift+PageUp/PageDown/Home/End et Echap sur selection gardent leur
  comportement local ; relachements envoyes si le drapeau 2 est pose.
- Legacy : Shift+Entree envoie LF (comme Ctrl+J) au lieu de CR. rustyline
  le lit comme Ctrl+J, la touche de saut de ligne de Modjo/Goose ; pour
  un shell c'est toujours Entree.

Connu, non corrige ici : alacritty_terminal 0.25.1 push_keyboard_mode vide
title_stack au lieu de la pile des modes au-dela de 4096 empilements
(panique du fil PTY de l'onglet seulement, panic=unwind).

10 tests, dont la reponse a CSI ? u et l'application de CSI > 5 u.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:32:23 +02:00
2414aaece3 yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.

- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
  petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
  (les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
  dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
  recuperer le jeton ActivationToken au clic : la notification ramene la
  fenetre au premier plan, sur le bon panneau et le bon onglet. Une
  notification par onglet au plus (replaces_id), anti-rafale de Ghostty
  (1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
  porte une icone d'attention (cloche ou notification), eteinte quand il
  reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
  xdg-activation : cosmic-comp donne un jeton valide a toute application
  non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
  actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
  croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
  en HTML sans decoder les entites.

7 tests, dont un de bout en bout dans un vrai PTY.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
Kartik Nayak
da80dcd744 fix: update developers to System76 2026-09-30 14:10:11 -06:00
Jeremy Soller
5139e36f56 COSMIC policy update
Generated by cosmic-epoch scripts/policy-update.sh. Uses `policy_nobuild` branch to avoid package builds as it contains no code changes.
2026-09-30 09:56:29 -06:00
vacenty
487d890e7e Add moving tabs to keyboard shortcut group tabs 2026-09-27 18:18:46 +02:00
vacenty
003a9b3b06 Add moving tabs left and right with key shortcuts 2026-09-27 18:18:46 +02:00
81dc7d3eeb chore: fige Cargo.lock (epoch-1.9.0, [patch] locaux)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 21:03:54 +02:00
083e09f97e merge: epoch-1.9.0 d'upstream
# Conflicts:
#	Cargo.lock
#	src/main.rs
#	src/menu.rs
2026-09-24 20:52:15 +02:00
Hojjat
9129277bed chore: update libcosmic to fix esc handling in menus 2026-09-23 11:55:55 -06:00
Hojjat
78eaba9db1 chore: tweak context menu items order and wording 2026-09-15 19:09:43 -04:00
Michael Aaron Murphy
d25c2108c0 chore: update cosmic-files 2026-09-15 19:09:43 -04:00
Hojjat
120b4fdb12 feat: use libcosmic's context_menu widget for the terminal context menu 2026-09-15 19:09:43 -04:00
Michael Aaron Murphy
4cd8f8d605 Epoch 1.9.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-09-15 19:09:43 -04:00
Michael Murphy
1d9e5784d2
i18n: translation update from Hosted Weblate (#908)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Term](https://hosted.weblate.org/projects/pop-os/cosmic-term/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-term/matrix-auto.svg)
2026-09-15 21:57:44 +02:00
Hosted Weblate
327febcc92
i18n: translation updates from weblate
Co-authored-by: Amadɣas <massiin@proton.me>
Co-authored-by: Blauen <butterflee555@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kurt Harren Catindig <kurtharrencatindig03@gmail.com>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Satakun Utama <galaxytomostore@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/kab/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/oc/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/th/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/zh_Hant/
Translation: Pop OS/COSMIC Term
2026-09-13 01:56:16 +02:00
99842bde03 chore: fige Cargo.lock 2026-09-10 18:12:37 +02:00
5fc081e257 Merge tag 'epoch-1.8.0' into local/yoda-main
Epoch 1.8.0

# Conflicts:
#	Cargo.lock
2026-09-10 17:57:28 +02:00
b8fce5a272 yoda: elements de bloc U+2580-259F peints en quads natifs
Le glyphe de police d'un bloc ne couvre que ~1 em alors que la cellule
fait line_height (~1.4 em) : rangées de blocs trouees par l'interligne,
bords creneles, rendu flou aux echelles fractionnees.

Desormais le caractere est remplace par une espace (aucun glyphe) et le
remplissage voyage dans Metadata jusqu'au tracé, ou terminal_box le
peint en quad GPU couvrant exactement la fraction de cellule (huitiemes,
quadrants, trames alpha). Consecutive cells de meme metadata fusionnent
en un seul quad : jointif par construction, aligne sur la grille, net a
toute echelle. C'est l'approche de kitty/WezTerm. Tout TUI a blocs
profite (bannieres, jauges htop/btop, barres de progression).

Couverture totale de la plage + geometrie bornee, 2 tests.
2026-09-04 16:53:35 +02:00
Levi Portenier
4f0f0b6e4e
Epoch 1.8.0 version update (#911)
Generated by cosmic-epoch scripts/version-update.sh
2026-09-02 12:25:35 -06:00
Jeremy Soller
7945b9ffc6 Epoch 1.8.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-09-02 08:16:02 -06:00
41203ab5f1 chore: fige Cargo.lock tel que resolu apres le merge 1.7.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 07:58:51 +02:00
f3f87e59da Merge tag 'epoch-1.7.0' into local/yoda-main
Epoch 1.7.0

# Conflicts:
#	Cargo.lock
2026-08-27 07:52:54 +02:00
Jeremy Soller
909a0d31aa Epoch 1.7.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-08-25 12:12:31 -06:00
Michael Murphy
7df9265243
i18n: translation update from Hosted Weblate (#902)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Term](https://hosted.weblate.org/projects/pop-os/cosmic-term/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-term/matrix-auto.svg)
2026-08-24 20:55:33 +02:00
Hosted Weblate
dfa59f3ca4
i18n: translation updates from weblate
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jens Peter Nielsen <mail@jp-nielsen.dk>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/da/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/el/
Translation: Pop OS/COSMIC Term
2026-08-21 21:17:19 +02:00
78fb2be076 chore: fige Cargo.lock tel que resolu avec les dependances locales
Resolution obtenue avec les patchs de chemin du workspace (libcosmic-yoda,
cosmic-protocols, window_clipboard, cosmic-text). Pas de changement de code,
uniquement le verrou remis en coherence avec l'arbre local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 09:54:41 +02:00
eda2fb1d58 Merge remote-tracking branch 'upstream/master' into local/yoda-main
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/main.rs
#	src/menu.rs
#	src/terminal.rs
#	src/terminal_box.rs
2026-08-19 22:02:40 +02:00
Jeremy Soller
032a10796b
Epoch 1.6.0 version update (#901)
Generated by cosmic-epoch scripts/version-update.sh
2026-08-18 15:24:53 -06:00
Jeremy Soller
f8ccd51d4a Epoch 1.6.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-08-18 14:13:52 -06:00
Jeremy Soller
ad9a5bcc40
Redraw when needed and context menu in input fields (#896)
Selection changes, scrolling and hyperlink highlights all just flag the
terminal as dirty. Without asking for a redraw they would not show up
until something else happens to repaint the window. This becomes a
problem after iced PR below gets merged.


Depends on 
- [x] https://github.com/pop-os/iced/pull/377
- [x] https://github.com/pop-os/libcosmic/pull/1382
- [x] https://github.com/pop-os/libcosmic/pull/1388
- [x] https://github.com/pop-os/cosmic-files/pull/1815

____
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-08-18 14:12:39 -06:00
Hojjat
67b00d6392 chore: update libcosmic and cosmic-files 2026-08-18 07:26:59 -06:00
Michael Murphy
c6ab857444
i18n: translation update from Hosted Weblate (#891) 2026-08-18 14:07:28 +02:00
Hosted Weblate
ba9c1aa69c
i18n: translation updates from weblate
Co-authored-by: Alex <aleha@users.noreply.hosted.weblate.org>
Co-authored-by: Andi Chandler <andi@gowling.com>
Co-authored-by: Arve Eriksson <arve.eriksson@startmail.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jens Peter Nielsen <mail@jp-nielsen.dk>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Martin Meizoso <martinmeizoso@gmail.com>
Co-authored-by: Michael Millet <mikevlet@protonmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Steen Rabol <steen@rabol.dev>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Wilbur Lindqvist <william@willmantrade.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/da/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/en_GB/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/gl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/it/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/oc/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/sat/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/zh_Hant/
Translation: Pop OS/COSMIC Term
2026-08-18 11:09:40 +02:00
Hojjat
5095059f90 chore: cargo clippy --fix 2026-08-05 14:09:24 -06:00
Hojjat
69cdcbd1c1 fix: request a redraw if terminal needs to update 2026-08-05 14:03:29 -06:00
Connor-GH
7daf10e3b5 fix: round subpixel coordinates for glyph background rectangles
`cosmic-term` currently has an issue where borders appear to separate
some cells columnwise. This was caused due to the accumulation of
floating point error when using font sizes that can't be fully
represented in an f32. For example, a font size of 8.4x20 and a unique
color per cell is enough to reproduce this. Essentially, the glyph
background rectangles are being drawn slightly too narrow, causing the
terminal background to leak through. This patch rounds the rectangles in
such a way that we guarantee the drawing of that "border" pixel using
floor() and ceil().
2026-07-31 04:43:10 +02:00
Nitin Gurram
6bf0c65ca9
feat: allow configuring terminal pane borders 2026-07-29 18:21:03 +02:00
Jeremy Soller
543cbb0cb8
Epoch 1.5.0 version update (#890)
Generated by cosmic-epoch scripts/version-update.sh
2026-07-28 18:07:01 -06:00
Jeremy Soller
33c3869c40 Epoch 1.5.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-07-28 09:15:09 -06:00
Michael Murphy
e28eebbfb2
i18n: translation update from Hosted Weblate (#858)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Term](https://hosted.weblate.org/projects/pop-os/cosmic-term/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-term/horizontal-auto.svg)
2026-07-28 16:42:01 +02:00
Hosted Weblate
17678baf55
i18n: translation updates from weblate
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Aman Alam <aalam@users.noreply.hosted.weblate.org>
Co-authored-by: Aman Shaw <shaw4m4n@netc.fr>
Co-authored-by: Arve Eriksson <arve.eriksson@startmail.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Isaac Subirana <isaacsubiranac@gmail.com>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Kartik Nayak <65574961+kodebarista@users.noreply.github.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Saeed <s.zare.org.82@gmail.com>
Co-authored-by: Tadas Misiūnas <tadujo@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Vilius Paliokas <viliuspaliokas@gmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: lucamosca1 <luca.moscato@gmail.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/fa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/hi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/it/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/lt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/pa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/sat/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/zh_Hant/
Translation: Pop OS/COSMIC Term
2026-07-28 06:02:35 +00:00
Ben Gubler
5db18f3fac feat: allow configuring terminal pane borders 2026-07-27 15:58:51 -04:00
Jeremy Soller
117644e9c1
Merge pull request #879 from pop-os/epoch-update
Epoch 1.4.0 version update
2026-07-20 16:36:09 -06:00
Jeremy Soller
01af42aff5 Epoch 1.4.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-07-20 14:37:02 -06:00
RipleyTom
44d042f45d fix: only PaneClicked events change focus 2026-07-14 13:32:02 -06:00
Jeremy Soller
d05ed099b6 Epoch 1.3.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-07-13 09:57:38 -06:00
Ashley Wulber
46f8b1a9cf chore: update libcosmic 2026-07-09 18:16:12 -04:00
Jeremy Soller
1019e04b82
Merge pull request #865 from pop-os/terminal-focus-background
Remove background shading when not focused
2026-07-09 14:54:25 -06:00
Jeremy Soller
a43b5ae500 Remove background shading when not focused 2026-07-09 14:32:41 -06:00
Jeremy Soller
10ca3f1025
Merge pull request #862 from pop-os/theme-v2
feat: blur
2026-07-07 17:46:43 -06:00
Ashley Wulber
e3b1cda8ab chore: update libcosmic 2026-07-07 11:47:35 -04:00
Ashley Wulber
6bd07a6363 feat: blur 2026-07-06 09:28:54 -04:00
49 changed files with 3943 additions and 1463 deletions

View file

@ -1,8 +1,10 @@
- [ ] I have disclosed use of any AI generated code in my commit messages.
- If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment.
- [ ] I understand these changes in full and will be able to respond to review comments.
- [ ] My change is accurately described in the commit message.
- [ ] My contribution is tested and working as described.
- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.
Replace this text with a description of your changes, and why these changes are important. Read and acknowledge the list below. PRs without a completed checkbox will be closed.
---
- I have not included any LLM (also known as AI) generated content in this PR, including code, comments, and descriptions.
- I understand these changes in full and will be able to respond to review comments.
- My change is accurately described in the commit message.
- My contribution is tested and working as described.
- I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.
- [ ] I have read and followed all of the above requirements. For more information, see our [contributor guide](https://github.com/pop-os/pop/blob/master/CONTRIBUTING.md).

14
.github/workflows/require-checklist.yml vendored Normal file
View file

@ -0,0 +1,14 @@
# From https://github.com/mheap/require-checklist-action, license MIT
name: Require Checklist
on:
pull_request:
types: [opened, edited, synchronize]
jobs:
job1:
runs-on: ubuntu-latest
steps:
- uses: mheap/require-checklist-action@v2
with:
requireChecklist: true

1856
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
[package]
name = "cosmic-term"
version = "1.3.0"
version = "1.10.0"
authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2024"
license = "GPL-3.0-only"
rust-version = "1.90"
rust-version = "1.93"
[dependencies]
alacritty_terminal = "0.25.1"
@ -18,6 +18,9 @@ ron = "0.11"
serde = { version = "1", features = ["serde_derive"] }
shlex = "1"
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
# Notifications de bureau (OSC 9 / 777) et enveloppe du PTY (yoda)
polling = "3.11"
zbus = "5.19"
# HTTP client for Modjo AI integration
reqwest = { version = "0.12", features = ["json", "stream"], optional = true }
serde_json = { version = "1", optional = true }
@ -51,7 +54,14 @@ package = "libcosmic-yoda"
path = "../libcosmic"
default-features = false
#TODO: a11y feature crashes file chooser dialog
features = ["about", "autosize", "multi-window", "tokio", "winit", "surface-message"]
features = [
"about",
"autosize",
"multi-window",
"tokio",
"winit",
"surface-message",
]
[target.'cfg(unix)'.dependencies]
fork = "0.4"

70
cargo.just Normal file
View file

@ -0,0 +1,70 @@
cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
# Compile with debug profile
[no-cd]
build-debug *args:
cargo build {{args}}
# Compile with release profile
[no-cd]
build-release *args: (build-debug '--release' args)
# Compile with a vendored tarball
[no-cd]
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
# Check for errors and linter warnings
[no-cd]
check *args:
cargo clippy --all-features {{args}} -- -W clippy::pedantic
# Runs a check with JSON message format for IDE integration
[no-cd]
check-json: (check '--message-format=json')
# Remove Cargo build artifacts
[no-cd]
clean:
cargo clean
# Also remove .cargo and vendored dependencies
[no-cd]
clean-dist: clean
rm -rf .cargo vendor vendor.tar target
# Run the application for testing purposes
[no-cd]
run *args:
cargo run {{args}}
# Run `cargo test`
[no-cd]
test *args:
cargo test {{args}}
# Vendor Cargo dependencies locally
[no-cd]
vendor:
#!/usr/bin/env bash
mkdir -p .cargo
cargo vendor --locked | head -n -1 > .cargo/config.toml
echo 'directory = "vendor"' >> .cargo/config.toml
echo >> .cargo/config.toml
echo '[env]' >> .cargo/config.toml
if [ -n "${SOURCE_DATE_EPOCH}" ]
then
source_date="$(date -d "@${SOURCE_DATE_EPOCH}" "+%Y-%m-%d")"
echo "VERGEN_GIT_COMMIT_DATE = \"${source_date}\"" >> .cargo/config.toml
fi
if [ -n "${SOURCE_GIT_HASH}" ]
then
echo "VERGEN_GIT_SHA = \"${SOURCE_GIT_HASH}\"" >> .cargo/config.toml
fi
tar pcf vendor.tar .cargo vendor
rm -rf .cargo vendor
# Extracts vendored dependencies
[no-cd]
vendor-extract:
rm -rf vendor
tar pxf vendor.tar

48
debian/changelog vendored
View file

@ -1,3 +1,51 @@
cosmic-term (1.10.0) noble; urgency=medium
* Epoch 1.10.0 version update
-- Michael Murphy <michael@mmurphy.dev> Mon, 05 Oct 2026 21:28:17 +0200
cosmic-term (1.9.0) noble; urgency=medium
* Epoch 1.9.0 version update
-- Michael Murphy <michael@mmurphy.dev> Mon, 14 Sep 2026 19:50:47 +0200
cosmic-term (1.8.0) noble; urgency=medium
* Epoch 1.8.0 version update
-- Jeremy Soller <jeremy@system76.com> Wed, 02 Sep 2026 08:15:55 -0600
cosmic-term (1.7.0) noble; urgency=medium
* Epoch 1.7.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 25 Aug 2026 09:21:20 -0600
cosmic-term (1.6.0) noble; urgency=medium
* Epoch 1.6.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 18 Aug 2026 07:15:01 -0600
cosmic-term (1.5.0) noble; urgency=medium
* Epoch 1.5.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 28 Jul 2026 09:15:04 -0600
cosmic-term (1.4.0) noble; urgency=medium
* Epoch 1.4.0 version update
-- Jeremy Soller <jeremy@system76.com> Mon, 20 Jul 2026 14:36:58 -0600
cosmic-term (1.3.0) noble; urgency=medium
* Epoch 1.3.0 version update
-- Jeremy Soller <jeremy@system76.com> Sun, 12 Jul 2026 13:22:28 -0600
cosmic-term (1.2.0) noble; urgency=medium
* Epoch 1.2.0 version update

View file

@ -1,4 +1,6 @@
tar-ignore=.github
tar-ignore=.vscode
tar-ignore=target/.rustc_info.json
tar-ignore=target/CACHEDIR.TAG
tar-ignore=target/debug
tar-ignore=target/release
tar-ignore=vendor
tar-ignore=target

View file

@ -41,7 +41,7 @@ settings = الإعدادات
### Appearance
appearance = المظهر
theme = النسق
theme = السمة
match-desktop = طابق سطح المكتب
dark = داكن
light = فاتح
@ -88,7 +88,7 @@ new-window = نافذة جديدة
profile = ملف التعريف
menu-profiles = ملفات التعريف...
close-tab = أغلق اللسان
quit = أنهِ
quit = إنهِ
## Edit

View file

@ -1,3 +1,6 @@
cancel = Cancel·lar
settings = Configuració
quit = Surt
delete = Suprimeix
rename = Canvia el nom
name = Nom

View file

@ -38,15 +38,15 @@ advanced-font-settings = Pokročilá nastavení písma
default-font = Písmo
default-font-size = Velikost písma
default-font-stretch = Šířka písma
default-font-weight = Normální tloušťka písma
default-dim-font-weight = Tenká tloušťka písma
default-bold-font-weight = Tučná tloušťka písma
default-font-weight = Tloušťka normálního písma
default-dim-font-weight = Tloušťka zeslabeného písma
default-bold-font-weight = Tloušťka tučného písma
use-bright-bold = Světlejší tučný text
splits = Rozdělení
focus-follow-mouse = Zaměření psaní sleduje myš
advanced = Pokročilé
show-headerbar = Zobrazit hlavičku
show-header-description = Hlavičku je možné odkrýt z kontextového menu
show-headerbar = Zobrazit záhlaví
show-header-description = Záhlaví lze odkrýt v nabídce zobrazené po kliknutí pravým tlačítkem myši
find-placeholder = Najít...
find-previous = Najít předchozí
find-next = Najít další
@ -69,7 +69,7 @@ next-tab = Další karta
previous-tab = Předchozí karta
split-horizontal = Rozdělit horizontálně
split-vertical = Rozdělit vertikálně
pane-toggle-maximize = Přepnout maximalizaci
pane-toggle-maximize = Maximalizovat nebo obnovit podokno
menu-color-schemes = Barevná schémata...
menu-settings = Nastavení...
menu-about = O aplikaci Terminál COSMIC...
@ -99,13 +99,17 @@ 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
focus-pane-down = Zaměřit podokno dole
focus-pane-left = Zaměřit podokno vlevo
focus-pane-right = Zaměřit podokno vpravo
focus-pane-up = Zaměřit podokno 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í
copy-link = Kopírovat odkaz
tab-new-inherit-working-directory = Nové karty a okna používají aktuální adresář
tab-new-inherit-working-directory-description = Otevírat nové karty a okna v pracovním adresáři aktivní karty
show-pane-borders = Zobrazit ohraničení podoken
window-header = Záhlaví okna
move-tab-left = Přesunout kartu doleva
move-tab-right = Přesunout kartu doprava

View file

@ -0,0 +1,31 @@
rename = Omdøb
delete = Slet
name = Navn
cancel = Afbryd
quit = Afslut
find-next = Find næste
select-all = Vælg alle
new-window = Nyt vindue
edit = Redigér
copy = Kopiér
theme = Tema
appearance = Udseende
light = Lyst
repository = Pakkearkiv
paste = Indsæt
menu-settings = Indstillinger...
view = Vis
dark = Mørkt
match-desktop = Følg skrivebordets indstillinger
find-previous = Find forrige
find-placeholder = Find...
default-zoom-step = Zoom-trin
file = Fil
find = Find
support = Support
settings = Indstillinger
keyboard-shortcuts = Tastaturgenveje
menu-keyboard-shortcuts = Tastaturgenveje…
replace = Erstat
close-tab = Luk fane
new-tab = Ny fane

View file

@ -105,11 +105,11 @@ next-tab = Nächster Tab
previous-tab = Vorheriger Tab
split-horizontal = Horizontal teilen
split-vertical = Vertikal teilen
pane-toggle-maximize = Maximierung umschalten
pane-toggle-maximize = Bereich maximieren oder wiederherstellen
menu-color-schemes = Farbschemen ...
menu-settings = Einstellungen ...
menu-about = Über COSMIC Terminal ...
repository = Repository
repository = Repositorium
cancel = Abbrechen
keyboard-shortcuts = Tastenkombinationen
open-link = Link öffnen
@ -149,3 +149,7 @@ shortcut-replace-body = { $binding } ist bereits { $existing } zugeordnet. Durch
keywords = Befehl;Shell;Terminal;CLI;
tab-new-inherit-working-directory = Neue Tabs und Fenster verwenden das aktuelle Verzeichnis
tab-new-inherit-working-directory-description = Neue Tabs und Fenster im Arbeitsverzeichnis des aktiven Tabs öffnen
show-pane-borders = Bereichsrahmen anzeigen
window-header = Fenstertitel
move-tab-left = Tab nach links verschieben
move-tab-right = Tab nach rechts verschieben

View file

@ -87,7 +87,7 @@ new-window = Νέο παράθυρο
profile = Προφίλ
menu-profiles = Προφίλ...
close-tab = Κλείσιμο καρτέλας
quit = Έξοδος
quit = Τερματισμός
## Edit
@ -107,7 +107,7 @@ next-tab = Επόμενη καρτέλα
previous-tab = Προηγούμενη καρτέλα
split-horizontal = Οριζόντιος διαχωρισμός
split-vertical = Κάθετος διαχωρισμός
pane-toggle-maximize = Εναλλαγή μεγιστοποίησης
pane-toggle-maximize = Μεγιστοποίηση ή επαναφορά προβολής
menu-color-schemes = Χρωματικοί συνδυασμοί...
menu-settings = Ρυθμίσεις...
menu-about = Σχετικά με το Τερματικό COSMIC...
@ -152,3 +152,7 @@ shortcut-group-zoom = Ζουμ
shortcut-replace-body = Ο συνδυασμός { $binding } έχει ήδη αντιστοιχιστεί στην ενέργεια «{ $existing }». Θέλετε να αντικατασταθεί με την ενέργεια «{ $new_action }»;
tab-new-inherit-working-directory = Οι νέες καρτέλες και παράθυρα χρησιμοποιούν τον τρέχοντα κατάλογο
tab-new-inherit-working-directory-description = Άνοιγμα νέων καρτελών και παραθύρων στον τρέχοντα κατάλογο της ενεργής καρτέλας
show-pane-borders = Εμφάνιση περιγραμμάτων πλαισίων
window-header = Κεφαλίδα παραθύρου
move-tab-left = Μετακίνηση καρτέλας στα αριστερά
move-tab-right = Μετακίνηση καρτέλας στα δεξιά

View file

@ -105,9 +105,14 @@ next-tab = Next tab
previous-tab = Previous tab
split-horizontal = Split horizontal
split-vertical = Split vertical
pane-toggle-maximize = Toggle maximized
pane-toggle-maximize = Maximise or restore pane
menu-color-schemes = Colour schemes...
menu-settings = Settings...
menu-about = About COSMIC Terminal...
repository = Repository
support = Support
cancel = Cancel
keyboard-shortcuts = Keyboard shortcuts
replace = Replace
menu-keyboard-shortcuts = Keyboard shortcuts...
window-header = Window header

View file

@ -58,13 +58,18 @@ use-bright-bold = Make bold text brighter
### Splits
splits = Splits
focus-follow-mouse = Typing focus follows mouse
show-pane-borders = Show pane borders
unfocused-pane-dim = Dim unfocused panes
### Advanced
advanced = Advanced
show-headerbar = Show header
window-header = Window header
show-header-description = Reveal the header from the right-click menu
tab-new-inherit-working-directory = New tabs and windows use current directory
tab-new-inherit-working-directory-description = Open new tabs and windows in the active tab's working directory
copy-on-select-clipboard = Copy selection to clipboard
copy-on-select-clipboard-description = In addition to the primary selection (middle click), like copy-on-select = clipboard in Ghostty
### Keyboard shortcuts
add-another-keybinding = Add another keybinding
@ -137,6 +142,10 @@ save-output = Save output as text...
monitor-activity = Monitor for activity
monitor-silence = Monitor for silence
monitor-process-finished = Monitor for process finishing
read-only = Read-only
## Notifications
notification-show = Show tab
## Open
open-link = Open Link
@ -149,9 +158,11 @@ zoom-reset = Default text size
zoom-out = Smaller text
next-tab = Next tab
previous-tab = Previous tab
move-tab-left = Move tab left
move-tab-right = Move tab right
split-horizontal = Split horizontal
split-vertical = Split vertical
pane-toggle-maximize = Toggle maximized
pane-toggle-maximize = Maximize or restore pane
menu-color-schemes = Color schemes...
menu-settings = Settings...
menu-about = About COSMIC Terminal...

View file

@ -0,0 +1,5 @@
name = نام
cancel = لغو
quit = خروج
rename = تغییر نام
delete = حذف

View file

@ -38,7 +38,7 @@ settings = Asetukset
appearance = Ulkoasu
theme = Teema
match-desktop = Sovita työpöytään
match-desktop = Sama kuin työpöytä
dark = Tumma
light = Vaalea
syntax-dark = Tumma väriteema
@ -107,7 +107,7 @@ next-tab = Seuraava välilehti
previous-tab = Edellinen välilehti
split-horizontal = Halkaise näkymä vaakasuunnassa
split-vertical = Halkaise näkymä pystysuunnassa
pane-toggle-maximize = Suurennus päällä tai pois
pane-toggle-maximize = Suurenna tai palauta paneeli
menu-color-schemes = Väriteemat…
menu-settings = Asetukset…
menu-about = Tietoa COSMICin päätteestä…
@ -151,3 +151,7 @@ toggle-fullscreen = Koko näyttö päällä tai pois
shortcut-group-other = Muut
tab-new-inherit-working-directory = Uudet välilehdet käyttävät nykyistä hakemistoa
tab-new-inherit-working-directory-description = Avaa uudet välilehdet aktiivisen välilehden työskentelyhakemistossa
show-pane-borders = Näytä paneelin rajat
window-header = Ikkunan otsake
move-tab-left = Siirrä välilehti vasemmalle
move-tab-right = Siirrä välilehti oikealle

0
i18n/fil/cosmic_term.ftl Normal file
View file

View file

@ -96,7 +96,7 @@ quit = Quitter
## Edit
edit = Modifier
edit = Édition
copy = Copier
paste = Coller
select-all = Sélectionner tout
@ -108,6 +108,10 @@ save-output = Enregistrer la sortie comme texte...
monitor-activity = Surveiller l'activité
monitor-silence = Surveiller le silence
monitor-process-finished = Surveiller la fin du processus
read-only = Lecture seule
## Notifications
notification-show = Afficher l'onglet
## View
@ -119,7 +123,7 @@ next-tab = Onglet suivant
previous-tab = Onglet précédent
split-horizontal = Scinder en horizontal
split-vertical = Scinder en vertical
pane-toggle-maximize = Vue unique
pane-toggle-maximize = Agrandir ou restaurer le panneau
menu-color-schemes = Palettes de couleurs...
menu-settings = Paramètres...
menu-about = À propos de terminal COSMIC...
@ -168,3 +172,10 @@ comment = Émulateur de terminal pour le bureau COSMIC
keywords = Commande;Shell;Terminal;CLI;
tab-new-inherit-working-directory = Les nouveaux onglets et fenêtres utilisent le répertoire actuel
tab-new-inherit-working-directory-description = Ouvrir les nouveaux onglets et fenêtres dans le répertoire de travail de l'onglet actif
copy-on-select-clipboard = Copier la sélection dans le presse-papiers
copy-on-select-clipboard-description = En plus de la sélection primaire (clic du milieu), comme copy-on-select = clipboard dans Ghostty
show-pane-borders = Afficher les bordures des panneaux
unfocused-pane-dim = Atténuation des panneaux inactifs
window-header = Barre de titre de la fenêtre
move-tab-left = Déplacer l'onglet à gauche
move-tab-right = Déplacer l'onglet à droite

View file

@ -105,7 +105,7 @@ next-tab = An cluaisín ar aghaidh
previous-tab = An cluaisín roimhe seo
split-horizontal = Scoilt cothrománach
split-vertical = Scoilt ingearach
pane-toggle-maximize = Scoránaigh uasmhéadú
pane-toggle-maximize = Uasmhéadaigh nó athchóirigh an phána
menu-color-schemes = Scéimeanna dathanna...
menu-settings = Socruithe...
menu-about = Maidir le COSMIC Teirminéal...
@ -147,5 +147,7 @@ type-to-search = Clóscríobh le cuardach a dhéanamh...
comment = Aithriseoir teirminéal don deasc COSMIC
keywords = Ordú;Blaosc;Teirminéal;CLI;
copy-link = Cóipeáil Nasc
tab-new-inherit-working-directory = Úsáideann cluaisíní nua an eolaire reatha
tab-new-inherit-working-directory-description = Oscail cluaisíní nua in eolaire oibre an chluaisín ghníomhaigh
tab-new-inherit-working-directory = Úsáideann cluaisíní agus fuinneoga nua an t-eolaire reatha
tab-new-inherit-working-directory-description = Oscail cluaisíní agus fuinneoga nua in eolaire oibre an chluaisín ghníomhaigh
show-pane-borders = Taispeáin teorainneacha na pána
window-header = Ceanntásc na fuinneoige

115
i18n/gl/cosmic_term.ftl Normal file
View file

@ -0,0 +1,115 @@
name = Nome
cancel = Cancelar
delete = Eliminar
rename = Renomear
repository = Repositorio
support = Soporte
settings = Axustes
appearance = Apariencia
theme = Tema
match-desktop = Emparexar co escritorio
dark = Escuro
light = Claro
default-zoom-step = Pasos do zoom
keyboard-shortcuts = Atallos de teclado
find-placeholder = Atopar…
find-previous = Atopar antes
find-next = Atopar seguinte
replace = Remplazar
file = Ficheiro
new-window = Nova xanela
quit = Pechar
edit = Editar
copy = Copiar
paste = Pegar
select-all = Seleccionar todo
find = Atopar
view = Ver
menu-settings = Axustes...
menu-keyboard-shortcuts = Atallos de teclado…
new-tab = Nova lapela
close-tab = Pechar lapela
export = Exportar
import = Importar
reset-to-default = Restablecer aos valores predeterminados
type-to-search = Escribe para buscar...
add-another-keybinding = Engadir outro atallo de teclado
cosmic-terminal = Terminal de COSMIC
comment = Emulador de terminal para o escritorio COSMIC
keywords = Orde; Shell; Terminal; CLI;
new-terminal = Novo terminal
color-schemes = Esquemas de cores
import-errors = Importar erros
profiles = Perfís
command-line = Liña de comandos
tab-title = Titulo da lapela
tab-title-description = Substituír o título predeterminado da lapela
add-profile = Engadir perfil
new-profile = Novo perfil
make-default = Facer predeterminado
working-directory = Directorio de traballo
hold = Manter
remain-open = Permanecer aberto despois de que remate o proceso fillo.
syntax-dark = Esquema de cores escuro
syntax-light = Esquema de cores claras
opacity = Opacidade do fondo
font = Tipografía
advanced-font-settings = Configuración avanzada da tipografía
default-font = Tipografía
default-font-size = Tamaño da tipografía
default-font-stretch = Ancho da tipografía
default-font-weight = Grosor da tipografía normal
default-dim-font-weight = Grosor da tipografía fina
default-bold-font-weight = Grosor da tipografía en negriña
use-bright-bold = Facer máis brillante o texto en negriña
splits = Divisións
focus-follow-mouse = O foco de escritura segue o rato
show-pane-borders = Mostrar os bordos dos paneis
advanced = Avanzado
show-headerbar = Mostrar a cabeceira
show-header-description = Mostrar a cabeceira desde o menú contextual
tab-new-inherit-working-directory = As novas lapelas e xanelas usan o directorio actual
tab-new-inherit-working-directory-description = Abrir novas lapelas e xanelas no directorio de traballo da pestana activa
close-window = Pechar xanela
copy-or-sigint = Copiar ou SIGINT
disable = Desactivar
focus-pane-down = Poñer o foco no panel inferior
focus-pane-left = Poñer o foco no panel esquerdo
focus-pane-right = Poñer o foco no panel dereito
focus-pane-up = Poñer o foco no panel superior
no-shortcuts = Sen atallos de teclado
password-manager = Xestor de contrasinais
paste-primary = Pegar o contido primario
shortcut-capture-hint = Preme a combinación de teclas
shortcut-group-clipboard = Portapapeis
shortcut-group-other = Outro
shortcut-group-tabs = Lapelas
shortcut-group-window = Xanela
shortcut-group-zoom = Zoom
shortcut-replace-body = { $binding } xa está asignado a { $existing }. Substituílo por { $new_action }?
shortcut-replace-title = Remplazar atallo de teclado ?
tab-activate = Activar a lapela { $number }
toggle-fullscreen = Cambiar a pantalla completa
profile = Perfil
menu-profiles = Perfís...
clear-scrollback = Limpar o historial de desprazamento
open-link = Abrir enderezo
copy-link = Copiar enderezo
zoom-in = Texto máis grande
zoom-reset = Tamaño de texto predeterminado
zoom-out = Texto máis pequeno
next-tab = Lapela seguinte
previous-tab = Lapela previa
split-horizontal = Dividir horizontalmente
split-vertical = Dividir verticalmente
pane-toggle-maximize = Alternar maximizado
menu-color-schemes = Esquema de cores...
menu-about = Sobre o Terminal de COSMIC...
menu-password-manager = Contrasinais...
passwords-title = Contrasinais
add-password = Engadir contrasinal
password-input = Contrasinal
password-input-description = Descripción
window-header = Cabeceira da xanela
move-tab-left = Mover a pestaña á esquerda
move-tab-right = Mover a pestaña á dereita

View file

@ -29,18 +29,18 @@ remain-open = चाइल्ड प्रोसेस के निकलने
## Settings
settings = सेटिंग
settings = सेटिंग्स
### Appearance
appearance = दिखावट
theme = थीम
match-desktop = डेस्कटॉप से मिलाएं
appearance = रुप-रंग
theme = प्रसंग
match-desktop = डेस्कटॉप से मेल खाएँ
dark = डार्क
light = लाइट
syntax-dark = रंग योजना डार्क
syntax-light = रंग योजना लाइट
default-zoom-step = ज़ूम स्तर
default-zoom-step = ज़ूम चरण
opacity = पृष्ठभूमि अपारदर्शिता
### Font
@ -66,7 +66,7 @@ advanced = एडवांस
show-headerbar = हैडर दिखाएं
show-header-description = हैडर को राइट-क्लिक मेन्यू से दिखाएं।
# Find
find-placeholder = खोजें...
find-placeholder = खोजें..।
find-previous = पिछला खोजें
find-next = अगला खोजें
@ -81,13 +81,13 @@ new-window = नई विंडो
profile = प्रोफाइल
menu-profiles = प्रोफाइल्स...
close-tab = टैब बंद करें
quit = बंद करें
quit = बाहर जाएँ
### Edit
edit = संपादित करें
edit = संपादन
copy = कॉपी करें
paste = पेस्ट करें
paste = चिपकाएँ
select-all = सभी चुनें
find = खोजें
clear-scrollback = स्क्रॉलबैक साफ़ करें
@ -104,7 +104,11 @@ split-horizontal = क्षैतिज विभाजन
split-vertical = लंबवत विभाजन
pane-toggle-maximize = अधिकतम करें
menu-color-schemes = रंग योजनाएँ...
menu-settings = सेटिंग्स...
menu-settings = सेटिंग्स..।
menu-about = कॉस्मिक टर्मिनल के बारे में...
repository = रिपॉजिटरी
support = सहायता
support = समर्थन
cancel = रद्द करें
keyboard-shortcuts = कीबोर्ड शॉर्टकट्स
replace = बदलें
menu-keyboard-shortcuts = कीबोर्ड शॉर्टकट्स..।

View file

@ -52,14 +52,14 @@ opacity = Háttér átlátszósága
### Font
font = Betűtípus
advanced-font-settings = Speciális betűtípus-beállítások
default-font = Betűtípus
font = Betűkészlet
advanced-font-settings = Speciális betűkészlet-beállítások
default-font = Betűkészlet
default-font-size = Betűméret
default-font-stretch = Betűszélesség
default-font-weight = Normál betűsúly
default-dim-font-weight = Vékony betűsúly
default-bold-font-weight = Félkövér betűsúly
default-font-weight = Normál betűvastagság
default-dim-font-weight = Vékony betűvastagság
default-bold-font-weight = Félkövér betűvastagság
use-bright-bold = Félkövér szöveg világosítása
### Splits
@ -109,7 +109,7 @@ next-tab = Következő lap
previous-tab = Előző lap
split-horizontal = Vízszintes felosztás
split-vertical = Függőleges felosztás
pane-toggle-maximize = Panel maximalizálása
pane-toggle-maximize = Panel maximalizálása vagy visszaállítása
menu-color-schemes = Színsémák…
menu-settings = Beállítások…
menu-about = A COSMIC Terminál névjegye…
@ -149,3 +149,7 @@ type-to-search = Gépelj a kereséshez…
copy-link = Hivatkozás másolása
tab-new-inherit-working-directory = Az új lapok és ablakok az aktuális könyvtárat használják
tab-new-inherit-working-directory-description = Az új lapok és ablakok az aktív lap munkakönyvtárában nyílnak meg
show-pane-borders = Ablaktábla-szegélyek megjelenítése
window-header = Ablakfejléc
move-tab-left = Lap mozgatása balra
move-tab-right = Lap mozgatása jobbra

View file

@ -4,7 +4,7 @@ repository = Repositori
support = Dukungan
color-schemes = Skema warna
cosmic-terminal = Terminal COSMIC
rename = Ganti nama
rename = Ganti Nama
export = Ekspor
delete = Hapus
import = Impor
@ -23,7 +23,7 @@ remain-open = Tetap buka setelah proses anak berakhir.
settings = Pengaturan
appearance = Tampilan
theme = Tema
match-desktop = Cocokkan desktop
match-desktop = Sesuaikan desktop
dark = Gelap
light = Terang
syntax-dark = Skema warna gelap
@ -68,7 +68,7 @@ next-tab = Tab selanjutnya
previous-tab = Tab sebelumnya
split-horizontal = Pemisahan horisontal
split-vertical = Pemisahan vertikal
pane-toggle-maximize = Ubah ke maksimal
pane-toggle-maximize = Maksimalkan atau pulihkan panel
menu-color-schemes = Skema warna...
menu-settings = Pengaturan...
menu-about = Tentang Terminal COSMIC...
@ -109,3 +109,7 @@ keywords = Perintah;Shell;Terminal;CLI;
copy-link = Salin Tautan
tab-new-inherit-working-directory = Tab dan jendela baru menggunakan direktori saat ini
tab-new-inherit-working-directory-description = Buka tab dan jendela baru di direktori pekerjaan tab aktif
show-pane-borders = Tampilkan batas panel
window-header = Header jendela
move-tab-left = Pindahkan tab ke kiri
move-tab-right = Pindahkan tab ke kanan

View file

@ -40,7 +40,7 @@ settings = Impostazioni
appearance = Aspetto
theme = Tema
match-desktop = Sistema
match-desktop = Abbina al desktop
dark = Scuro
light = Chiaro
syntax-dark = Schema colori scuro
@ -115,3 +115,7 @@ menu-settings = Impostazioni...
menu-about = Informazioni su COSMIC Terminal...
support = Supporto
repository = Repository
cancel = Annulla
keyboard-shortcuts = Scorciatoie da tastiera
replace = Sostituisci
menu-keyboard-shortcuts = Scorciatoie da tastiera...

View file

@ -1,9 +1,9 @@
cancel = Sefsex
cancel = Semmet
name = Isem
delete = kkes
rename = Snifel isem
quit = Tuffɣa
support = Tallalt
quit = Ffeɣ
support = Tallelt
repository = Asarsay
settings = Iɣewwaṛen
appearance = Timeẓri
@ -14,7 +14,7 @@ paste = Senteḍ
select-all = Fren akk
view = Wali
menu-settings = Iɣewwaṛen…
match-desktop = Amṣada d tnarit
match-desktop = Semṣada akked tnarit
file = Afaylu
new-window = Asfaylu amaynut
edit = Ẓreg

View file

@ -3,11 +3,11 @@ delete = Өшіру
repository = Репозиторий
support = Қолдау
settings = Баптаулар
appearance = Сыртқы түрі
theme = Тақырып
match-desktop = Жұмыс үстеліне сәйкес келу
appearance = Сыртқы көрініс
theme = Тема
match-desktop = Жұмыс үстеліне сәйкес
dark = Қараңғы
light = Жарық
light = Ашық
file = Файл
new-tab = Жаңа бет
new-window = Жаңа терезе
@ -29,7 +29,7 @@ find = Табу
cosmic-terminal = COSMIC терминалы
new-terminal = Жаңа терминал
color-schemes = Түстер схемалары
rename = Атын өзгерту
rename = Атауын өзгерту
import-errors = Импорттау қателері
profiles = Профильдер
command-line = Командалық жол
@ -69,7 +69,7 @@ next-tab = Келесі бет
previous-tab = Алдыңғы бет
split-horizontal = Горизонталды бөлу
split-vertical = Вертикалды бөлу
pane-toggle-maximize = Толық экран режимін ауыстыру
pane-toggle-maximize = Панельді үлкейту немесе қалпына келтіру
menu-color-schemes = Түс схемалары...
menu-about = COSMIC терминалы туралы...
menu-password-manager = Парольдер...
@ -107,5 +107,9 @@ type-to-search = Іздеу үшін теріңіз...
comment = COSMIC жұмыс үстелі үшін терминал эмуляторы
keywords = Command;Shell;Terminal;CLI;Команда;Қоршам;Терминал;
copy-link = Сілтемені көшіру
tab-new-inherit-working-directory = Жаңа беттерде ағымдағы буманы қолдану
tab-new-inherit-working-directory-description = Жаңа беттерді белсенді беттің жұмыс бумасында ашу
tab-new-inherit-working-directory = Жаңа беттерде және терезелерде ағымдағы буманы қолдану
tab-new-inherit-working-directory-description = Жаңа беттерді және терезелерді белсенді беттің жұмыс бумасында ашу
show-pane-borders = Панель жақтауларын көрсету
window-header = Терезе тақырыптамасы
move-tab-left = Бетті солға жылжыту
move-tab-right = Бетті оңға жылжыту

View file

@ -33,7 +33,7 @@ command-line = Rêza fermanê
view = Dîtin
dark = Tarî
hold = Ragire
match-desktop = Li gorî sermaseyê
match-desktop = Bi sermaseyê re guncav bike
find-previous = Yê paş bibîne
cosmic-terminal = Termînalê COSMIC
find-placeholder = Bibîne...
@ -62,7 +62,7 @@ shortcut-group-zoom = Nêzîkbûn
repository = Embar
find = Bibîne
replace = Biguhêrîne
syntax-light = Şêwaza rengê ronî
syntax-light = Şêwaza rengan a ronî
default-font-weight = Giraniya cûrenivîsê ya normal
zoom-in = Nivîsa mezintir
previous-tab = Rûgera paş
@ -109,3 +109,7 @@ tab-activate = Rûgera { $number } çalak bike
toggle-fullscreen = Dîmendera tijî biguhêrîne
type-to-search = Ji bo lêgerînê binivîse...
copy-link = Girêdanê jê bigire
show-pane-borders = Sînorên kilîtdankê nîşan bide
window-header = Sernavê çarçoveyê
move-tab-left = Rûgerê bilivîne çepê
move-tab-right = Rûgerê bilivîne rastê

View file

@ -1,6 +1,6 @@
name = Pavadinimas
delete = Ištrinti
cosmic-terminal = COSMIC Terminal
cosmic-terminal = COSMIC Terminalas
new-terminal = Naujas terminalas
repository = Saugykla
support = Palaikymas
@ -19,7 +19,7 @@ make-default = Padaryti numatytu
working-directory = Darbinis katalogas
hold = Laikyti
remain-open = Palikti atidarytą išsijungus vaikiniam procesui.
settings = Nuostatos
settings = Nuostatai
appearance = Išvaizda
theme = Stilius
match-desktop = Pagal darbalaukio temą
@ -47,7 +47,7 @@ new-window = Naujas langas
profile = Profilis
menu-profiles = Profiliai...
close-tab = Uždaryti skirtuką
quit = Išeiti
quit = Išjungti
edit = Redaguoti
copy = Kopijuoti
paste = Įdėti
@ -65,7 +65,7 @@ split-horizontal = Skaidyti horizontaliai
split-vertical = Skaidyti vertikaliai
pane-toggle-maximize = Perjungti maksimalų padidinimą
menu-color-schemes = Spalvų schemos...
menu-settings = Nuostatos...
menu-settings = Nuostatai...
menu-about = Apie COSMIC Terminal...
menu-password-manager = Slaptažodžiai...
passwords-title = Slaptažodžiai

View file

@ -1 +1,14 @@
settings = Paramètres
settings = Configuracion
appearance = Aparéncia
theme = Tèma
match-desktop = Assortir al burèu
dark = Escur
light = Clar
file = Fichièr
name = Nom
repository = Depaus
cancel = Anullar
menu-settings = Configuracion...
quit = Quitar
delete = Suprimir
replace = Remplaçar

View file

@ -8,11 +8,11 @@ name = ਨਾਂ
settings = ਸੈਟਿੰਗਾਂ
appearance = ਦਿੱਖ
theme = ਥੀਮ
match-desktop = ਡੈਸਕਟਾਪ ਨਾਲ ਮਿਲਦਾ
match-desktop = ਡੈਸਕਟਾਪ ਨਾਲ ਮਿਲਾਓ
dark = ਗੂੜ੍ਹਾ
light = ਹਲਕਾ
light = ਫਿੱਕਾ
default-zoom-step = ਜ਼ੂਮ ਸਟੈਪ
quit = ਬਾਹਰ
quit = ਸਮਾਪਤ
edit = ਸੋਧੋ
copy = ਕਾਪੀ ਕਰੋ
paste = ਚੇਪੋ

View file

@ -1,5 +1,5 @@
cosmic-terminal = Terminal COSMIC
comment = Emulator terminala dla pulpitu COSMIC
comment = Emulator terminala dla środowiska COSMIC
keywords = Cli;WierszPoleceń;Powłoka;Terminal;
new-terminal = Nowy terminal
@ -73,7 +73,7 @@ advanced = Zaawansowane
show-headerbar = Pokaż pasek nagłówka
show-header-description = Ukazuje pasek nagłówka z menu wybieranego prawym przyciskiem myszki
# Find
find-placeholder = Szukaj…
find-placeholder = Wyszukaj…
find-previous = Znajdź poprzedni
find-next = Znajdź następny
@ -88,7 +88,7 @@ new-window = Nowe okno
profile = Profil
menu-profiles = Profile...
close-tab = Zamknij kartę
quit = Zamknij
quit = Zakończ
## Edit
@ -96,7 +96,7 @@ edit = Edytuj
copy = Kopiuj
paste = Wklej
select-all = Zaznacz wszystko
find = Szukaj
find = Wyszukaj
clear-scrollback = Wyczyść przewijanie
## View
@ -109,7 +109,7 @@ next-tab = Następna karta
previous-tab = Poprzednia karta
split-horizontal = Podziel w poziomie
split-vertical = Podziel w pionie
pane-toggle-maximize = Przełącznik maksymalizacji
pane-toggle-maximize = Maksymalizuj lub przywróć panel
menu-color-schemes = Schemat kolorów...
menu-settings = Ustawienia…
menu-about = O Terminalu COSMIC...
@ -149,3 +149,5 @@ focus-pane-up = Aktywuj panel wyżej
copy-link = Skopiuj Odnośnik
tab-new-inherit-working-directory = Nowe karty i okna użyją obecnego katalogu
tab-new-inherit-working-directory-description = Otwieraj nowe karty i okna w katalogu roboczym aktywnej karty
show-pane-borders = Pokaż obramowania paneli
window-header = Nagłówek okna

View file

@ -70,7 +70,7 @@ focus-follow-mouse = Foco de digitação segue o mouse
### Advanced
advanced = Avançado
show-headerbar = Mostrar a barra de título
show-headerbar = Mostrar cabeçalho
show-header-description = Mostrar o cabeçalho ao clicar no menu com o botão direito do mouse
# Find
find-placeholder = Localizar...
@ -88,7 +88,7 @@ new-window = Nova janela
profile = Perfil
menu-profiles = Perfis...
close-tab = Fechar aba
quit = Sair
quit = Encerrar
## Edit
@ -109,7 +109,7 @@ next-tab = Próxima aba
previous-tab = Aba anterior
split-horizontal = Dividir horizontalmente
split-vertical = Dividir verticalmente
pane-toggle-maximize = Alternar maximização
pane-toggle-maximize = Maximizar ou restaurar o quadro
menu-color-schemes = Esquemas de cores...
menu-settings = Configurações...
menu-about = Sobre o Terminal...
@ -149,3 +149,7 @@ type-to-search = Digite para pesquisar...
copy-link = Copiar link
tab-new-inherit-working-directory = Novas abas e janelas usam o diretório atual
tab-new-inherit-working-directory-description = Abrir novas abas e janelas no diretório de trabalho da aba ativa
show-pane-borders = Mostrar bordas do quadro
window-header = Cabeçalho da janela
move-tab-left = Mover a aba para a esquerda
move-tab-right = Mover a aba para a direita

View file

@ -1,5 +1,5 @@
cosmic-terminal = Терминал
comment = Эмулятор терминала для рабочей среды COSMIC
comment = Эмулятор терминала для среды COSMIC
keywords = Команда;Оболочка;Терминал;Консоль;
new-terminal = Новый терминал
@ -86,7 +86,7 @@ new-window = Новое окно
profile = Профиль
menu-profiles = Профили...
close-tab = Закрыть вкладку
quit = Выйти
quit = Завершить
## Edit
@ -106,7 +106,7 @@ next-tab = Следующая вкладка
previous-tab = Предыдущая вкладка
split-horizontal = Разделение по горизонтали
split-vertical = Разделение по вертикали
pane-toggle-maximize = Переключить на весь экран
pane-toggle-maximize = Развернуть/восстановить область
menu-color-schemes = Цветовые схемы...
menu-settings = Параметры...
menu-about = О Терминале COSMIC...
@ -141,5 +141,9 @@ tab-activate = Активировать вкладку { $number }
toggle-fullscreen = Вкл./выкл. полноэкранный режим
type-to-search = Введите для поиска...
copy-link = Копировать ссылку
tab-new-inherit-working-directory = В новых вкладках исп. текущий каталог
tab-new-inherit-working-directory-description = Открывать новые вкладки в рабочем каталоге активной вкладки
tab-new-inherit-working-directory = Текущий каталог в новых вкладках и окнах
tab-new-inherit-working-directory-description = Открывать новые вкладки и окна в рабочем каталоге активной вкладки
show-pane-borders = Показывать границы областей
window-header = Заголовок окна
move-tab-left = Перемещение вкладки налево
move-tab-right = Перемещение вкладки направо

2
i18n/sat/cosmic_term.ftl Normal file
View file

@ -0,0 +1,2 @@
cancel = ᱵᱟᱹᱛᱤᱞ
name = ᱧᱩᱛᱩᱢ

View file

@ -110,10 +110,10 @@ next-tab = Nästa flik
previous-tab = Föregående flik
split-horizontal = Dela horisontellt
split-vertical = Dela vertikalt
pane-toggle-maximize = Växla maximerad
pane-toggle-maximize = Maximera eller återställ fält
clear-scrollback = Töm tillbakarullning
menu-color-schemes = Färgscheman…
menu-settings = Inställningar…
menu-settings = Inställningar...
menu-about = Om COSMIC Terminal…
repository = Källkod
support = Support
@ -153,3 +153,7 @@ shortcut-group-zoom = Zooma
copy-link = Kopiera länk
tab-new-inherit-working-directory-description = Öppna nya flikar och fönster i den aktiva flikens arbetskatalog
tab-new-inherit-working-directory = Nya flikar och fönster använder aktuell katalog
show-pane-borders = Visa ram runt fält
window-header = Fönstertitel
move-tab-left = Flytta flik vänster
move-tab-right = Flytta flik höger

View file

@ -39,8 +39,8 @@ settings = การตั้งค่า
appearance = ลักษณะ
theme = ธีม
match-desktop = ใช้ตามธีมหน้าจอหลัก
dark = ธีมมืด
light = ธีมสว่าง
dark = มืด
light = สว่าง
syntax-dark = โทนสีมืด
syntax-light = โทนสีสว่าง
default-zoom-step = ระยะซูม
@ -109,3 +109,8 @@ pane-toggle-maximize = ขยายให้ใหญ่สุด
menu-color-schemes = โทนสี...
menu-settings = การตั้งค่า...
menu-about = เกี่ยวกับ เทอร์มินัล COSMIC...
cancel = ยกเลิก
reset-to-default = รีเซ็ตเป็นค่าเริ่มต้น
type-to-search = พิมพ์เพื่อค้นหา
add-another-keybinding = เพิ่มคีย์ลัดเพิ่มเติม
replace = แทนที่

View file

@ -38,7 +38,7 @@ settings = Налаштування
appearance = Вигляд
theme = Тема
match-desktop = Системна
match-desktop = Підігнати під стільницю
dark = Темна
light = Світла
syntax-dark = Темна колірна схема
@ -84,7 +84,7 @@ new-window = Нове вікно
profile = Профіль
menu-profiles = Профілі...
close-tab = Закрити вкладку
quit = Вийти
quit = Завершити
## Edit
@ -147,5 +147,9 @@ focus-pane-up = Фокус на верхню панель
comment = Емулятор терміналу для середовища COSMIC
keywords = Команда;Оболонка;Термінал;Інтерфейс;CLI;
copy-link = Копіювати ланку
tab-new-inherit-working-directory = У нових вкладках використовується поточний каталог
tab-new-inherit-working-directory-description = Відкривати нові вкладки в робочому каталозі активної вкладки
tab-new-inherit-working-directory = Нові вкладки та вікна відкривати в поточному каталозі
tab-new-inherit-working-directory-description = Відкривати нові вкладки та вікна в робочому каталозі активної вкладки
show-pane-borders = Показувати межі панелей
window-header = Заголовок вікна
move-tab-left = Перемістити ліворуч
move-tab-right = Перемістити праворуч

View file

@ -105,7 +105,7 @@ next-tab = 下一个标签
previous-tab = 上一个标签
split-horizontal = 水平分割
split-vertical = 垂直分割
pane-toggle-maximize = 切换最大化
pane-toggle-maximize = 将窗格最大化或还原
menu-color-schemes = 配色方案…
menu-settings = 设置…
menu-about = 关于 COSMIC 终端…
@ -149,3 +149,7 @@ keywords = 指令;外壳;终端;命令行界面;
copy-link = 复制链接
tab-new-inherit-working-directory = 新标签和窗口使用当前目录
tab-new-inherit-working-directory-description = 在活动标签的工作目录中打开新标签和窗口
show-pane-borders = 显示窗格边框
window-header = 窗口标题栏
move-tab-left = 将标签向左移动
move-tab-right = 将标签向右移动

View file

@ -10,7 +10,7 @@ new-terminal = 新建終端機
## Color schemes
color-schemes = 配色方案
rename = 重命名
rename = 重新命名
export = 匯出
delete = 刪除
import = 匯入
@ -24,7 +24,7 @@ command-line = 命令列
tab-title = 分頁標題
tab-title-description = 覆寫預設分頁標題
add-profile = 新增設定檔
new-profile = 新建設定檔
new-profile = 新增設定檔
make-default = 設為預設值
working-directory = 工作目錄
hold = 保留
@ -79,12 +79,12 @@ find-next = 下一個
## File
file = 檔案
new-tab = 新建分頁
new-window = 新建視窗
new-tab = 新增分頁
new-window = 新增視窗
profile = 設定檔案
menu-profiles = 設定檔案...
close-tab = 關閉分頁
quit = 退出
quit = 結束
## Edit
@ -104,16 +104,16 @@ next-tab = 下個分頁
previous-tab = 上個分頁
split-horizontal = 水平分割
split-vertical = 垂直分割
pane-toggle-maximize = 切換最大化
pane-toggle-maximize = 將窗格最大化或還原
menu-color-schemes = 配色方案...
menu-settings = 設定...
menu-about = 關於 COSMIC 終端機…
cancel = 取消
repository = 軟體庫源
repository = 軟體庫
support = 支援
keyboard-shortcuts = 鍵盤快速鍵
keywords = 指令;殼層;終端;指令行介面CLI;
add-another-keybinding = 添加另一個按鍵組合
add-another-keybinding = 新增另一個鍵盤快速鍵
close-window = 關閉視窗
copy-or-sigint = 複製或 SIGINT
disable = 停用
@ -136,7 +136,7 @@ focus-pane-up = 聚焦上方窗格
open-link = 開啟連結
menu-password-manager = 密碼…
passwords-title = 密碼
add-password = 添增密碼
add-password = 新增密碼
password-input = 密碼
password-input-description = 描述
shortcut-replace-title = 取代快速鍵?
@ -149,3 +149,7 @@ clear-scrollback = 清除捲動回朔
comment = COSMIC 桌面終端機模擬器
tab-new-inherit-working-directory = 新分頁和視窗使用目前目錄
tab-new-inherit-working-directory-description = 在作用中分頁的工作目錄開啟新分頁和視窗
show-pane-borders = 顯示窗格邊框
window-header = 視窗標題列
move-tab-left = 將分頁向左移動
move-tab-right = 將分頁向右移動

View file

@ -231,17 +231,34 @@ pub struct Config {
pub opacity: u8,
pub profiles: BTreeMap<ProfileId, Profile>,
pub show_headerbar: bool,
pub show_pane_borders: bool,
/// [yoda] Voile posé sur les panneaux non focalisés, en pourcentage
/// (`unfocused-split-opacity` de Ghostty : 0,7 d'opacité = 30 % de voile).
#[serde(default = "default_unfocused_pane_dim")]
pub unfocused_pane_dim: u8,
pub use_bright_bold: bool,
pub syntax_theme_dark: String,
pub syntax_theme_light: String,
pub focus_follow_mouse: bool,
#[serde(default)]
pub tab_new_inherit_working_directory: bool,
/// [yoda] Copier aussi la sélection dans le presse-papiers, et pas
/// seulement dans la sélection primaire (`copy-on-select = clipboard` de
/// Ghostty).
#[serde(default)]
pub copy_on_select_clipboard: bool,
pub default_profile: Option<ProfileId>,
#[serde(default)]
pub shortcuts_custom: Shortcuts,
}
/// Voile maximal des panneaux non focalisés : Ghostty borne leur opacité à 0,15.
pub const UNFOCUSED_PANE_DIM_MAX: u8 = 85;
fn default_unfocused_pane_dim() -> u8 {
30
}
impl Default for Config {
fn default() -> Self {
Self {
@ -252,6 +269,7 @@ impl Default for Config {
dim_font_weight: Weight::NORMAL.0,
focus_follow_mouse: false,
tab_new_inherit_working_directory: false,
copy_on_select_clipboard: false,
font_name: "Noto Sans Mono".to_string(),
font_size: 14,
font_size_zoom_step_mul_100: 100,
@ -260,6 +278,8 @@ impl Default for Config {
opacity: 100,
profiles: BTreeMap::new(),
show_headerbar: true,
show_pane_borders: false,
unfocused_pane_dim: default_unfocused_pane_dim(),
syntax_theme_dark: COSMIC_THEME_DARK.to_string(),
syntax_theme_light: COSMIC_THEME_LIGHT.to_string(),
use_bright_bold: false,
@ -341,6 +361,12 @@ impl Config {
Metrics::new(font_size, line_height)
}
/// Opacité du voile des panneaux non focalisés ; `None` si désactivé.
pub fn unfocused_pane_dim_ratio(&self) -> Option<f32> {
let dim = self.unfocused_pane_dim.min(UNFOCUSED_PANE_DIM_MAX);
(dim > 0).then(|| f32::from(dim) / 100.0)
}
pub fn opacity_ratio(&self) -> f32 {
f32::from(self.opacity) / 100.0
}
@ -402,3 +428,22 @@ impl Config {
})[&self.font_stretch]
}
}
#[cfg(test)]
mod tests {
use super::{Config, UNFOCUSED_PANE_DIM_MAX};
#[test]
fn unfocused_pane_dim_ratio() {
let mut config = Config::default();
// Défaut de Ghostty : opacité 0,7, soit 30 % de voile
assert_eq!(config.unfocused_pane_dim_ratio(), Some(0.3));
config.unfocused_pane_dim = 0;
assert_eq!(config.unfocused_pane_dim_ratio(), None);
config.unfocused_pane_dim = 100;
assert_eq!(
config.unfocused_pane_dim_ratio(),
Some(f32::from(UNFOCUSED_PANE_DIM_MAX) / 100.0)
);
}
}

185
src/desktop_notify.rs Normal file
View file

@ -0,0 +1,185 @@
// SPDX-License-Identifier: GPL-3.0-only
//! Notifications de bureau des OSC 9 / 777 (yoda).
//!
//! On parle directement à `org.freedesktop.Notifications` plutôt que de passer
//! par notify-rust : au clic, cosmic-notifications émet `ActivationToken` puis
//! `ActionInvoked`, et c'est ce jeton xdg-activation qui permet de ramener la
//! fenêtre au premier plan sur le bon onglet.
//!
//! Une notification par onglet au plus : la suivante remplace la précédente
//! (`replaces_id`).
use cosmic::{
Application,
iced::{
Subscription,
futures::{SinkExt, channel::mpsc::Sender},
stream,
},
widget::{pane_grid, segmented_button},
};
use std::{any::TypeId, collections::HashMap};
use tokio::sync::mpsc;
use crate::fl;
/// Onglet à l'origine d'une notification.
pub type TabKey = (pane_grid::Pane, segmented_button::Entity);
/// Demande envoyée par l'application à la tâche D-Bus.
#[derive(Debug)]
pub enum Request {
Show {
tab: TabKey,
summary: String,
body: String,
},
}
/// Événement renvoyé par la tâche D-Bus à l'application.
#[derive(Clone, Debug)]
pub enum Event {
/// Canal prêt : l'application peut envoyer ses demandes.
Ready(mpsc::UnboundedSender<Request>),
/// L'utilisateur a cliqué la notification de cet onglet.
Activated { tab: TabKey, token: Option<String> },
}
#[zbus::proxy(
interface = "org.freedesktop.Notifications",
default_service = "org.freedesktop.Notifications",
default_path = "/org/freedesktop/Notifications"
)]
trait Notifications {
#[allow(clippy::too_many_arguments)]
fn notify(
&self,
app_name: &str,
replaces_id: u32,
app_icon: &str,
summary: &str,
body: &str,
actions: &[&str],
hints: HashMap<&str, zbus::zvariant::Value<'_>>,
expire_timeout: i32,
) -> zbus::Result<u32>;
#[zbus(signal)]
fn action_invoked(&self, id: u32, action_key: String) -> zbus::Result<()>;
#[zbus(signal)]
fn activation_token(&self, id: u32, activation_token: String) -> zbus::Result<()>;
#[zbus(signal)]
fn notification_closed(&self, id: u32, reason: u32) -> zbus::Result<()>;
}
struct DesktopNotifySubscription;
pub fn subscription() -> Subscription<Event> {
Subscription::run_with(TypeId::of::<DesktopNotifySubscription>(), |_| {
stream::channel(16, |mut output: Sender<Event>| async move {
let (request_tx, request_rx) = mpsc::unbounded_channel();
if output.send(Event::Ready(request_tx)).await.is_err() {
return;
}
if let Err(err) = run(request_rx, output).await {
log::warn!("desktop notifications unavailable: {err}");
}
})
})
}
async fn run(
mut request_rx: mpsc::UnboundedReceiver<Request>,
mut output: Sender<Event>,
) -> zbus::Result<()> {
use cosmic::iced::futures::StreamExt;
let connection = zbus::Connection::session().await?;
let proxy = NotificationsProxy::new(&connection).await?;
let mut tokens = proxy.receive_activation_token().await?;
let mut invoked = proxy.receive_action_invoked().await?;
let mut closed = proxy.receive_notification_closed().await?;
let app_name = fl!("cosmic-terminal");
let show_label = fl!("notification-show");
let mut tab_by_id: HashMap<u32, TabKey> = HashMap::new();
let mut id_by_tab: HashMap<TabKey, u32> = HashMap::new();
let mut pending_tokens: HashMap<u32, String> = HashMap::new();
loop {
tokio::select! {
request = request_rx.recv() => {
let Some(Request::Show { tab, summary, body }) = request else {
return Ok(());
};
let replaces_id = id_by_tab.get(&tab).copied().unwrap_or(0);
let body = neutralize_markup(&body);
let hints = HashMap::from([
("desktop-entry", zbus::zvariant::Value::from(crate::App::APP_ID)),
("urgency", zbus::zvariant::Value::U8(1)),
]);
match proxy
.notify(
&app_name,
replaces_id,
crate::App::APP_ID,
&summary,
&body,
&["default", &show_label],
hints,
-1,
)
.await
{
Ok(id) => {
if replaces_id != 0 && replaces_id != id {
tab_by_id.remove(&replaces_id);
}
tab_by_id.insert(id, tab);
id_by_tab.insert(tab, id);
}
Err(err) => log::warn!("failed to send desktop notification: {err}"),
}
}
Some(signal) = tokens.next() => {
if let Ok(args) = signal.args()
&& tab_by_id.contains_key(args.id())
{
pending_tokens.insert(*args.id(), args.activation_token().clone());
}
}
Some(signal) = invoked.next() => {
if let Ok(args) = signal.args()
&& let Some(tab) = tab_by_id.get(args.id()).copied()
{
let token = pending_tokens.remove(args.id());
if output.send(Event::Activated { tab, token }).await.is_err() {
return Ok(());
}
}
}
Some(signal) = closed.next() => {
if let Ok(args) = signal.args()
&& let Some(tab) = tab_by_id.remove(args.id())
{
if id_by_tab.get(&tab) == Some(args.id()) {
id_by_tab.remove(&tab);
}
pending_tokens.remove(args.id());
}
}
}
}
}
/// Le corps d'une notification est du balisage (`body-markup`). Échapper `<`
/// en `&lt;` serait conforme à la spécification, mais cosmic-notifications ne
/// décode pas les entités (il afficherait `&lt;`), et un `<` brut y avale le
/// texte qui suit. Les chevrons deviennent donc des guillemets simples
/// d'aspect voisin, lisibles sur tous les serveurs.
fn neutralize_markup(body: &str) -> String {
body.replace('<', "‹").replace('>', "›")
}

697
src/kitty_keyboard.rs Normal file
View file

@ -0,0 +1,697 @@
// SPDX-License-Identifier: GPL-3.0-only
//! Protocole clavier kitty (yoda).
//!
//! Avec `kitty_keyboard` actif, alacritty_terminal tient la pile des modes
//! (`CSI > n u`, `CSI < n u`, `CSI = n ; m u`) et répond à la requête
//! `CSI ? u`. Il n'encode en revanche aucune touche : c'est le rôle du frontal,
//! donc de ce module. Il ne sert que quand l'application au premier plan a
//! demandé le protocole ; sinon l'encodage legacy de `terminal_box` s'applique
//! seul.
//!
//! C'est ce qui donne Shift+Entrée (`CSI 13;2u`) à Claude Code, qui interroge
//! le terminal avec `CSI ? u` au démarrage.
//!
//! Spécification : <https://sw.kovidgoyal.net/kitty/keyboard-protocol/>. La
//! découpe suit l'encodeur du frontal Alacritty (`alacritty/src/input/keyboard.rs`).
//!
//! Limites : les drapeaux 2 (types d'événements), 4 (touches alternatives) et
//! 16 (texte associé) ne sont honorés qu'avec le drapeau 1 (désambiguïsation)
//! ou 8 (toutes les touches), ce que font toutes les applications connues.
//! La touche de la disposition de base (3e champ du drapeau 4) n'est pas
//! envoyée, et Caps Lock / Num Lock ne figurent pas dans les modificateurs
//! (iced ne les expose pas).
use alacritty_terminal::term::TermMode;
use cosmic::iced::keyboard::{Key, Location, Modifiers, key::Named};
/// Nature de l'événement clavier.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum KeyAction {
Press,
Repeat,
Release,
}
/// Événement clavier tel que livré par iced.
pub struct KeyInput<'a> {
/// Touche sans aucun modificateur (`key_without_modifiers` de winit).
pub key: &'a Key,
/// Touche avec Shift (et AltGr) appliqués, mais pas Ctrl (`logical_key`).
pub modified_key: &'a Key,
pub location: Location,
pub modifiers: Modifiers,
/// Texte produit, tous modificateurs appliqués.
pub text: Option<&'a str>,
pub action: KeyAction,
}
const SHIFT: u8 = 1;
const ALT: u8 = 2;
const CTRL: u8 = 4;
const SUPER: u8 = 8;
/// Le protocole kitty pilote-t-il l'encodage des touches ?
pub fn active(mode: TermMode) -> bool {
mode.intersects(TermMode::DISAMBIGUATE_ESC_CODES | TermMode::REPORT_ALL_KEYS_AS_ESC)
}
/// Octets à envoyer au PTY quand [`active`] est vrai. Vide : rien à envoyer.
pub fn encode(input: &KeyInput, mode: TermMode) -> Vec<u8> {
let all = mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC);
let mods = modifier_bits(input);
let named = match input.modified_key {
Key::Named(named) => Some(*named),
_ => None,
};
if input.action == KeyAction::Release {
if !mode.contains(TermMode::REPORT_EVENT_TYPES) {
return Vec::new();
}
// Pas de relâchement d'Entrée, Tab et Retour arrière hors « toutes les
// touches » : le shell qui reprend la main après un plantage les
// recevrait.
if !all && matches!(named, Some(Named::Enter | Named::Tab | Named::Backspace)) {
return Vec::new();
}
return build_sequence(input, mode, mods).unwrap_or_default();
}
// Sans modificateur, Entrée, Tab et Retour arrière gardent leurs octets
// legacy (spécification : un `reset` doit rester tapable). L'Entrée du
// pavé numérique, elle, a son code dédié.
if !all && mods == 0 && input.location != Location::Numpad {
match named {
Some(Named::Enter) => return b"\r".to_vec(),
Some(Named::Tab) => return b"\t".to_vec(),
Some(Named::Backspace) => return b"\x7f".to_vec(),
_ => {}
}
}
let text = printable_text(input);
let as_sequence = all
|| text.is_none()
|| named == Some(Named::Escape)
|| (mods != 0 && mods != SHIFT)
|| (mods == SHIFT && matches!(named, Some(Named::Enter | Named::Tab | Named::Backspace)));
if as_sequence {
build_sequence(input, mode, mods).unwrap_or_default()
} else {
text.unwrap_or_default().as_bytes().to_vec()
}
}
/// Texte imprimable produit par la touche, s'il y en a un.
fn printable_text<'a>(input: &KeyInput<'a>) -> Option<&'a str> {
input
.text
.filter(|text| !text.is_empty() && !text.chars().any(char::is_control))
}
fn modifier_bits(input: &KeyInput) -> u8 {
let mut bits = 0;
if input.modifiers.shift() {
bits |= SHIFT;
}
if input.modifiers.alt() {
bits |= ALT;
}
if input.modifiers.control() {
bits |= CTRL;
}
if input.modifiers.logo() {
bits |= SUPER;
}
// Pour une touche de modification, l'état rapporté est celui qui suit
// l'événement : le bit est posé à l'appui, retiré au relâchement.
let own = match input.modified_key {
Key::Named(Named::Shift) => SHIFT,
Key::Named(Named::Alt) => ALT,
Key::Named(Named::Control) => CTRL,
Key::Named(Named::Super) => SUPER,
_ => 0,
};
if input.action == KeyAction::Release {
bits & !own
} else {
bits | own
}
}
/// Terminateur d'une séquence : `u` pour les codes kitty, sinon le final
/// legacy (`A`, `~`…) que le protocole conserve pour les touches de fonction.
#[derive(Clone, Copy)]
enum Final {
Kitty,
Legacy(char),
}
fn build_sequence(input: &KeyInput, mode: TermMode, mods: u8) -> Option<Vec<u8>> {
let event_type = mode.contains(TermMode::REPORT_EVENT_TYPES)
&& matches!(input.action, KeyAction::Repeat | KeyAction::Release);
let associated_text = printable_text(input).filter(|_| {
mode.contains(TermMode::REPORT_ASSOCIATED_TEXT) && input.action != KeyAction::Release
});
// Le paramètre par défaut vaut 1 : `CSI A` plutôt que `CSI 1 A`.
let explicit = mods != 0 || event_type || associated_text.is_some();
let (payload, terminator) = numpad(input)
.or_else(|| functional(input, explicit))
.or_else(|| special(input, mode))
.or_else(|| textual(input, mode, associated_text.is_some()))?;
let mut sequence = format!("\x1b[{payload}");
if explicit {
sequence.push_str(&format!(";{}", mods + 1));
}
if event_type {
let kind = match input.action {
KeyAction::Press => '1',
KeyAction::Repeat => '2',
KeyAction::Release => '3',
};
sequence.push(':');
sequence.push(kind);
}
if let Some(text) = associated_text {
let codepoints: Vec<String> = text.chars().map(|c| u32::from(c).to_string()).collect();
sequence.push(';');
sequence.push_str(&codepoints.join(":"));
}
sequence.push(match terminator {
Final::Kitty => 'u',
Final::Legacy(c) => c,
});
Some(sequence.into_bytes())
}
/// Pavé numérique : codes kitty dédiés (57399 et suivants).
fn numpad(input: &KeyInput) -> Option<(String, Final)> {
if input.location != Location::Numpad {
return None;
}
let code = match input.modified_key {
Key::Character(c) => match c.as_str() {
"0" => 57399,
"1" => 57400,
"2" => 57401,
"3" => 57402,
"4" => 57403,
"5" => 57404,
"6" => 57405,
"7" => 57406,
"8" => 57407,
"9" => 57408,
"." | "," => 57409,
"/" => 57410,
"*" => 57411,
"-" => 57412,
"+" => 57413,
"=" => 57415,
_ => return None,
},
Key::Named(named) => match named {
Named::Enter => 57414,
Named::ArrowLeft => 57417,
Named::ArrowRight => 57418,
Named::ArrowUp => 57419,
Named::ArrowDown => 57420,
Named::PageUp => 57421,
Named::PageDown => 57422,
Named::Home => 57423,
Named::End => 57424,
Named::Insert => 57425,
Named::Delete => 57426,
Named::Clear => 57427,
_ => return None,
},
Key::Unidentified => return None,
};
Some((code.to_string(), Final::Kitty))
}
/// Touches de fonction : finals legacy conservés par le protocole, sauf F3
/// (`CSI R` serait pris pour un rapport de position du curseur) et F13+.
fn functional(input: &KeyInput, explicit: bool) -> Option<(String, Final)> {
let Key::Named(named) = input.modified_key else {
return None;
};
let one = if explicit { "1" } else { "" };
let (payload, terminator) = match named {
Named::ArrowUp => (one, Final::Legacy('A')),
Named::ArrowDown => (one, Final::Legacy('B')),
Named::ArrowRight => (one, Final::Legacy('C')),
Named::ArrowLeft => (one, Final::Legacy('D')),
Named::End => (one, Final::Legacy('F')),
Named::Home => (one, Final::Legacy('H')),
Named::F1 => (one, Final::Legacy('P')),
Named::F2 => (one, Final::Legacy('Q')),
Named::F3 => ("13", Final::Legacy('~')),
Named::F4 => (one, Final::Legacy('S')),
Named::Insert => ("2", Final::Legacy('~')),
Named::Delete => ("3", Final::Legacy('~')),
Named::PageUp => ("5", Final::Legacy('~')),
Named::PageDown => ("6", Final::Legacy('~')),
Named::F5 => ("15", Final::Legacy('~')),
Named::F6 => ("17", Final::Legacy('~')),
Named::F7 => ("18", Final::Legacy('~')),
Named::F8 => ("19", Final::Legacy('~')),
Named::F9 => ("20", Final::Legacy('~')),
Named::F10 => ("21", Final::Legacy('~')),
Named::F11 => ("23", Final::Legacy('~')),
Named::F12 => ("24", Final::Legacy('~')),
_ => {
let code = match named {
Named::F13 => 57376,
Named::F14 => 57377,
Named::F15 => 57378,
Named::F16 => 57379,
Named::F17 => 57380,
Named::F18 => 57381,
Named::F19 => 57382,
Named::F20 => 57383,
Named::F21 => 57384,
Named::F22 => 57385,
Named::F23 => 57386,
Named::F24 => 57387,
Named::CapsLock => 57358,
Named::ScrollLock => 57359,
Named::NumLock => 57360,
Named::PrintScreen => 57361,
Named::Pause => 57362,
Named::ContextMenu => 57363,
Named::MediaPlay => 57428,
Named::MediaPause => 57429,
Named::MediaPlayPause => 57430,
Named::MediaStop => 57432,
Named::MediaFastForward => 57433,
Named::MediaRewind => 57434,
Named::MediaTrackNext => 57435,
Named::MediaTrackPrevious => 57436,
Named::MediaRecord => 57437,
Named::AudioVolumeDown => 57438,
Named::AudioVolumeUp => 57439,
Named::AudioVolumeMute => 57440,
_ => return None,
};
return Some((code.to_string(), Final::Kitty));
}
};
Some((payload.to_string(), terminator))
}
/// Touches de contrôle (Entrée, Tab, Échap, Retour arrière, Espace) et, avec
/// « toutes les touches », les touches de modification elles-mêmes.
fn special(input: &KeyInput, mode: TermMode) -> Option<(String, Final)> {
let code = match input.modified_key {
Key::Named(Named::Enter) => 13,
Key::Named(Named::Tab) => 9,
Key::Named(Named::Escape) => 27,
Key::Named(Named::Backspace) => 127,
Key::Character(c) if c == " " => 32,
Key::Named(modifier) if mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) => {
let right = input.location == Location::Right;
match (modifier, right) {
(Named::Shift, false) => 57441,
(Named::Control, false) => 57442,
(Named::Alt, false) => 57443,
(Named::Super, false) => 57444,
(Named::Hyper, false) => 57445,
(Named::Meta, false) => 57446,
(Named::Shift, true) => 57447,
(Named::Control, true) => 57448,
(Named::Alt, true) => 57449,
(Named::Super, true) => 57450,
(Named::Hyper, true) => 57451,
(Named::Meta, true) => 57452,
(Named::AltGraph, _) => 57453,
_ => return None,
}
}
_ => return None,
};
Some((code.to_string(), Final::Kitty))
}
/// Touches de caractère : code de la touche non décalée, suivi de la touche
/// décalée si le drapeau 4 le demande (`CSI 97:65;6u` pour Ctrl+Shift+A).
fn textual(input: &KeyInput, mode: TermMode, has_text: bool) -> Option<(String, Final)> {
let Key::Character(shifted) = input.modified_key else {
return None;
};
let mut chars = shifted.chars();
let (Some(shifted_char), None) = (chars.next(), chars.next()) else {
// Plusieurs caractères pour une touche : seul « toutes les touches »
// avec texte associé sait le dire, par le code 0.
return (mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) && has_text)
.then(|| ("0".to_string(), Final::Kitty));
};
let unshifted_char = if input.modifiers.shift() {
shifted_char.to_lowercase().next().unwrap_or(shifted_char)
} else {
shifted_char
};
let mut code = u32::from(unshifted_char);
let alternate = u32::from(shifted_char);
// `!` n'a pas de minuscule : la touche de base est celle sans modificateur
// (`1` en QWERTY ; `&` pour le `1` de l'AZERTY).
if code == alternate
&& let Key::Character(base) = input.key
&& let Some(base_char) = base.chars().next()
{
code = u32::from(base_char);
}
let payload = if mode.contains(TermMode::REPORT_ALTERNATE_KEYS) && alternate != code {
format!("{code}:{alternate}")
} else {
code.to_string()
};
Some((payload, Final::Kitty))
}
#[cfg(test)]
mod tests {
use super::*;
const DISAMBIGUATE: TermMode = TermMode::DISAMBIGUATE_ESC_CODES;
fn ch(c: &str) -> Key {
Key::Character(c.into())
}
fn named(n: Named) -> Key {
Key::Named(n)
}
fn press(
key: Key,
modified: Key,
modifiers: Modifiers,
text: Option<&str>,
mode: TermMode,
) -> String {
event(key, modified, modifiers, text, KeyAction::Press, mode)
}
fn event(
key: Key,
modified: Key,
modifiers: Modifiers,
text: Option<&str>,
action: KeyAction,
mode: TermMode,
) -> String {
let input = KeyInput {
key: &key,
modified_key: &modified,
location: Location::Standard,
modifiers,
text,
action,
};
String::from_utf8(encode(&input, mode)).unwrap()
}
#[test]
fn shift_enter_for_claude_code() {
let enter = || named(Named::Enter);
assert_eq!(
press(enter(), enter(), Modifiers::SHIFT, Some("\r"), DISAMBIGUATE),
"\x1b[13;2u"
);
assert_eq!(
press(enter(), enter(), Modifiers::CTRL, Some("\r"), DISAMBIGUATE),
"\x1b[13;5u"
);
// Entrée seule reste `\r`
assert_eq!(
press(enter(), enter(), Modifiers::empty(), Some("\r"), DISAMBIGUATE),
"\r"
);
}
#[test]
fn escape_tab_backspace() {
let esc = || named(Named::Escape);
assert_eq!(
press(esc(), esc(), Modifiers::empty(), Some("\x1b"), DISAMBIGUATE),
"\x1b[27u"
);
let tab = || named(Named::Tab);
assert_eq!(
press(tab(), tab(), Modifiers::empty(), Some("\t"), DISAMBIGUATE),
"\t"
);
assert_eq!(
press(tab(), tab(), Modifiers::SHIFT, Some("\t"), DISAMBIGUATE),
"\x1b[9;2u"
);
let bs = || named(Named::Backspace);
assert_eq!(
press(bs(), bs(), Modifiers::empty(), Some("\x08"), DISAMBIGUATE),
"\x7f"
);
assert_eq!(
press(bs(), bs(), Modifiers::CTRL, Some("\x08"), DISAMBIGUATE),
"\x1b[127;5u"
);
}
#[test]
fn text_keys() {
// Texte simple ou décalé : envoyé tel quel
assert_eq!(
press(ch("a"), ch("a"), Modifiers::empty(), Some("a"), DISAMBIGUATE),
"a"
);
assert_eq!(
press(ch("a"), ch("A"), Modifiers::SHIFT, Some("A"), DISAMBIGUATE),
"A"
);
assert_eq!(
press(ch("é"), ch("é"), Modifiers::empty(), Some("é"), DISAMBIGUATE),
"é"
);
// Ctrl et Alt : séquences
assert_eq!(
press(ch("c"), ch("c"), Modifiers::CTRL, Some("\x03"), DISAMBIGUATE),
"\x1b[99;5u"
);
assert_eq!(
press(ch("a"), ch("a"), Modifiers::ALT, Some("a"), DISAMBIGUATE),
"\x1b[97;3u"
);
assert_eq!(
press(ch(" "), ch(" "), Modifiers::CTRL, Some("\0"), DISAMBIGUATE),
"\x1b[32;5u"
);
}
#[test]
fn alternate_keys() {
let mode = DISAMBIGUATE | TermMode::REPORT_ALTERNATE_KEYS;
assert_eq!(
press(
ch("a"),
ch("A"),
Modifiers::CTRL | Modifiers::SHIFT,
Some("\x01"),
mode
),
"\x1b[97:65;6u"
);
// AZERTY : Ctrl+Shift+& donne « 1 »
assert_eq!(
press(
ch("&"),
ch("1"),
Modifiers::CTRL | Modifiers::SHIFT,
None,
mode
),
"\x1b[38:49;6u"
);
// Sans le drapeau 4, pas d'alternative
assert_eq!(
press(
ch("a"),
ch("A"),
Modifiers::CTRL | Modifiers::SHIFT,
Some("\x01"),
DISAMBIGUATE
),
"\x1b[97;6u"
);
}
#[test]
fn functional_keys() {
let up = || named(Named::ArrowUp);
assert_eq!(
press(up(), up(), Modifiers::empty(), None, DISAMBIGUATE),
"\x1b[A"
);
assert_eq!(
press(up(), up(), Modifiers::CTRL, None, DISAMBIGUATE),
"\x1b[1;5A"
);
let f3 = || named(Named::F3);
assert_eq!(
press(f3(), f3(), Modifiers::empty(), None, DISAMBIGUATE),
"\x1b[13~"
);
let del = || named(Named::Delete);
assert_eq!(
press(del(), del(), Modifiers::SHIFT, None, DISAMBIGUATE),
"\x1b[3;2~"
);
}
#[test]
fn event_types() {
let mode = DISAMBIGUATE | TermMode::REPORT_EVENT_TYPES;
assert_eq!(
event(
ch("a"),
ch("a"),
Modifiers::empty(),
None,
KeyAction::Release,
mode
),
"\x1b[97;1:3u"
);
assert_eq!(
event(
ch("a"),
ch("a"),
Modifiers::CTRL,
Some("\x01"),
KeyAction::Repeat,
mode
),
"\x1b[97;5:2u"
);
// Pas de relâchement d'Entrée hors « toutes les touches »
let enter = || named(Named::Enter);
assert_eq!(
event(
enter(),
enter(),
Modifiers::empty(),
None,
KeyAction::Release,
mode
),
""
);
// Ni de relâchement du tout sans le drapeau 2
assert_eq!(
event(
ch("a"),
ch("a"),
Modifiers::empty(),
None,
KeyAction::Release,
DISAMBIGUATE
),
""
);
}
#[test]
fn report_all_keys() {
let mode = TermMode::REPORT_ALL_KEYS_AS_ESC | TermMode::REPORT_ASSOCIATED_TEXT;
assert_eq!(
press(ch("a"), ch("a"), Modifiers::empty(), Some("a"), mode),
"\x1b[97;1;97u"
);
let enter = || named(Named::Enter);
assert_eq!(
press(enter(), enter(), Modifiers::empty(), Some("\r"), mode),
"\x1b[13u"
);
// Touche de modification : son propre bit est posé à l'appui
let shift = || named(Named::Shift);
assert_eq!(
press(shift(), shift(), Modifiers::empty(), None, mode),
"\x1b[57441;2u"
);
}
#[test]
fn numpad_keys() {
let input = |key: &Key, text| {
String::from_utf8(encode(
&KeyInput {
key,
modified_key: key,
location: Location::Numpad,
modifiers: Modifiers::empty(),
text,
action: KeyAction::Press,
},
DISAMBIGUATE,
))
.unwrap()
};
// Un chiffre du pavé produit du texte : envoyé tel quel
assert_eq!(input(&ch("5"), Some("5")), "5");
// Entrée du pavé n'en produit pas d'imprimable : code dédié
assert_eq!(input(&named(Named::Enter), Some("\r")), "\x1b[57414u");
}
#[test]
fn only_with_disambiguate_or_all() {
assert!(!active(TermMode::empty()));
assert!(!active(TermMode::REPORT_EVENT_TYPES));
assert!(active(DISAMBIGUATE));
assert!(active(TermMode::REPORT_ALL_KEYS_AS_ESC));
}
/// Le terminal répond à `CSI ? u` et applique `CSI > 5 u` dès que
/// `kitty_keyboard` est actif : c'est ce que Claude Code interroge.
#[test]
fn term_reports_and_applies_kitty_modes() {
use alacritty_terminal::{
Term,
event::{Event, EventListener},
term::{Config, test::TermSize},
vte::ansi::Processor,
};
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
struct Replies(Arc<Mutex<Vec<String>>>);
impl EventListener for Replies {
fn send_event(&self, event: Event) {
if let Event::PtyWrite(text) = event {
self.0.lock().unwrap().push(text);
}
}
}
let replies = Replies::default();
let config = Config {
kitty_keyboard: true,
..Config::default()
};
let mut term = Term::new(config, &TermSize::new(80, 24), replies.clone());
let mut parser: Processor = Processor::new();
parser.advance(&mut term, b"\x1b[?u\x1b[>5u\x1b[?u");
assert_eq!(*replies.0.lock().unwrap(), ["\x1b[?0u", "\x1b[?5u"]);
let mode = *term.mode();
assert!(mode.contains(TermMode::DISAMBIGUATE_ESC_CODES));
assert!(mode.contains(TermMode::REPORT_ALTERNATE_KEYS));
assert!(active(mode));
parser.advance(&mut term, b"\x1b[<u");
assert!(!active(*term.mode()));
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,15 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::iced::Point;
use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::{Item as MenuItem, menu_button};
use cosmic::widget::{Column, space};
use cosmic::{
Element,
app::Core,
iced::core::Border,
iced::{Background, Length, advanced::widget::text::Style as TextStyle},
theme,
iced::{Background, Length},
widget::{
self, divider,
self,
menu::{ItemHeight, ItemWidth},
responsive_menu_bar, segmented_button,
},
@ -23,147 +21,94 @@ use crate::{Action, ColorSchemeId, ColorSchemeKind, Config, Message, fl, termina
static MENU_ID: LazyLock<cosmic::widget::Id> =
LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
/// What the terminal found under the right-click
#[derive(Debug, Clone)]
pub struct MenuState {
pub position: Option<Point>,
pub local_position: Option<Point>,
pub link: Option<String>,
}
pub fn context_menu<'a>(
/// A context menu action dispatched to the tab the menu was opened on.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TabAction(pub segmented_button::Entity, pub Action);
impl MenuAction for TabAction {
type Message = Message;
fn message(&self) -> Message {
Message::TabContextAction(self.0, self.1)
}
}
pub fn context_menu(
config: &Config,
key_binds: &HashMap<KeyBind, Action>,
entity: segmented_button::Entity,
link: Option<String>,
monitors: (bool, bool, bool),
) -> Element<'a, Message> {
let find_key = |action: &Action| -> String {
for (key_bind, key_action) in key_binds {
if action == key_action {
return key_bind.to_string();
}
}
String::new()
};
fn key_style(theme: &cosmic::Theme) -> TextStyle {
let mut color = theme.cosmic().background(false).component.on;
color.alpha *= 0.75;
TextStyle {
color: Some(color.into()),
..Default::default()
}
}
let menu_item = |label, action| {
let key = find_key(&action);
menu_button(vec![
widget::text(label).into(),
space::horizontal().into(),
widget::text(key)
.class(theme::Text::Custom(key_style))
.into(),
])
.on_press(Message::TabContextAction(entity, action))
read_only: bool,
) -> Vec<widget::menu::Tree<Message>> {
let item =
|label: String, action: Action| MenuItem::Button(label, None, TabAction(entity, action));
let check = |label: String, value: bool, action: Action| {
MenuItem::CheckBox(label, None, value, TabAction(entity, action))
};
let menu_checkbox = |label, value, action| {
menu_button(vec![
widget::text(label).into(),
widget::space::horizontal().into(),
widget::toggler(value)
.on_toggle(move |_| Message::TabContextAction(entity, action))
.size(16.0)
.into(),
])
.on_press(Message::TabContextAction(entity, action))
};
let mut rows = vec![
Element::from(menu_item(fl!("copy"), Action::Copy)),
Element::from(menu_item(fl!("paste"), Action::Paste)),
Element::from(menu_item(fl!("select-all"), Action::SelectAll)),
Element::from(divider::horizontal::light()),
Element::from(menu_item(fl!("clear-scrollback"), Action::ClearScrollback)),
Element::from(menu_item(fl!("save-output"), Action::SaveOutput)),
Element::from(divider::horizontal::light()),
Element::from(menu_checkbox(
let mut items = vec![
item(fl!("new-tab"), Action::TabNew),
item(fl!("rename-tab"), Action::TabRename),
MenuItem::Divider,
item(fl!("copy"), Action::Copy),
item(fl!("paste"), Action::Paste),
item(fl!("select-all"), Action::SelectAll),
MenuItem::Divider,
item(fl!("clear-scrollback"), Action::ClearScrollback),
item(fl!("save-output"), Action::SaveOutput),
MenuItem::Divider,
check(
fl!("monitor-activity"),
monitors.0,
Action::Monitor(MonitorKind::Activity),
)),
Element::from(menu_checkbox(
),
check(
fl!("monitor-silence"),
monitors.1,
Action::Monitor(MonitorKind::Silence),
)),
Element::from(menu_checkbox(
),
check(
fl!("monitor-process-finished"),
monitors.2,
Action::Monitor(MonitorKind::ProcessFinished),
)),
Element::from(divider::horizontal::light()),
Element::from(menu_item(
fl!("split-horizontal"),
Action::PaneSplitHorizontal,
)),
Element::from(menu_item(fl!("split-vertical"), Action::PaneSplitVertical)),
Element::from(menu_item(
fl!("pane-toggle-maximize"),
Action::PaneToggleMaximized,
)),
Element::from(divider::horizontal::light()),
Element::from(menu_item(fl!("new-tab"), Action::TabNew)),
Element::from(menu_item(fl!("rename-tab"), Action::TabRename)),
Element::from(menu_item(fl!("menu-settings"), Action::Settings)),
Element::from(menu_item(fl!("menu-about"), Action::About)),
),
check(fl!("read-only"), read_only, Action::ToggleReadOnly),
MenuItem::Divider,
item(fl!("split-horizontal"), Action::PaneSplitHorizontal),
item(fl!("split-vertical"), Action::PaneSplitVertical),
item(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized),
MenuItem::Divider,
MenuItem::CheckBox(
fl!("window-header"),
None,
config.show_headerbar,
TabAction(entity, Action::ShowHeaderBar(!config.show_headerbar)),
),
MenuItem::Divider,
];
#[cfg(feature = "password_manager")]
{
rows.push(Element::from(menu_item(
fl!("menu-password-manager"),
Action::PasswordManager,
)));
}
rows.push(Element::from(menu_checkbox(
fl!("show-headerbar"),
config.show_headerbar,
Action::ShowHeaderBar(!config.show_headerbar),
)));
//If we have a link
//prepend the Open Link item
items.push(item(fl!("menu-password-manager"), Action::PasswordManager));
items.push(item(fl!("menu-settings"), Action::Settings));
items.push(item(fl!("menu-about"), Action::About));
// If we have a link, prepend the link items
if link.is_some() {
rows.insert(
0,
Element::from(menu_item(fl!("open-link"), Action::LaunchUrlByMenu)),
);
rows.insert(
1,
Element::from(menu_item(fl!("copy-link"), Action::CopyUrlByMenu)),
);
rows.insert(2, Element::from(divider::horizontal::light()));
items.insert(0, item(fl!("open-link"), Action::LaunchUrlByMenu));
items.insert(1, item(fl!("copy-link"), Action::CopyUrlByMenu));
items.insert(2, MenuItem::Divider);
}
let content = Column::with_children(rows);
widget::container(content)
.padding(1)
//TODO: move style to libcosmic
.style(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(false).component;
widget::container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),
border: Border {
radius: cosmic.radius_s().map(|x| x + 1.0).into(),
width: 1.0,
color: component.divider.into(),
},
..Default::default()
}
})
.width(Length::Fixed(360.0))
.into()
let key_binds: HashMap<KeyBind, TabAction> = key_binds
.iter()
.map(|(key_bind, action)| (key_bind.clone(), TabAction(entity, *action)))
.collect();
widget::menu::items(&key_binds, items)
}
pub fn color_scheme_menu<'a>(
@ -174,7 +119,8 @@ pub fn color_scheme_menu<'a>(
let menu_item =
|label, message| menu_button(vec![widget::text(label).into()]).on_press(message);
let mut column = widget::column::with_capacity(if id_opt.is_some() { 3 } else { 1 });
let mut column =
widget::menu::menu_column::MenuColumn::with_capacity(if id_opt.is_some() { 3 } else { 1 });
if let Some(id) = id_opt {
column = column.push(menu_item(
fl!("rename"),
@ -197,7 +143,7 @@ pub fn color_scheme_menu<'a>(
//TODO: move style to libcosmic
.style(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(false).component;
let component = &cosmic.background(theme.transparent).component;
widget::container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
@ -219,6 +165,7 @@ pub fn menu_bar<'a>(
config: &Config,
key_binds: &HashMap<KeyBind, Action>,
monitors: (bool, bool, bool),
read_only: bool,
ssh_hosts: &[String],
) -> Element<'a, Message> {
let mut profile_items = Vec::with_capacity(config.profiles.len());
@ -290,6 +237,9 @@ pub fn menu_bar<'a>(
MenuItem::Button(fl!("next-tab"), None, Action::TabNext),
MenuItem::Button(fl!("previous-tab"), None, Action::TabPrev),
MenuItem::Divider,
MenuItem::Button(fl!("move-tab-left"), None, Action::TabMoveLeft),
MenuItem::Button(fl!("move-tab-right"), None, Action::TabMoveRight),
MenuItem::Divider,
MenuItem::Button(
fl!("split-horizontal"),
None,
@ -320,6 +270,12 @@ pub fn menu_bar<'a>(
monitors.2,
Action::Monitor(MonitorKind::ProcessFinished),
),
MenuItem::CheckBox(
fl!("read-only"),
None,
read_only,
Action::ToggleReadOnly,
),
MenuItem::Divider,
MenuItem::Button(
fl!("menu-color-schemes"),

407
src/osc_tap.rs Normal file
View file

@ -0,0 +1,407 @@
// SPDX-License-Identifier: GPL-3.0-only
//! Séquences OSC que vte ignore, interceptées à la sortie du PTY (yoda).
//!
//! alacritty_terminal 0.25 / vte 0.15 jettent les OSC 9 et 777 (« unhandled
//! osc_dispatch ») : l'application ne les voit jamais. On les repère donc dans
//! les octets lus sur le PTY, juste avant que vte ne les analyse, en
//! enveloppant le `Pty` : ni alacritty_terminal ni vte ne sont forkés.
//!
//! Formes reconnues, comme dans Ghostty :
//! - `OSC 777 ; notify ; titre ; corps` (forme rxvt, canal « ghostty » de
//! Claude Code) ;
//! - `OSC 9 ; texte` (forme iTerm2), sauf les commandes ConEmu
//! `OSC 9 ; N [; …]` (barre de progression `9;4` comprise), qui ne sont pas
//! des notifications.
/// Taille maximale retenue pour le contenu d'une séquence OSC ; au-delà, la
/// séquence est ignorée jusqu'à son terminateur.
const OSC_MAX_LEN: usize = 4096;
/// Longueur maximale (en caractères) du titre et du corps d'une notification.
const TITLE_MAX_CHARS: usize = 200;
const BODY_MAX_CHARS: usize = 1000;
/// Événement extrait d'une séquence OSC.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OscEvent {
/// Notification de bureau demandée par le programme (OSC 9 ou 777).
Notify { title: Option<String>, body: String },
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum State {
#[default]
Ground,
Escape,
Osc,
}
/// Automate minimal qui suit le flux d'octets du PTY et ne retient que le
/// contenu des séquences OSC. Les séquences peuvent être coupées entre deux
/// lectures : l'état est conservé d'un appel à l'autre.
#[derive(Debug, Default)]
pub struct OscScanner {
state: State,
buf: Vec<u8>,
overflow: bool,
}
impl OscScanner {
pub fn feed(&mut self, bytes: &[u8], emit: &mut dyn FnMut(OscEvent)) {
for &byte in bytes {
match self.state {
State::Ground => {
if byte == 0x1b {
self.state = State::Escape;
}
}
State::Escape => {
self.state = match byte {
b']' => {
self.buf.clear();
self.overflow = false;
State::Osc
}
0x1b => State::Escape,
_ => State::Ground,
};
}
State::Osc => match byte {
// BEL termine la séquence.
0x07 => {
self.dispatch(emit);
self.state = State::Ground;
}
// ESC termine aussi la séquence, comme dans vte : c'est le
// début de ST (`ESC \`) ou d'une nouvelle séquence.
0x1b => {
self.dispatch(emit);
self.state = State::Escape;
}
// CAN et SUB annulent la séquence.
0x18 | 0x1a => self.state = State::Ground,
_ => {
if self.buf.len() < OSC_MAX_LEN {
self.buf.push(byte);
} else {
self.overflow = true;
}
}
},
}
}
}
fn dispatch(&mut self, emit: &mut dyn FnMut(OscEvent)) {
if !self.overflow
&& let Some(event) = parse_osc(&self.buf)
{
emit(event);
}
self.buf.clear();
self.overflow = false;
}
}
/// Interprète le contenu d'une séquence OSC (entre `ESC ]` et le terminateur).
pub fn parse_osc(payload: &[u8]) -> Option<OscEvent> {
if let Some(rest) = payload.strip_prefix(b"777;notify;") {
match rest.iter().position(|&b| b == b';') {
Some(i) => notify(&rest[..i], &rest[i + 1..]),
None => notify(b"", rest),
}
} else if let Some(rest) = payload.strip_prefix(b"9;") {
if is_conemu_command(rest) {
None
} else {
notify(b"", rest)
}
} else {
None
}
}
/// `OSC 9 ; N` ou `OSC 9 ; N ; …` avec N numérique : commande ConEmu
/// (progression, titre d'onglet, répertoire…), pas une notification.
fn is_conemu_command(rest: &[u8]) -> bool {
let digits = rest.iter().take_while(|b| b.is_ascii_digit()).count();
digits > 0 && matches!(rest.get(digits), None | Some(b';'))
}
fn notify(title: &[u8], body: &[u8]) -> Option<OscEvent> {
let title = clean(title, TITLE_MAX_CHARS);
let body = clean(body, BODY_MAX_CHARS);
match (title.is_empty(), body.is_empty()) {
(true, true) => None,
// Un titre seul devient le corps.
(false, true) => Some(OscEvent::Notify {
title: None,
body: title,
}),
(true, false) => Some(OscEvent::Notify { title: None, body }),
(false, false) => Some(OscEvent::Notify {
title: Some(title),
body,
}),
}
}
/// UTF-8 tolérant, caractères de contrôle remplacés par des espaces, longueur
/// bornée.
fn clean(bytes: &[u8], max_chars: usize) -> String {
String::from_utf8_lossy(bytes)
.chars()
.map(|c| if c.is_control() { ' ' } else { c })
.take(max_chars)
.collect::<String>()
.trim()
.to_string()
}
#[cfg(unix)]
pub use self::unix::OscTapPty;
#[cfg(unix)]
mod unix {
use super::{OscEvent, OscScanner};
use alacritty_terminal::{
event::{OnResize, WindowSize},
tty::{ChildEvent, EventedPty, EventedReadWrite, Pty},
};
use polling::{Event, PollMode, Poller};
use std::{
fs::File,
io::{self, Read},
sync::Arc,
};
/// Lecteur du PTY qui passe chaque lecture au [`OscScanner`] avant de la
/// rendre à la boucle d'alacritty_terminal.
pub struct TapReader {
file: File,
scanner: OscScanner,
sink: Box<dyn FnMut(OscEvent) + Send>,
}
impl Read for TapReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let len = self.file.read(buf)?;
self.scanner.feed(&buf[..len], &mut *self.sink);
Ok(len)
}
}
/// `Pty` d'alacritty_terminal dont la lecture est observée par un
/// [`TapReader`]. Le lecteur lit un duplicata du descripteur maître (même
/// description de fichier, donc même mode non bloquant) ; l'enregistrement
/// auprès du `Poller`, l'écriture, le redimensionnement et la fin du
/// processus restent ceux du `Pty`.
pub struct OscTapPty {
// Déclaré avant `pty` : le duplicata est fermé avant le maître.
reader: TapReader,
pty: Pty,
}
impl OscTapPty {
pub fn new(pty: Pty, sink: Box<dyn FnMut(OscEvent) + Send>) -> io::Result<Self> {
let file = pty.file().try_clone()?;
Ok(Self {
reader: TapReader {
file,
scanner: OscScanner::default(),
sink,
},
pty,
})
}
}
impl EventedReadWrite for OscTapPty {
type Reader = TapReader;
type Writer = File;
unsafe fn register(
&mut self,
poll: &Arc<Poller>,
interest: Event,
mode: PollMode,
) -> io::Result<()> {
unsafe { self.pty.register(poll, interest, mode) }
}
fn reregister(
&mut self,
poll: &Arc<Poller>,
interest: Event,
mode: PollMode,
) -> io::Result<()> {
self.pty.reregister(poll, interest, mode)
}
fn deregister(&mut self, poll: &Arc<Poller>) -> io::Result<()> {
self.pty.deregister(poll)
}
fn reader(&mut self) -> &mut TapReader {
&mut self.reader
}
fn writer(&mut self) -> &mut File {
self.pty.writer()
}
}
impl EventedPty for OscTapPty {
fn next_child_event(&mut self) -> Option<ChildEvent> {
self.pty.next_child_event()
}
}
impl OnResize for OscTapPty {
fn on_resize(&mut self, window_size: WindowSize) {
self.pty.on_resize(window_size);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scan(chunks: &[&[u8]]) -> Vec<OscEvent> {
let mut scanner = OscScanner::default();
let mut events = Vec::new();
for chunk in chunks {
scanner.feed(chunk, &mut |event| events.push(event));
}
events
}
fn notify(title: Option<&str>, body: &str) -> OscEvent {
OscEvent::Notify {
title: title.map(str::to_string),
body: body.to_string(),
}
}
#[test]
fn osc_777_with_bel_and_st() {
assert_eq!(
scan(&[b"x\x1b]777;notify;Claude Code;En attente\x07y"]),
[notify(Some("Claude Code"), "En attente")]
);
assert_eq!(
scan(&[b"\x1b]777;notify;T;corps; avec point-virgule\x1b\\"]),
[notify(Some("T"), "corps; avec point-virgule")]
);
}
#[test]
fn osc_9_text_but_not_conemu() {
assert_eq!(
scan(&[b"\x1b]9;Claude Code: termine\x07"]),
[notify(None, "Claude Code: termine")]
);
// Commandes ConEmu : progression, avec ou sans argument.
assert!(scan(&[b"\x1b]9;4;1;50\x07\x1b]9;4;0;\x07\x1b]9;12\x07"]).is_empty());
// Un chiffre suivi d'autre chose qu'un `;` reste du texte.
assert_eq!(
scan(&[b"\x1b]9;3 tests en echec\x07"]),
[notify(None, "3 tests en echec")]
);
}
#[test]
fn sequence_split_across_reads() {
assert_eq!(
scan(&[b"\x1b", b"]777;no", b"tify;Ti", b"tre;Co", b"rps\x1b", b"\\"]),
[notify(Some("Titre"), "Corps")]
);
}
#[test]
fn other_osc_and_noise_are_ignored() {
assert!(scan(&[b"\x1b]0;titre\x07\x1b]8;;https://x\x1b\\lien\x1b]8;;\x1b\\"]).is_empty());
assert!(scan(&[b"\x1b]777;notify;;\x07\x1b]9;\x07"]).is_empty());
// CAN annule la séquence en cours.
assert!(scan(&[b"\x1b]9;annulee\x18\x07"]).is_empty());
}
#[test]
fn control_chars_cleaned_and_title_alone_becomes_body() {
assert_eq!(
scan(&[b"\x1b]777;notify;Seul\x07"]),
[notify(None, "Seul")]
);
assert_eq!(
scan(&[b"\x1b]9;a\tb\x01c\x07"]),
[notify(None, "a b c")]
);
}
/// De bout en bout : un vrai processus écrit dans un vrai PTY, lu par la
/// boucle d'alacritty_terminal à travers l'enveloppe.
#[cfg(unix)]
#[test]
fn tap_sees_osc_written_by_child() {
use alacritty_terminal::{
Term,
event::{Event, EventListener, WindowSize},
event_loop::EventLoop,
sync::FairMutex,
term::{Config, test::TermSize},
tty::{self, Options, Shell},
};
use std::{sync::Arc, sync::mpsc, time::Duration};
#[derive(Clone)]
struct Listener;
impl EventListener for Listener {
fn send_event(&self, _event: Event) {}
}
let options = Options {
shell: Some(Shell::new(
"printf".into(),
vec!["avant\x1b]777;notify;Titre;Corps\x07apres".into()],
)),
drain_on_exit: true,
..Default::default()
};
let window_size = WindowSize {
num_lines: 24,
num_cols: 80,
cell_width: 8,
cell_height: 16,
};
let pty = tty::new(&options, window_size, 0).unwrap();
let (tx, rx) = mpsc::channel();
let pty = OscTapPty::new(
pty,
Box::new(move |event| {
let _ = tx.send(event);
}),
)
.unwrap();
let term = Arc::new(FairMutex::new(Term::new(
Config::default(),
&TermSize::new(80, 24),
Listener,
)));
let event_loop = EventLoop::new(term, Listener, pty, true, false).unwrap();
let _join = event_loop.spawn();
assert_eq!(
rx.recv_timeout(Duration::from_secs(5)).unwrap(),
notify(Some("Titre"), "Corps")
);
}
#[test]
fn oversized_sequence_is_dropped() {
let mut long = b"\x1b]9;".to_vec();
long.extend(std::iter::repeat_n(b'a', OSC_MAX_LEN + 10));
long.extend(b"\x07\x1b]9;ensuite\x07");
assert_eq!(scan(&[&long]), [notify(None, "ensuite")]);
}
}

View file

@ -89,7 +89,10 @@ pub enum KeyBindAction {
TabNext,
TabPrev,
TabRename,
TabMoveLeft,
TabMoveRight,
ToggleFullscreen,
ToggleReadOnly,
WindowClose,
WindowNew,
ZoomIn,
@ -131,7 +134,10 @@ impl KeyBindAction {
Self::TabNext => Some(Action::TabNext),
Self::TabPrev => Some(Action::TabPrev),
Self::TabRename => Some(Action::TabRename),
Self::TabMoveLeft => Some(Action::TabMoveLeft),
Self::TabMoveRight => Some(Action::TabMoveRight),
Self::ToggleFullscreen => Some(Action::ToggleFullscreen),
Self::ToggleReadOnly => Some(Action::ToggleReadOnly),
Self::WindowClose => Some(Action::WindowClose),
Self::WindowNew => Some(Action::WindowNew),
Self::ZoomIn => Some(Action::ZoomIn),
@ -294,7 +300,10 @@ pub fn action_label(action: KeyBindAction) -> String {
KeyBindAction::TabNext => fl!("next-tab"),
KeyBindAction::TabPrev => fl!("previous-tab"),
KeyBindAction::TabRename => fl!("rename-tab"),
KeyBindAction::TabMoveLeft => fl!("move-tab-left"),
KeyBindAction::TabMoveRight => fl!("move-tab-right"),
KeyBindAction::ToggleFullscreen => fl!("toggle-fullscreen"),
KeyBindAction::ToggleReadOnly => fl!("read-only"),
KeyBindAction::WindowClose => fl!("close-window"),
KeyBindAction::WindowNew => fl!("new-window"),
KeyBindAction::ZoomIn => fl!("zoom-in"),
@ -329,6 +338,8 @@ pub fn shortcut_groups() -> Vec<ShortcutGroup> {
KeyBindAction::TabRename,
KeyBindAction::TabNext,
KeyBindAction::TabPrev,
KeyBindAction::TabMoveLeft,
KeyBindAction::TabMoveRight,
KeyBindAction::TabActivate0,
KeyBindAction::TabActivate1,
KeyBindAction::TabActivate2,
@ -369,7 +380,11 @@ pub fn shortcut_groups() -> Vec<ShortcutGroup> {
KeyBindAction::ZoomReset,
],
});
let mut other_actions = vec![KeyBindAction::ClearScrollback, KeyBindAction::SaveOutput];
let mut other_actions = vec![
KeyBindAction::ClearScrollback,
KeyBindAction::SaveOutput,
KeyBindAction::ToggleReadOnly,
];
#[cfg(feature = "password_manager")]
other_actions.push(KeyBindAction::PasswordManager);
groups.push(ShortcutGroup {
@ -479,6 +494,10 @@ fn fallback_shortcuts() -> Shortcuts {
bind!([Ctrl], "Tab", TabNext);
bind!([Ctrl, Shift], "Tab", TabPrev);
// Ctrl+Shift+PageUp/Down move tabs left and right
bind!([Ctrl, Shift], "PageUp", TabMoveLeft);
bind!([Ctrl, Shift], "PageDown", TabMoveRight);
// Ctrl+Shift+# activates tabs by index
bind!([Ctrl, Shift], "1", TabActivate0);
bind!([Ctrl, Shift], "2", TabActivate1);

View file

@ -27,13 +27,15 @@ use indexmap::IndexSet;
use std::{
borrow::Cow,
collections::HashMap,
fs, io, mem,
fs,
hash::{DefaultHasher, Hash, Hasher},
io, mem,
path::PathBuf,
sync::{
Arc, Mutex, Weak,
atomic::{AtomicU32, Ordering},
},
time::Instant,
time::{Duration, Instant},
};
use tokio::sync::mpsc;
@ -43,6 +45,7 @@ use crate::{
config::{ColorSchemeKind, Config as AppConfig, ProfileId},
menu::MenuState,
mouse_reporter::MouseReporter,
osc_tap::OscEvent,
};
/// Minimum contrast between a fixed cursor color and the cell's background.
@ -65,6 +68,28 @@ pub enum MonitorKind {
/// Seconds without output before a silence alert fires (Konsole default).
pub const MONITOR_SILENCE_SECONDS: u64 = 10;
/// Appel d'attention d'un onglet en arrière-plan (yoda).
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AttentionKind {
/// Cloche (BEL).
Bell,
/// Notification de bureau (OSC 9 / 777).
Notification,
}
/// Anti-rafale des notifications d'un onglet, aux valeurs de Ghostty : au plus
/// une par seconde, et une notification identique au plus toutes les 5 s.
const NOTIFY_MIN_INTERVAL: Duration = Duration::from_secs(1);
const NOTIFY_SAME_INTERVAL: Duration = Duration::from_secs(5);
/// Événement remonté d'un terminal : celui d'alacritty_terminal, ou une
/// séquence OSC interceptée à la sortie du PTY (voir [`crate::osc_tap`]).
#[derive(Debug)]
pub enum PtyEvent {
Term(Event),
Osc(OscEvent),
}
/// https://github.com/alacritty/alacritty/blob/4a7728bf7fac06a35f27f6c4f31e0d9214e5152b/alacritty/src/config/ui_config.rs#L36-L39
fn url_regex_search() -> RegexSearch {
let url_regex = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)\
@ -109,13 +134,13 @@ impl From<Size> for WindowSize {
pub struct EventProxy(
pane_grid::Pane,
segmented_button::Entity,
mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, Event)>,
mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>,
);
impl EventListener for EventProxy {
fn send_event(&self, event: Event) {
//TODO: handle error
let _ = self.2.send((self.0, self.1, event));
let _ = self.2.send((self.0, self.1, PtyEvent::Term(event)));
}
}
@ -196,34 +221,63 @@ impl TerminalPaneGrid {
self.focus
}
pub fn update_terminal_focus(&self) {
for (pane, tab_model) in self.panes.panes.iter() {
let entity = tab_model.active();
pub fn update_terminal_focus(&mut self) {
let focus = self.focus;
for (pane, tab_model) in self.panes.panes.iter_mut() {
let active = tab_model.active();
let entities: Vec<segmented_button::Entity> = tab_model.iter().collect();
for entity in entities {
let mut icon_update = None;
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.set_focused(self.focus == *pane);
if entity == active {
let focused = focus == *pane;
terminal.set_focused(focused);
terminal.update();
// Regarder l'onglet acquitte son appel d'attention
if focused && terminal.attention.take().is_some() {
icon_update = Some(crate::monitor_icon_name(&terminal));
}
} else {
// Onglet en arrière-plan : il perd le focus (rapport
// 1004), sinon un agent qui s'y trouve croit encore
// qu'on le regarde.
terminal.set_focused(false);
}
}
if let Some(icon_name) = icon_update {
crate::monitor_update_tab_icon(tab_model, entity, icon_name);
}
}
}
}
pub fn unfocus_all_terminals(&self) {
for (_pane, tab_model) in self.panes.panes.iter() {
for tab_model in self.panes.panes.values() {
let active = tab_model.active();
for entity in tab_model.iter() {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.set_focused(false);
if entity == active {
terminal.update();
}
}
}
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Metadata {
pub bg: cosmic_text::Color,
pub underline_color: cosmic_text::Color,
pub flags: Flags,
/// Remplissage natif (élément de bloc Unicode) — voir [`BlockFill`].
pub block: Option<BlockFill>,
/// Couleur du remplissage natif. Hors bloc, sans effet ; participant à
/// l'égalité pour que deux blocs adjacents de couleurs différentes ne
/// fusionnent pas en un seul quad.
pub block_color: cosmic_text::Color,
}
impl Metadata {
@ -233,6 +287,8 @@ impl Metadata {
bg,
underline_color,
flags,
block: None,
block_color: underline_color,
}
}
@ -246,6 +302,73 @@ impl Metadata {
fn with_flags(self, flags: Flags) -> Self {
Self { flags, ..self }
}
fn with_block(mut self, block: Option<BlockFill>, color: cosmic_text::Color) -> Self {
self.block = block;
self.block_color = color;
self
}
}
/// Élément de bloc Unicode (U+2580–U+259F) rendu nativement.
///
/// Ces caractères ne passent PAS par le glyphe de la police : le `█` d'une
/// police couvre à peine l'em (≈ 1× la taille de police) alors que la cellule
/// fait `line_height` (≈ 1,4×) — d'où des trous entre les rangées et un rendu
/// crénelé. À la place, chaque bloc est remplacé par une espace (le glyphe
/// disparaît) et peint en quad GPU couvrant exactement la fraction voulue de
/// la cellule : bords alignés sur la grille, jointif par construction, net à
/// toute échelle. C'est l'approche de kitty/WezTerm.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlockFill {
/// Un ou deux rectangles en huitièmes de cellule : `(x0, x1, y0, y1)`,
/// abscisse et ordonnée depuis le coin haut-gauche. Un rectangle vide
/// (`x0 >= x1` ou `y0 >= y1`) est ignoré au tracé — deux rectangles
/// suffisent pour toute la famille des quadrants.
Rects([(u8, u8, u8, u8); 2]),
/// Trame (`░▒▓`) : rectangle plein, alpha en quarts (1–3).
Shade(u8),
}
/// Mappe un caractère vers son remplissage natif, s'il est un élément de bloc.
pub fn block_fill(c: char) -> Option<BlockFill> {
use BlockFill::{Rects, Shade};
const NONE: (u8, u8, u8, u8) = (8, 8, 8, 8);
match c {
'▀' => Some(Rects([(0, 8, 0, 4), NONE])),
'▁' => Some(Rects([(0, 8, 7, 8), NONE])),
'▂' => Some(Rects([(0, 8, 6, 8), NONE])),
'▃' => Some(Rects([(0, 8, 5, 8), NONE])),
'▄' => Some(Rects([(0, 8, 4, 8), NONE])),
'▅' => Some(Rects([(0, 8, 3, 8), NONE])),
'▆' => Some(Rects([(0, 8, 2, 8), NONE])),
'▇' => Some(Rects([(0, 8, 1, 8), NONE])),
'█' => Some(Rects([(0, 8, 0, 8), NONE])),
'▉' => Some(Rects([(0, 7, 0, 8), NONE])),
'▊' => Some(Rects([(0, 6, 0, 8), NONE])),
'▋' => Some(Rects([(0, 5, 0, 8), NONE])),
'▌' => Some(Rects([(0, 4, 0, 8), NONE])),
'▍' => Some(Rects([(0, 3, 0, 8), NONE])),
'▎' => Some(Rects([(0, 2, 0, 8), NONE])),
'▏' => Some(Rects([(0, 1, 0, 8), NONE])),
'▐' => Some(Rects([(4, 8, 0, 8), NONE])),
'░' => Some(Shade(1)),
'▒' => Some(Shade(2)),
'▓' => Some(Shade(3)),
'▔' => Some(Rects([(0, 8, 0, 1), NONE])),
'▕' => Some(Rects([(7, 8, 0, 8), NONE])),
'▖' => Some(Rects([(0, 4, 4, 8), NONE])),
'▗' => Some(Rects([(4, 8, 4, 8), NONE])),
'▘' => Some(Rects([(0, 4, 0, 4), NONE])),
'▝' => Some(Rects([(4, 8, 0, 4), NONE])),
'▙' => Some(Rects([(0, 4, 0, 8), (4, 8, 4, 8)])),
'▚' => Some(Rects([(0, 4, 0, 4), (4, 8, 4, 8)])),
'▛' => Some(Rects([(0, 8, 0, 4), (0, 4, 4, 8)])),
'▜' => Some(Rects([(0, 8, 0, 4), (4, 8, 4, 8)])),
'▞' => Some(Rects([(4, 8, 0, 4), (0, 4, 4, 8)])),
'▟' => Some(Rects([(0, 8, 4, 8), (4, 8, 0, 4)])),
_ => None,
}
}
pub struct Terminal {
@ -257,6 +380,11 @@ pub struct Terminal {
pub monitor_alert: Option<MonitorKind>,
pub monitor_last_output: Instant,
pub monitor_job_running: bool,
pub attention: Option<AttentionKind>,
notify_last: Option<(Instant, u64)>,
/// [yoda] Lecture seule : aucune saisie de l'utilisateur n'atteint le PTY
/// (touches, collage, souris) ; les réponses du terminal passent toujours.
pub read_only: bool,
pub needs_update: bool,
pub profile_id_opt: Option<ProfileId>,
pub tab_title_override: Option<String>,
@ -287,7 +415,7 @@ impl Terminal {
pub fn new(
pane: pane_grid::Pane,
entity: segmented_button::Entity,
event_tx: mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, Event)>,
event_tx: mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>,
config: Config,
options: Options,
app_config: &AppConfig,
@ -339,6 +467,8 @@ impl Terminal {
cell_width,
cell_height,
};
#[cfg(unix)]
let osc_tx = event_tx.clone();
let event_proxy = EventProxy(pane, entity, event_tx);
let term = Arc::new(FairMutex::new(Term::new(
config,
@ -353,6 +483,15 @@ impl Terminal {
#[cfg(windows)]
let shell_pid = pty.child_watcher().pid().map(|pid| pid.get());
// OSC 9 / 777 : interceptés avant vte, qui les ignore
#[cfg(unix)]
let pty = crate::osc_tap::OscTapPty::new(
pty,
Box::new(move |event| {
let _ = osc_tx.send((pane, entity, PtyEvent::Osc(event)));
}),
)?;
let pty_event_loop =
EventLoop::new(term.clone(), event_proxy, pty, options.drain_on_exit, false)?;
let notifier = Notifier(pty_event_loop.channel());
@ -376,6 +515,9 @@ impl Terminal {
monitor_alert: None,
monitor_last_output: Instant::now(),
monitor_job_running: false,
attention: None,
notify_last: None,
read_only: false,
mouse_reporter: Default::default(),
needs_update: true,
notifier,
@ -439,6 +581,25 @@ impl Terminal {
self.zoom_adj = value;
}
/// Anti-rafale des notifications de bureau de cet onglet ; `true` si
/// celle-ci peut partir.
pub fn notify_allowed(&mut self, title: Option<&str>, body: &str) -> bool {
let mut hasher = DefaultHasher::new();
(title, body).hash(&mut hasher);
let hash = hasher.finish();
let now = Instant::now();
if let Some((last, last_hash)) = self.notify_last {
let elapsed = now.duration_since(last);
if elapsed < NOTIFY_MIN_INTERVAL
|| (hash == last_hash && elapsed < NOTIFY_SAME_INTERVAL)
{
return false;
}
}
self.notify_last = Some((now, hash));
true
}
fn set_focused(&mut self, is_focused: bool) {
let focus_changed = self.is_focused != is_focused;
self.is_focused = is_focused;
@ -450,7 +611,7 @@ impl Terminal {
const FOCUS_OUT: &[u8] = b"\x1b[O";
let input = if is_focused { FOCUS_IN } else { FOCUS_OUT };
self.input_no_scroll(input);
self.respond(input);
}
}
}
@ -463,7 +624,18 @@ impl Terminal {
self.with_buffer_mut(|buffer| buffer.set_redraw(redraw));
}
/// Saisie de l'utilisateur vers le PTY ; ignorée en lecture seule.
pub fn input_no_scroll<I: Into<Cow<'static, [u8]>>>(&self, input: I) {
if self.read_only {
return;
}
self.notifier.notify(input);
}
/// [yoda] Réponse du terminal lui-même (rapports demandés par
/// l'application, focus) : jamais bloquée par la lecture seule, sinon
/// l'application attendrait une réponse qui ne vient pas.
pub fn respond<I: Into<Cow<'static, [u8]>>>(&self, input: I) {
self.notifier.notify(input);
}
@ -481,6 +653,10 @@ impl Terminal {
}
pub fn input_scroll<I: Into<Cow<'static, [u8]>>>(&self, input: I) {
// En lecture seule, une frappe ne ramène pas non plus la vue en bas
if self.read_only {
return;
}
self.input_no_scroll(input);
self.scroll(TerminalScroll::Bottom);
}
@ -906,9 +1082,14 @@ impl Terminal {
}
let start = text.len();
// Tab skip/stop is handled by alacritty_terminal
// Tab skip/stop is handled by alacritty_terminal.
// Les éléments de bloc ne deviennent pas des glyphes : la
// police ne couvre pas la cellule (trous d'interligne) ;
// ils sont peints en quads natifs au tracé, via Metadata.
let block = block_fill(indexed.cell.c);
text.push(match indexed.cell.c {
'\t' => ' ',
c if block.is_some() => ' ',
c => c,
});
if let Some(zerowidth) = indexed.cell.zerowidth() {
@ -1026,6 +1207,7 @@ impl Terminal {
}
let metadata = Metadata::new(bg, fg)
.with_block(block, fg)
.with_flags(flags)
.with_underline_color(underline_color);
let (meta_idx, _) = self.metadata_set.insert_full(metadata);
@ -1136,9 +1318,12 @@ impl Terminal {
y,
);
// Saisie de l'utilisateur : rien en lecture seule
if !self.read_only {
for code in codes {
self.notifier.notify(code);
}
}
} else {
self.scroll_as_arrows(delta);
}
@ -1345,3 +1530,108 @@ impl Drop for Terminal {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Lecture seule, de bout en bout : un vrai `cat` dans un vrai PTY. La
/// saisie bloquée n'arrive pas ; une réponse du terminal, si ; la saisie
/// repasse quand le verrou est levé.
#[cfg(unix)]
#[test]
fn read_only_blocks_input_but_not_replies() {
use std::time::Duration;
let (_panes, pane) = pane_grid::State::new(());
let mut model = segmented_button::Model::<segmented_button::SingleSelect>::default();
let entity = model.insert().id();
let (event_tx, _event_rx) = mpsc::unbounded_channel();
let options = Options {
shell: Some(tty::Shell::new("cat".into(), Vec::new())),
..Options::default()
};
let mut terminal = Terminal::new(
pane,
entity,
event_tx,
Config::default(),
options,
&AppConfig::default(),
crate::terminal_theme::cosmic_dark(),
None,
None,
)
.unwrap();
let wait_for = |terminal: &Terminal, needle: &str| {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if terminal.scrollback_text().contains(needle) {
return true;
}
std::thread::sleep(Duration::from_millis(20));
}
false
};
terminal.read_only = true;
terminal.input_scroll(&b"bloque\n"[..]);
terminal.paste("colle\n".to_string());
terminal.respond(&b"repond\n"[..]);
assert!(wait_for(&terminal, "repond"));
// Écrites avant la réponse, la saisie et le collage seraient déjà là
let text = terminal.scrollback_text();
assert!(!text.contains("bloque"), "{text}");
assert!(!text.contains("colle"), "{text}");
terminal.read_only = false;
terminal.input_scroll(&b"libre\n"[..]);
assert!(wait_for(&terminal, "libre"));
}
#[test]
fn block_fill_couvre_toute_la_plage_unicode() {
// U+2580–U+259F : chaque élément de bloc a sa géométrie native.
for code in 0x2580..=0x259F {
let c = char::from_u32(code).unwrap();
assert!(
block_fill(c).is_some(),
"{c} (U+{code:04X}) sans remplissage"
);
}
// Hors de la plage : filets, espaces, lettres — pas de remplissage.
for c in [' ', '─', '│', 'M', '●', '\u{25A0}'] {
assert!(
block_fill(c).is_none(),
"{c} ne doit pas être un bloc natif"
);
}
}
#[test]
fn block_fill_geometry_toujours_valide() {
// Fractions croissantes, bornées à la cellule — sinon le quad
// déborderait sur la cellule voisine ou serait rejeté au tracé.
for c in (char::from_u32(0x2580).unwrap()..=char::from_u32(0x259F).unwrap())
.chain(['░', '▒', '▓'].iter().copied())
{
let Some(fill) = block_fill(c) else { continue };
match fill {
BlockFill::Rects(rects) => {
for (x0, x1, y0, y1) in rects {
// Rectangle vide (sentinelle) ou borné à la cellule.
let empty = x0 >= x1 || y0 >= y1;
assert!(
empty || (x1 <= 8 && y1 <= 8),
"{c}: géométrie déborde la cellule"
);
}
}
BlockFill::Shade(quarters) => {
assert!((1..=3).contains(&quarters), "{c}: alpha de trame invalide")
}
}
}
}
}

View file

@ -12,6 +12,7 @@ use cosmic::{
cosmic_theme::palette::{WithAlpha, blend::Compose},
iced::core::{
Border, Shell,
border::Radius,
clipboard::Clipboard,
input_method::{self, InputMethod},
keyboard::key::Named,
@ -45,7 +46,11 @@ use std::{
time::{Duration, Instant},
};
use crate::{Action, Terminal, TerminalScroll, menu::MenuState, terminal::Metadata};
use crate::{
Action, Terminal, TerminalScroll, kitty_keyboard,
menu::MenuState,
terminal::{BlockFill, Metadata},
};
const AUTOSCROLL_INTERVAL: Duration = Duration::from_millis(100);
@ -109,13 +114,16 @@ pub struct TerminalBox<'a, Message> {
border: Border,
padding: Padding,
show_headerbar: bool,
pane_border_radius: Option<Radius>,
click_timing: Duration,
context_menu: Option<Point>,
context_menu_open: bool,
on_context_menu: Option<Box<dyn Fn(Option<MenuState>) -> Message + 'a>>,
on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>,
opacity: Option<f32>,
dim: Option<f32>,
mouse_inside_boundary: Option<bool>,
on_middle_click: Option<Box<dyn Fn() -> Message + 'a>>,
on_selection_done: Option<Box<dyn Fn() -> Message + 'a>>,
on_open_hyperlink: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_window_focused: Option<Box<dyn Fn() -> Message + 'a>>,
on_window_unfocused: Option<Box<dyn Fn() -> Message + 'a>>,
@ -135,13 +143,16 @@ where
border: Border::default(),
padding: Padding::new(0.0),
show_headerbar: true,
pane_border_radius: None,
click_timing: Duration::from_millis(500),
context_menu: None,
context_menu_open: false,
on_context_menu: None,
on_mouse_enter: None,
opacity: None,
dim: None,
mouse_inside_boundary: None,
on_middle_click: None,
on_selection_done: None,
key_binds,
on_open_hyperlink: None,
on_window_focused: None,
@ -171,13 +182,18 @@ where
self
}
pub fn pane_border_radius(mut self, pane_border_radius: Option<Radius>) -> Self {
self.pane_border_radius = pane_border_radius;
self
}
pub fn click_timing(mut self, click_timing: Duration) -> Self {
self.click_timing = click_timing;
self
}
pub fn context_menu(mut self, position: Point) -> Self {
self.context_menu = Some(position);
pub fn context_menu_open(mut self, open: bool) -> Self {
self.context_menu_open = open;
self
}
@ -194,6 +210,14 @@ where
self
}
/// [yoda] Fin d'un geste de sélection fait dans ce panneau (glisser, double
/// ou triple clic, Shift+clic) : le moment de copier la sélection. Un
/// simple clic ailleurs dans la fenêtre ne le déclenche pas.
pub fn on_selection_done(mut self, on_selection_done: impl Fn() -> Message + 'a) -> Self {
self.on_selection_done = Some(Box::new(on_selection_done));
self
}
pub fn on_middle_click(mut self, on_middle_click: impl Fn() -> Message + 'a) -> Self {
self.on_middle_click = Some(Box::new(on_middle_click));
self
@ -204,6 +228,13 @@ where
self
}
/// [yoda] Voile de la couleur du fond posé sur tout le panneau (texte
/// compris), d'opacité `amount` : panneau non focalisé.
pub fn dim(mut self, amount: f32) -> Self {
self.dim = Some(amount);
self
}
pub fn sharp_corners(mut self, sharp_corners: bool) -> Self {
self.sharp_corners = sharp_corners;
self
@ -241,7 +272,7 @@ where
if !state.is_focused {
return InputMethod::Disabled;
}
if self.context_menu.is_some() {
if self.context_menu_open {
return InputMethod::Disabled;
}
@ -409,13 +440,22 @@ where
let state = tree.state.downcast_ref::<State>();
let cosmic_theme = theme.cosmic();
let corner_radius = if let Some(r) = self.pane_border_radius {
r.into()
} else if self.sharp_corners {
// matches the corners to the window border
let corner_radius = if self.sharp_corners {
cosmic_theme.radius_0()
} else {
cosmic_theme.radius_s()
}
.map(|x| if x < 4.0 { x - 1.0 } else { x + 3.0 });
cosmic_theme
.radius_s()
.map(|x| if x < 4.0 { x - 1.0 } else { x + 3.0 })
};
// When there's a headerbar and no pane border, only round the bottom corners
let border_radius: Radius = if self.show_headerbar && self.pane_border_radius.is_none() {
[0.0, 0.0, corner_radius[2], corner_radius[3]].into()
} else {
corner_radius.into()
};
let scrollbar_w = f32::from(cosmic_theme.spacing.space_xxs);
let view_position = layout.position() + [self.padding.left, self.padding.top].into();
@ -429,16 +469,11 @@ where
// Pane too small for content, but still fill background
let terminal = self.terminal.lock().unwrap();
let meta = &terminal.metadata_set[terminal.default_attrs().metadata];
let background_color = shade(meta.bg, state.is_focused && !self.disabled);
renderer.fill_quad(
Quad {
bounds: layout.bounds(),
border: Border {
radius: if self.show_headerbar {
[0.0, 0.0, corner_radius[2], corner_radius[3]].into()
} else {
corner_radius.into()
},
radius: border_radius,
width: self.border.width,
color: self.border.color,
},
@ -446,12 +481,12 @@ where
..Default::default()
},
Color::from_rgba(
f32::from(background_color.r()) / 255.0,
f32::from(background_color.g()) / 255.0,
f32::from(background_color.b()) / 255.0,
f32::from(meta.bg.r()) / 255.0,
f32::from(meta.bg.g()) / 255.0,
f32::from(meta.bg.b()) / 255.0,
match self.opacity {
Some(opacity) => opacity,
None => f32::from(background_color.a()) / 255.0,
None => f32::from(meta.bg.a()) / 255.0,
},
),
);
@ -472,17 +507,11 @@ where
// Render default background
{
let meta = &terminal.metadata_set[terminal.default_attrs().metadata];
let background_color = shade(meta.bg, state.is_focused && !self.disabled);
renderer.fill_quad(
Quad {
bounds: layout.bounds(),
border: Border {
radius: if self.show_headerbar {
[0.0, 0.0, corner_radius[2], corner_radius[3]].into()
} else {
corner_radius.into()
},
radius: border_radius,
width: self.border.width,
color: self.border.color,
},
@ -490,12 +519,12 @@ where
..Default::default()
},
Color::from_rgba(
f32::from(background_color.r()) / 255.0,
f32::from(background_color.g()) / 255.0,
f32::from(background_color.b()) / 255.0,
f32::from(meta.bg.r()) / 255.0,
f32::from(meta.bg.g()) / 255.0,
f32::from(meta.bg.b()) / 255.0,
match self.opacity {
Some(opacity) => opacity,
None => f32::from(background_color.a()) / 255.0,
None => f32::from(meta.bg.a()) / 255.0,
},
),
);
@ -537,7 +566,7 @@ where
fn fill<Renderer: renderer::Renderer>(
&mut self,
renderer: &mut Renderer,
is_focused: bool,
_is_focused: bool,
) {
let cosmic_text_to_iced_color = |color: cosmic_text::Color| {
Color::from_rgba(
@ -551,7 +580,7 @@ where
macro_rules! mk_pos_offset {
($x_offset:expr, $bottom_offset:expr) => {
Vector::new(
self.start_x + $x_offset,
(self.start_x + $x_offset).floor(),
self.line_top + self.line_height - $bottom_offset,
)
};
@ -568,19 +597,57 @@ where
}
};
($pos_offset:expr, $style_line_height:expr) => {
mk_quad!($pos_offset, $style_line_height, self.end_x - self.start_x)
mk_quad!(
$pos_offset,
$style_line_height,
(self.end_x - self.start_x).ceil()
)
};
}
let metadata = &self.metadata_set[self.metadata];
if metadata.bg != self.metadata_set[self.default_metadata].bg {
let color = shade(metadata.bg, is_focused);
renderer.fill_quad(
mk_quad!(mk_pos_offset!(0.0, self.line_height), self.line_height),
cosmic_text_to_iced_color(color),
cosmic_text_to_iced_color(metadata.bg),
);
}
// Éléments de bloc peints nativement : fractions de la
// cellule (ou de la course fusionnée, à géométrie
// séparable), bords calés sur la grille — zéro glyphe,
// zéro interligne.
if let Some(block) = metadata.block {
let span_w = self.end_x - self.start_x;
let mut color = cosmic_text_to_iced_color(metadata.block_color);
let rects = match block {
BlockFill::Rects(rects) => rects,
BlockFill::Shade(quarters) => {
color.a *= f32::from(quarters) / 4.0;
[(0, 8, 0, 8), (8, 8, 8, 8)]
}
};
for (x0, x1, y0, y1) in rects {
if x0 >= x1 || y0 >= y1 {
continue;
}
let left = self.start_x + span_w * f32::from(x0) / 8.0;
let right = self.start_x + span_w * f32::from(x1) / 8.0;
let top = self.line_top + self.line_height * f32::from(y0) / 8.0;
let bottom = self.line_top + self.line_height * f32::from(y1) / 8.0;
renderer.fill_quad(
Quad {
bounds: Rectangle::new(
self.view_position + Vector::new(left.floor(), top),
Size::new(right.ceil() - left.floor(), bottom - top),
),
..Default::default()
},
color,
);
}
}
if !metadata.flags.is_empty() {
let style_line_height = (self.glyph_font_size / 10.0).clamp(1.0, 16.0);
@ -756,7 +823,7 @@ where
let scrollbar_color: Color = if pressed {
// pressed_state_color, 0.5
cosmic_theme
.background(false)
.background(theme.transparent)
.component
.pressed
.without_alpha()
@ -766,7 +833,7 @@ where
} else if hover {
// hover_state_color, 0.2
cosmic_theme
.background(false)
.background(theme.transparent)
.component
.hover
.without_alpha()
@ -867,6 +934,32 @@ where
}
}
// [yoda] Panneau non focalisé : voile de la couleur du fond, comme
// `unfocused-split-opacity` de Ghostty. Calque à part, sinon le texte
// (primitive du même calque) serait tracé par-dessus.
if let Some(dim) = self.dim {
let bg = terminal.metadata_set[terminal.default_attrs().metadata].bg;
renderer.with_layer(layout.bounds(), |renderer| {
renderer.fill_quad(
Quad {
bounds: layout.bounds(),
border: Border {
radius: border_radius,
..Default::default()
},
snap: true,
..Default::default()
},
Color::from_rgba(
f32::from(bg.r()) / 255.0,
f32::from(bg.g()) / 255.0,
f32::from(bg.b()) / 255.0,
dim,
),
);
});
}
let duration = instant.elapsed();
log::trace!("redraw {}, {}: {:?}", view_w, view_h, duration);
}
@ -891,7 +984,11 @@ where
let buffer_size = terminal.with_buffer(|buffer| buffer.size());
let is_app_cursor = terminal.term.lock().mode().contains(TermMode::APP_CURSOR);
let is_mouse_mode = terminal.term.lock().mode().intersects(TermMode::MOUSE_MODE);
// [yoda] En lecture seule, la souris revient au terminal : sélection et
// défilement locaux, même dans une application qui la capture.
let is_mouse_mode = terminal.term.lock().mode().intersects(TermMode::MOUSE_MODE)
&& !terminal.read_only;
let term_mode = *terminal.term.lock().mode();
match event {
Event::Window(event) => match event {
cosmic::iced::window::Event::Focused => {
@ -931,6 +1028,101 @@ where
}
_ => {}
},
// [yoda] Protocole clavier kitty, quand l'application au premier plan
// l'a demandé (voir `kitty_keyboard`) ; sinon encodage legacy plus bas.
Event::Keyboard(KeyEvent::KeyPressed {
key,
modified_key,
physical_key,
location,
modifiers,
text,
repeat,
}) if state.is_focused && kitty_keyboard::active(term_mode) => {
for key_bind in self.key_binds.keys() {
if key_bind.matches(*modifiers, key, Some(physical_key)) {
shell.capture_event();
return;
}
}
// Comportements locaux conservés, comme en legacy : Shift+PageUp…
// fait défiler l'historique, Échap annule une sélection.
let local_scroll = match modified_key {
Key::Named(Named::PageUp) if modifiers.shift() => Some(TerminalScroll::PageUp),
Key::Named(Named::PageDown) if modifiers.shift() => {
Some(TerminalScroll::PageDown)
}
Key::Named(Named::Home) if modifiers.shift() => Some(TerminalScroll::Top),
Key::Named(Named::End) if modifiers.shift() => Some(TerminalScroll::Bottom),
_ => None,
};
if let Some(scroll) = local_scroll {
terminal.scroll(scroll);
shell.capture_event();
return;
}
if *modified_key == Key::Named(Named::Escape) {
let had_selection = terminal.term.lock().selection.take().is_some();
if had_selection {
terminal.update();
shell.capture_event();
return;
}
}
let input = kitty_keyboard::KeyInput {
key,
modified_key,
location: *location,
modifiers: *modifiers,
text: text.as_deref(),
action: if *repeat {
kitty_keyboard::KeyAction::Repeat
} else {
kitty_keyboard::KeyAction::Press
},
};
let bytes = kitty_keyboard::encode(&input, term_mode);
if !bytes.is_empty() {
// Une touche de modification seule (« toutes les touches »)
// ne ramène pas la vue en bas.
let modifier_only = matches!(
modified_key,
Key::Named(Named::Shift | Named::Control | Named::Alt | Named::Super)
);
if modifier_only {
terminal.input_no_scroll(bytes);
} else {
terminal.input_scroll(bytes);
}
shell.capture_event();
}
}
Event::Keyboard(KeyEvent::KeyReleased {
key,
modified_key,
location,
modifiers,
..
}) if state.is_focused
&& kitty_keyboard::active(term_mode)
&& term_mode.contains(TermMode::REPORT_EVENT_TYPES) =>
{
let input = kitty_keyboard::KeyInput {
key,
modified_key,
location: *location,
modifiers: *modifiers,
text: None,
action: kitty_keyboard::KeyAction::Release,
};
let bytes = kitty_keyboard::encode(&input, term_mode);
if !bytes.is_empty() {
terminal.input_no_scroll(bytes);
shell.capture_event();
}
}
Event::Keyboard(KeyEvent::KeyPressed {
key: Key::Named(named),
physical_key,
@ -1046,7 +1238,15 @@ where
shell.capture_event();
}
Named::Enter => {
terminal.input_scroll(format!("{}{}", alt_prefix, "\x0D").into_bytes());
// [yoda] Shift+Entrée envoie LF (`\n`, comme Ctrl+J) : saut de
// ligne dans les éditeurs qui ne parlent pas le protocole kitty
// (rustyline de Modjo/Goose) ; pour un shell, c'est Entrée.
let code = if modifiers.shift() && !modifiers.control() {
"\x0A"
} else {
"\x0D"
};
terminal.input_scroll(format!("{alt_prefix}{code}").into_bytes());
shell.capture_event();
}
Named::Escape => {
@ -1344,11 +1544,11 @@ where
}
// Update context menu state
if let Some(on_context_menu) = &self.on_context_menu {
match self.context_menu {
Some(_) => {
match self.context_menu_open {
true => {
shell.publish(on_context_menu(None));
}
None => {
false => {
if *button == Button::Right {
let x = p.x - self.padding.left;
let y = p.y - self.padding.top;
@ -1366,23 +1566,13 @@ where
None,
);
let link = get_hyperlink(&terminal, location);
let abs = cosmic::iced::Point::new(
layout.bounds().x + p.x,
layout.bounds().y + p.y,
);
shell.publish(on_context_menu(Some(MenuState {
position: Some(abs),
local_position: Some(p),
link,
})));
shell.publish(on_context_menu(Some(MenuState { link })));
}
}
}
}
shell.capture_event();
}
} else {
state.is_focused = false;
}
}
Event::Mouse(MouseEvent::ButtonReleased(Button::Left)) => {
@ -1401,6 +1591,9 @@ where
}
}
terminal.needs_update = true;
if let Some(on_selection_done) = &self.on_selection_done {
shell.publish(on_selection_done());
}
}
if let Some(p) = cursor_position.position_in(layout.bounds()) {
let x = p.x - self.padding.left;
@ -1608,6 +1801,10 @@ where
}
_ => (),
}
if terminal.needs_update {
shell.request_redraw();
}
}
}
@ -1754,20 +1951,6 @@ fn accumulate_wheel_lines(y: f32, accumulator: f32) -> (i32, f32) {
(lines, total - lines as f32)
}
fn shade(color: cosmic_text::Color, is_focused: bool) -> cosmic_text::Color {
if is_focused {
color
} else {
let shade = 0.92;
cosmic_text::Color::rgba(
(f32::from(color.r()) * shade) as u8,
(f32::from(color.g()) * shade) as u8,
(f32::from(color.b()) * shade) as u8,
color.a(),
)
}
}
impl<'a, Message> From<TerminalBox<'a, Message>> for Element<'a, Message, cosmic::Theme, Renderer>
where
Message: Clone + 'a,