Compare commits

..

No commits in common. "local/yoda-main" and "pre-1.6-merge-20260819" have entirely different histories.

49 changed files with 1461 additions and 3941 deletions

View file

@ -1,10 +1,8 @@
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 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.
---
- 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).

View file

@ -1,14 +0,0 @@
# 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

1852
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,70 +0,0 @@
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,51 +1,3 @@
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 cosmic-term (1.2.0) noble; urgency=medium
* Epoch 1.2.0 version update * Epoch 1.2.0 version update

View file

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

View file

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

View file

@ -1,6 +1,3 @@
cancel = Cancel·lar cancel = Cancel·lar
settings = Configuració settings = Configuració
quit = Surt 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 = Písmo
default-font-size = Velikost písma default-font-size = Velikost písma
default-font-stretch = Šířka písma default-font-stretch = Šířka písma
default-font-weight = Tloušťka normálního písma default-font-weight = Normální tloušťka písma
default-dim-font-weight = Tloušťka zeslabeného písma default-dim-font-weight = Tenká tloušťka písma
default-bold-font-weight = Tloušťka tučného písma default-bold-font-weight = Tučná tloušťka písma
use-bright-bold = Světlejší tučný text use-bright-bold = Světlejší tučný text
splits = Rozdělení splits = Rozdělení
focus-follow-mouse = Zaměření psaní sleduje myš focus-follow-mouse = Zaměření psaní sleduje myš
advanced = Pokročilé advanced = Pokročilé
show-headerbar = Zobrazit záhlaví show-headerbar = Zobrazit hlavičku
show-header-description = Záhlaví lze odkrýt v nabídce zobrazené po kliknutí pravým tlačítkem myši show-header-description = Hlavičku je možné odkrýt z kontextového menu
find-placeholder = Najít... find-placeholder = Najít...
find-previous = Najít předchozí find-previous = Najít předchozí
find-next = Najít další find-next = Najít další
@ -69,7 +69,7 @@ next-tab = Další karta
previous-tab = Předchozí karta previous-tab = Předchozí karta
split-horizontal = Rozdělit horizontálně split-horizontal = Rozdělit horizontálně
split-vertical = Rozdělit vertikálně split-vertical = Rozdělit vertikálně
pane-toggle-maximize = Maximalizovat nebo obnovit podokno pane-toggle-maximize = Přepnout maximalizaci
menu-color-schemes = Barevná schémata... menu-color-schemes = Barevná schémata...
menu-settings = Nastavení... menu-settings = Nastavení...
menu-about = O aplikaci Terminál COSMIC... menu-about = O aplikaci Terminál COSMIC...
@ -99,17 +99,13 @@ shortcut-group-zoom = Přiblížení
toggle-fullscreen = Přepnout režim celé obrazovky toggle-fullscreen = Přepnout režim celé obrazovky
type-to-search = Pište pro vyhledávání... type-to-search = Pište pro vyhledávání...
copy-or-sigint = Kopírovat nebo SIGINT copy-or-sigint = Kopírovat nebo SIGINT
focus-pane-down = Zaměřit podokno dole focus-pane-down = Zaměřit panel dole
focus-pane-left = Zaměřit podokno vlevo focus-pane-left = Zaměřit panel vlevo
focus-pane-right = Zaměřit podokno vpravo focus-pane-right = Zaměřit panel vpravo
focus-pane-up = Zaměřit podokno nahoře focus-pane-up = Zaměřit panel nahoře
shortcut-replace-body = { $binding } je již přiřazena k „{ $existing }“. Nahradit za „{ $new_action }“? shortcut-replace-body = { $binding } je již přiřazena k „{ $existing }“. Nahradit za „{ $new_action }“?
shortcut-capture-hint = Stiskněte kombinaci kláves shortcut-capture-hint = Stiskněte kombinaci kláves
paste-primary = Vložit primární paste-primary = Vložit primární
copy-link = Kopírovat odkaz copy-link = Kopírovat odkaz
tab-new-inherit-working-directory = Nové karty a okna používají aktuální adresář 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 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

@ -1,31 +0,0 @@
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

@ -69,7 +69,7 @@ advanced = Erweitert
show-headerbar = Kopfzeile anzeigen show-headerbar = Kopfzeile anzeigen
show-header-description = Kopfzeile über das Rechtsklick-Menü aufzeigen show-header-description = Kopfzeile über das Rechtsklick-Menü aufzeigen
# Suchen # Suchen
find-placeholder = Suchen ... find-placeholder = Suchen...
find-previous = Vorherigen suchen find-previous = Vorherigen suchen
find-next = Nächsten suchen find-next = Nächsten suchen
@ -82,7 +82,7 @@ file = Datei
new-tab = Neuer Tab new-tab = Neuer Tab
new-window = Neues Fenster new-window = Neues Fenster
profile = Profil profile = Profil
menu-profiles = Profile ... menu-profiles = Profile...
close-tab = Tab schließen close-tab = Tab schließen
quit = Beenden quit = Beenden
@ -105,15 +105,15 @@ next-tab = Nächster Tab
previous-tab = Vorheriger Tab previous-tab = Vorheriger Tab
split-horizontal = Horizontal teilen split-horizontal = Horizontal teilen
split-vertical = Vertikal teilen split-vertical = Vertikal teilen
pane-toggle-maximize = Bereich maximieren oder wiederherstellen pane-toggle-maximize = Maximierung umschalten
menu-color-schemes = Farbschemen ... menu-color-schemes = Farbschemen...
menu-settings = Einstellungen ... menu-settings = Einstellungen...
menu-about = Über COSMIC Terminal ... menu-about = Über COSMIC Terminal...
repository = Repositorium repository = Repository
cancel = Abbrechen cancel = Abbrechen
keyboard-shortcuts = Tastenkombinationen keyboard-shortcuts = Tastenkombinationen
open-link = Link öffnen open-link = Link öffnen
menu-password-manager = Passwörter ... menu-password-manager = Passwörter...
passwords-title = Passwörter passwords-title = Passwörter
add-password = Passwort hinzufügen add-password = Passwort hinzufügen
password-input = Passwort password-input = Passwort
@ -124,7 +124,7 @@ focus-pane-down = Bereich unten fokussieren
focus-pane-left = Bereich links fokussieren focus-pane-left = Bereich links fokussieren
focus-pane-right = Bereich rechts fokussieren focus-pane-right = Bereich rechts fokussieren
focus-pane-up = Bereich oben fokussieren focus-pane-up = Bereich oben fokussieren
menu-keyboard-shortcuts = Tastenkombinationen ... menu-keyboard-shortcuts = Tastenkombinationen...
no-shortcuts = Keine Tastenkombinationen no-shortcuts = Keine Tastenkombinationen
password-manager = Passwortmanager password-manager = Passwortmanager
replace = Ersetzen replace = Ersetzen
@ -137,7 +137,7 @@ shortcut-group-window = Fenster
shortcut-replace-title = Tastenkombination ersetzen? shortcut-replace-title = Tastenkombination ersetzen?
tab-activate = Tab { $number } aktivieren tab-activate = Tab { $number } aktivieren
copy-link = Link kopieren copy-link = Link kopieren
type-to-search = Zum Suchen tippen ... type-to-search = Zum Suchen tippen...
close-window = Fenster schließen close-window = Fenster schließen
copy-or-sigint = Kopieren oder SIGINT copy-or-sigint = Kopieren oder SIGINT
toggle-fullscreen = Vollbild umschalten toggle-fullscreen = Vollbild umschalten
@ -149,7 +149,3 @@ shortcut-replace-body = { $binding } ist bereits { $existing } zugeordnet. Durch
keywords = Befehl;Shell;Terminal;CLI; keywords = Befehl;Shell;Terminal;CLI;
tab-new-inherit-working-directory = Neue Tabs und Fenster verwenden das aktuelle Verzeichnis 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 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 = Προφίλ profile = Προφίλ
menu-profiles = Προφίλ... menu-profiles = Προφίλ...
close-tab = Κλείσιμο καρτέλας close-tab = Κλείσιμο καρτέλας
quit = Τερματισμός quit = Έξοδος
## Edit ## Edit
@ -107,7 +107,7 @@ next-tab = Επόμενη καρτέλα
previous-tab = Προηγούμενη καρτέλα previous-tab = Προηγούμενη καρτέλα
split-horizontal = Οριζόντιος διαχωρισμός split-horizontal = Οριζόντιος διαχωρισμός
split-vertical = Κάθετος διαχωρισμός split-vertical = Κάθετος διαχωρισμός
pane-toggle-maximize = Μεγιστοποίηση ή επαναφορά προβολής pane-toggle-maximize = Εναλλαγή μεγιστοποίησης
menu-color-schemes = Χρωματικοί συνδυασμοί... menu-color-schemes = Χρωματικοί συνδυασμοί...
menu-settings = Ρυθμίσεις... menu-settings = Ρυθμίσεις...
menu-about = Σχετικά με το Τερματικό COSMIC... menu-about = Σχετικά με το Τερματικό COSMIC...
@ -152,7 +152,3 @@ shortcut-group-zoom = Ζουμ
shortcut-replace-body = Ο συνδυασμός { $binding } έχει ήδη αντιστοιχιστεί στην ενέργεια «{ $existing }». Θέλετε να αντικατασταθεί με την ενέργεια «{ $new_action }»; shortcut-replace-body = Ο συνδυασμός { $binding } έχει ήδη αντιστοιχιστεί στην ενέργεια «{ $existing }». Θέλετε να αντικατασταθεί με την ενέργεια «{ $new_action }»;
tab-new-inherit-working-directory = Οι νέες καρτέλες και παράθυρα χρησιμοποιούν τον τρέχοντα κατάλογο tab-new-inherit-working-directory = Οι νέες καρτέλες και παράθυρα χρησιμοποιούν τον τρέχοντα κατάλογο
tab-new-inherit-working-directory-description = Άνοιγμα νέων καρτελών και παραθύρων στον τρέχοντα κατάλογο της ενεργής καρτέλας tab-new-inherit-working-directory-description = Άνοιγμα νέων καρτελών και παραθύρων στον τρέχοντα κατάλογο της ενεργής καρτέλας
show-pane-borders = Εμφάνιση περιγραμμάτων πλαισίων
window-header = Κεφαλίδα παραθύρου
move-tab-left = Μετακίνηση καρτέλας στα αριστερά
move-tab-right = Μετακίνηση καρτέλας στα δεξιά

View file

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

View file

@ -58,18 +58,13 @@ use-bright-bold = Make bold text brighter
### Splits ### Splits
splits = Splits splits = Splits
focus-follow-mouse = Typing focus follows mouse focus-follow-mouse = Typing focus follows mouse
show-pane-borders = Show pane borders
unfocused-pane-dim = Dim unfocused panes
### Advanced ### Advanced
advanced = Advanced advanced = Advanced
show-headerbar = Show header show-headerbar = Show header
window-header = Window header
show-header-description = Reveal the header from the right-click menu 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 = 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 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 ### Keyboard shortcuts
add-another-keybinding = Add another keybinding add-another-keybinding = Add another keybinding
@ -142,10 +137,6 @@ save-output = Save output as text...
monitor-activity = Monitor for activity monitor-activity = Monitor for activity
monitor-silence = Monitor for silence monitor-silence = Monitor for silence
monitor-process-finished = Monitor for process finishing monitor-process-finished = Monitor for process finishing
read-only = Read-only
## Notifications
notification-show = Show tab
## Open ## Open
open-link = Open Link open-link = Open Link
@ -158,11 +149,9 @@ zoom-reset = Default text size
zoom-out = Smaller text zoom-out = Smaller text
next-tab = Next tab next-tab = Next tab
previous-tab = Previous tab previous-tab = Previous tab
move-tab-left = Move tab left
move-tab-right = Move tab right
split-horizontal = Split horizontal split-horizontal = Split horizontal
split-vertical = Split vertical split-vertical = Split vertical
pane-toggle-maximize = Maximize or restore pane pane-toggle-maximize = Toggle maximized
menu-color-schemes = Color schemes... menu-color-schemes = Color schemes...
menu-settings = Settings... menu-settings = Settings...
menu-about = About COSMIC Terminal... menu-about = About COSMIC Terminal...

View file

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

View file

@ -38,7 +38,7 @@ settings = Asetukset
appearance = Ulkoasu appearance = Ulkoasu
theme = Teema theme = Teema
match-desktop = Sama kuin työpöytä match-desktop = Sovita työpöytään
dark = Tumma dark = Tumma
light = Vaalea light = Vaalea
syntax-dark = Tumma väriteema syntax-dark = Tumma väriteema
@ -107,7 +107,7 @@ next-tab = Seuraava välilehti
previous-tab = Edellinen välilehti previous-tab = Edellinen välilehti
split-horizontal = Halkaise näkymä vaakasuunnassa split-horizontal = Halkaise näkymä vaakasuunnassa
split-vertical = Halkaise näkymä pystysuunnassa split-vertical = Halkaise näkymä pystysuunnassa
pane-toggle-maximize = Suurenna tai palauta paneeli pane-toggle-maximize = Suurennus päällä tai pois
menu-color-schemes = Väriteemat… menu-color-schemes = Väriteemat…
menu-settings = Asetukset… menu-settings = Asetukset…
menu-about = Tietoa COSMICin päätteestä… menu-about = Tietoa COSMICin päätteestä…
@ -151,7 +151,3 @@ toggle-fullscreen = Koko näyttö päällä tai pois
shortcut-group-other = Muut shortcut-group-other = Muut
tab-new-inherit-working-directory = Uudet välilehdet käyttävät nykyistä hakemistoa 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 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

View file

@ -96,7 +96,7 @@ quit = Quitter
## Edit ## Edit
edit = Édition edit = Modifier
copy = Copier copy = Copier
paste = Coller paste = Coller
select-all = Sélectionner tout select-all = Sélectionner tout
@ -108,10 +108,6 @@ save-output = Enregistrer la sortie comme texte...
monitor-activity = Surveiller l'activité monitor-activity = Surveiller l'activité
monitor-silence = Surveiller le silence monitor-silence = Surveiller le silence
monitor-process-finished = Surveiller la fin du processus monitor-process-finished = Surveiller la fin du processus
read-only = Lecture seule
## Notifications
notification-show = Afficher l'onglet
## View ## View
@ -123,7 +119,7 @@ next-tab = Onglet suivant
previous-tab = Onglet précédent previous-tab = Onglet précédent
split-horizontal = Scinder en horizontal split-horizontal = Scinder en horizontal
split-vertical = Scinder en vertical split-vertical = Scinder en vertical
pane-toggle-maximize = Agrandir ou restaurer le panneau pane-toggle-maximize = Vue unique
menu-color-schemes = Palettes de couleurs... menu-color-schemes = Palettes de couleurs...
menu-settings = Paramètres... menu-settings = Paramètres...
menu-about = À propos de terminal COSMIC... menu-about = À propos de terminal COSMIC...
@ -172,10 +168,3 @@ comment = Émulateur de terminal pour le bureau COSMIC
keywords = Commande;Shell;Terminal;CLI; 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 = 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 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 previous-tab = An cluaisín roimhe seo
split-horizontal = Scoilt cothrománach split-horizontal = Scoilt cothrománach
split-vertical = Scoilt ingearach split-vertical = Scoilt ingearach
pane-toggle-maximize = Uasmhéadaigh nó athchóirigh an phána pane-toggle-maximize = Scoránaigh uasmhéadú
menu-color-schemes = Scéimeanna dathanna... menu-color-schemes = Scéimeanna dathanna...
menu-settings = Socruithe... menu-settings = Socruithe...
menu-about = Maidir le COSMIC Teirminéal... menu-about = Maidir le COSMIC Teirminéal...
@ -147,7 +147,5 @@ type-to-search = Clóscríobh le cuardach a dhéanamh...
comment = Aithriseoir teirminéal don deasc COSMIC comment = Aithriseoir teirminéal don deasc COSMIC
keywords = Ordú;Blaosc;Teirminéal;CLI; keywords = Ordú;Blaosc;Teirminéal;CLI;
copy-link = Cóipeáil Nasc copy-link = Cóipeáil Nasc
tab-new-inherit-working-directory = Úsáideann cluaisíní agus fuinneoga nua an t-eolaire reatha tab-new-inherit-working-directory = Úsáideann cluaisíní nua an eolaire reatha
tab-new-inherit-working-directory-description = Oscail cluaisíní agus fuinneoga nua in eolaire oibre an chluaisín ghníomhaigh tab-new-inherit-working-directory-description = Oscail cluaisíní nua in eolaire oibre an chluaisín ghníomhaigh
show-pane-borders = Taispeáin teorainneacha na pána
window-header = Ceanntásc na fuinneoige

View file

@ -1,115 +0,0 @@
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 = सेटिंग्स settings = सेटिंग
### Appearance ### Appearance
appearance = रुप-रंग appearance = दिखावट
theme = प्रसंग theme = थीम
match-desktop = डेस्कटॉप से मेल खाएँ match-desktop = डेस्कटॉप से मिलाएं
dark = डार्क dark = डार्क
light = लाइट light = लाइट
syntax-dark = रंग योजना डार्क syntax-dark = रंग योजना डार्क
syntax-light = रंग योजना लाइट syntax-light = रंग योजना लाइट
default-zoom-step = ज़ूम चरण default-zoom-step = ज़ूम स्तर
opacity = पृष्ठभूमि अपारदर्शिता opacity = पृष्ठभूमि अपारदर्शिता
### Font ### Font
@ -66,7 +66,7 @@ advanced = एडवांस
show-headerbar = हैडर दिखाएं show-headerbar = हैडर दिखाएं
show-header-description = हैडर को राइट-क्लिक मेन्यू से दिखाएं। show-header-description = हैडर को राइट-क्लिक मेन्यू से दिखाएं।
# Find # Find
find-placeholder = खोजें..। find-placeholder = खोजें...
find-previous = पिछला खोजें find-previous = पिछला खोजें
find-next = अगला खोजें find-next = अगला खोजें
@ -81,13 +81,13 @@ new-window = नई विंडो
profile = प्रोफाइल profile = प्रोफाइल
menu-profiles = प्रोफाइल्स... menu-profiles = प्रोफाइल्स...
close-tab = टैब बंद करें close-tab = टैब बंद करें
quit = बाहर जाएँ quit = बंद करें
### Edit ### Edit
edit = संपादन edit = संपादित करें
copy = कॉपी करें copy = कॉपी करें
paste = चिपकाएँ paste = पेस्ट करें
select-all = सभी चुनें select-all = सभी चुनें
find = खोजें find = खोजें
clear-scrollback = स्क्रॉलबैक साफ़ करें clear-scrollback = स्क्रॉलबैक साफ़ करें
@ -104,11 +104,7 @@ split-horizontal = क्षैतिज विभाजन
split-vertical = लंबवत विभाजन split-vertical = लंबवत विभाजन
pane-toggle-maximize = अधिकतम करें pane-toggle-maximize = अधिकतम करें
menu-color-schemes = रंग योजनाएँ... menu-color-schemes = रंग योजनाएँ...
menu-settings = सेटिंग्स..। menu-settings = सेटिंग्स...
menu-about = कॉस्मिक टर्मिनल के बारे में... menu-about = कॉस्मिक टर्मिनल के बारे में...
repository = रिपॉजिटरी 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
font = Betűkészlet font = Betűtípus
advanced-font-settings = Speciális betűkészlet-beállítások advanced-font-settings = Speciális betűtípus-beállítások
default-font = Betűkészlet default-font = Betűtípus
default-font-size = Betűméret default-font-size = Betűméret
default-font-stretch = Betűszélesség default-font-stretch = Betűszélesség
default-font-weight = Normál betűvastagság default-font-weight = Normál betűsúly
default-dim-font-weight = Vékony betűvastagság default-dim-font-weight = Vékony betűsúly
default-bold-font-weight = Félkövér betűvastagság default-bold-font-weight = Félkövér betűsúly
use-bright-bold = Félkövér szöveg világosítása use-bright-bold = Félkövér szöveg világosítása
### Splits ### Splits
@ -109,7 +109,7 @@ next-tab = Következő lap
previous-tab = Előző lap previous-tab = Előző lap
split-horizontal = Vízszintes felosztás split-horizontal = Vízszintes felosztás
split-vertical = Függőleges felosztás split-vertical = Függőleges felosztás
pane-toggle-maximize = Panel maximalizálása vagy visszaállítása pane-toggle-maximize = Panel maximalizálása
menu-color-schemes = Színsémák… menu-color-schemes = Színsémák…
menu-settings = Beállítások… menu-settings = Beállítások…
menu-about = A COSMIC Terminál névjegye… menu-about = A COSMIC Terminál névjegye…
@ -149,7 +149,3 @@ type-to-search = Gépelj a kereséshez…
copy-link = Hivatkozás másolása 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 = 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 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 support = Dukungan
color-schemes = Skema warna color-schemes = Skema warna
cosmic-terminal = Terminal COSMIC cosmic-terminal = Terminal COSMIC
rename = Ganti Nama rename = Ganti nama
export = Ekspor export = Ekspor
delete = Hapus delete = Hapus
import = Impor import = Impor
@ -23,7 +23,7 @@ remain-open = Tetap buka setelah proses anak berakhir.
settings = Pengaturan settings = Pengaturan
appearance = Tampilan appearance = Tampilan
theme = Tema theme = Tema
match-desktop = Sesuaikan desktop match-desktop = Cocokkan desktop
dark = Gelap dark = Gelap
light = Terang light = Terang
syntax-dark = Skema warna gelap syntax-dark = Skema warna gelap
@ -68,7 +68,7 @@ next-tab = Tab selanjutnya
previous-tab = Tab sebelumnya previous-tab = Tab sebelumnya
split-horizontal = Pemisahan horisontal split-horizontal = Pemisahan horisontal
split-vertical = Pemisahan vertikal split-vertical = Pemisahan vertikal
pane-toggle-maximize = Maksimalkan atau pulihkan panel pane-toggle-maximize = Ubah ke maksimal
menu-color-schemes = Skema warna... menu-color-schemes = Skema warna...
menu-settings = Pengaturan... menu-settings = Pengaturan...
menu-about = Tentang Terminal COSMIC... menu-about = Tentang Terminal COSMIC...
@ -109,7 +109,3 @@ keywords = Perintah;Shell;Terminal;CLI;
copy-link = Salin Tautan copy-link = Salin Tautan
tab-new-inherit-working-directory = Tab dan jendela baru menggunakan direktori saat ini 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 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 appearance = Aspetto
theme = Tema theme = Tema
match-desktop = Abbina al desktop match-desktop = Sistema
dark = Scuro dark = Scuro
light = Chiaro light = Chiaro
syntax-dark = Schema colori scuro syntax-dark = Schema colori scuro
@ -115,7 +115,3 @@ menu-settings = Impostazioni...
menu-about = Informazioni su COSMIC Terminal... menu-about = Informazioni su COSMIC Terminal...
support = Supporto support = Supporto
repository = Repository repository = Repository
cancel = Annulla
keyboard-shortcuts = Scorciatoie da tastiera
replace = Sostituisci
menu-keyboard-shortcuts = Scorciatoie da tastiera...

View file

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

View file

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

View file

@ -27,13 +27,13 @@ light = Ronî
advanced = Pêşketî advanced = Pêşketî
support = Piştgirî support = Piştgirî
default-font = Cûrenivîs default-font = Cûrenivîs
paste = Pê ve bike paste = Pêve bike
menu-settings = Sazkarî... menu-settings = Sazkarî...
command-line = Rêza fermanê command-line = Rêza fermanê
view = Dîtin view = Dîtin
dark = Tarî dark = Tarî
hold = Ragire hold = Ragire
match-desktop = Bi sermaseyê re guncav bike match-desktop = Li gorî sermaseyê
find-previous = Yê paş bibîne find-previous = Yê paş bibîne
cosmic-terminal = Termînalê COSMIC cosmic-terminal = Termînalê COSMIC
find-placeholder = Bibîne... find-placeholder = Bibîne...
@ -62,7 +62,7 @@ shortcut-group-zoom = Nêzîkbûn
repository = Embar repository = Embar
find = Bibîne find = Bibîne
replace = Biguhêrîne replace = Biguhêrîne
syntax-light = Şêwaza rengan a ronî syntax-light = Şêwaza rengê ronî
default-font-weight = Giraniya cûrenivîsê ya normal default-font-weight = Giraniya cûrenivîsê ya normal
zoom-in = Nivîsa mezintir zoom-in = Nivîsa mezintir
previous-tab = Rûgera paş previous-tab = Rûgera paş
@ -100,7 +100,7 @@ focus-pane-left = Balê bikişîne ser destgeha çepê
focus-pane-right = Balê bikişîne ser destgeha rastê focus-pane-right = Balê bikişîne ser destgeha rastê
focus-pane-up = Balê bikişîne ser destgeha jorîn focus-pane-up = Balê bikişîne ser destgeha jorîn
password-manager = Rêveberê borînpeyvê password-manager = Rêveberê borînpeyvê
paste-primary = A sereke pê ve bike paste-primary = A sereke pêve bike
reset-to-default = Vegere rewşa berdest reset-to-default = Vegere rewşa berdest
shortcut-capture-hint = Kombînasyona kilîtan bitikîne shortcut-capture-hint = Kombînasyona kilîtan bitikîne
shortcut-replace-body = { $binding } berê ji bo { $existing } hatiye destnîşankirin. Tu dixwazî bi { $new_action } re biguherînî? shortcut-replace-body = { $binding } berê ji bo { $existing } hatiye destnîşankirin. Tu dixwazî bi { $new_action } re biguherînî?
@ -109,7 +109,3 @@ tab-activate = Rûgera { $number } çalak bike
toggle-fullscreen = Dîmendera tijî biguhêrîne toggle-fullscreen = Dîmendera tijî biguhêrîne
type-to-search = Ji bo lêgerînê binivîse... type-to-search = Ji bo lêgerînê binivîse...
copy-link = Girêdanê jê bigire 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 name = Pavadinimas
delete = Ištrinti delete = Ištrinti
cosmic-terminal = COSMIC Terminalas cosmic-terminal = COSMIC Terminal
new-terminal = Naujas terminalas new-terminal = Naujas terminalas
repository = Saugykla repository = Saugykla
support = Palaikymas support = Palaikymas
@ -19,7 +19,7 @@ make-default = Padaryti numatytu
working-directory = Darbinis katalogas working-directory = Darbinis katalogas
hold = Laikyti hold = Laikyti
remain-open = Palikti atidarytą išsijungus vaikiniam procesui. remain-open = Palikti atidarytą išsijungus vaikiniam procesui.
settings = Nuostatai settings = Nuostatos
appearance = Išvaizda appearance = Išvaizda
theme = Stilius theme = Stilius
match-desktop = Pagal darbalaukio temą match-desktop = Pagal darbalaukio temą
@ -47,7 +47,7 @@ new-window = Naujas langas
profile = Profilis profile = Profilis
menu-profiles = Profiliai... menu-profiles = Profiliai...
close-tab = Uždaryti skirtuką close-tab = Uždaryti skirtuką
quit = Išjungti quit = Išeiti
edit = Redaguoti edit = Redaguoti
copy = Kopijuoti copy = Kopijuoti
paste = Įdėti paste = Įdėti
@ -65,7 +65,7 @@ split-horizontal = Skaidyti horizontaliai
split-vertical = Skaidyti vertikaliai split-vertical = Skaidyti vertikaliai
pane-toggle-maximize = Perjungti maksimalų padidinimą pane-toggle-maximize = Perjungti maksimalų padidinimą
menu-color-schemes = Spalvų schemos... menu-color-schemes = Spalvų schemos...
menu-settings = Nuostatai... menu-settings = Nuostatos...
menu-about = Apie COSMIC Terminal... menu-about = Apie COSMIC Terminal...
menu-password-manager = Slaptažodžiai... menu-password-manager = Slaptažodžiai...
passwords-title = Slaptažodžiai passwords-title = Slaptažodžiai

View file

@ -1,14 +1 @@
settings = Configuracion settings = Paramètres
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 = ਸੈਟਿੰਗਾਂ settings = ਸੈਟਿੰਗਾਂ
appearance = ਦਿੱਖ appearance = ਦਿੱਖ
theme = ਥੀਮ theme = ਥੀਮ
match-desktop = ਡੈਸਕਟਾਪ ਨਾਲ ਮਿਲਾਓ match-desktop = ਡੈਸਕਟਾਪ ਨਾਲ ਮਿਲਦਾ
dark = ਗੂੜ੍ਹਾ dark = ਗੂੜ੍ਹਾ
light = ਫਿੱਕਾ light = ਹਲਕਾ
default-zoom-step = ਜ਼ੂਮ ਸਟੈਪ default-zoom-step = ਜ਼ੂਮ ਸਟੈਪ
quit = ਸਮਾਪਤ quit = ਬਾਹਰ
edit = ਸੋਧੋ edit = ਸੋਧੋ
copy = ਕਾਪੀ ਕਰੋ copy = ਕਾਪੀ ਕਰੋ
paste = ਚੇਪੋ paste = ਚੇਪੋ

View file

@ -1,5 +1,5 @@
cosmic-terminal = Terminal COSMIC cosmic-terminal = Terminal COSMIC
comment = Emulator terminala dla środowiska COSMIC comment = Emulator terminala dla pulpitu COSMIC
keywords = Cli;WierszPoleceń;Powłoka;Terminal; keywords = Cli;WierszPoleceń;Powłoka;Terminal;
new-terminal = Nowy terminal new-terminal = Nowy terminal
@ -73,7 +73,7 @@ advanced = Zaawansowane
show-headerbar = Pokaż pasek nagłówka show-headerbar = Pokaż pasek nagłówka
show-header-description = Ukazuje pasek nagłówka z menu wybieranego prawym przyciskiem myszki show-header-description = Ukazuje pasek nagłówka z menu wybieranego prawym przyciskiem myszki
# Find # Find
find-placeholder = Wyszukaj… find-placeholder = Szukaj…
find-previous = Znajdź poprzedni find-previous = Znajdź poprzedni
find-next = Znajdź następny find-next = Znajdź następny
@ -88,7 +88,7 @@ new-window = Nowe okno
profile = Profil profile = Profil
menu-profiles = Profile... menu-profiles = Profile...
close-tab = Zamknij kartę close-tab = Zamknij kartę
quit = Zakończ quit = Zamknij
## Edit ## Edit
@ -96,7 +96,7 @@ edit = Edytuj
copy = Kopiuj copy = Kopiuj
paste = Wklej paste = Wklej
select-all = Zaznacz wszystko select-all = Zaznacz wszystko
find = Wyszukaj find = Szukaj
clear-scrollback = Wyczyść przewijanie clear-scrollback = Wyczyść przewijanie
## View ## View
@ -109,7 +109,7 @@ next-tab = Następna karta
previous-tab = Poprzednia karta previous-tab = Poprzednia karta
split-horizontal = Podziel w poziomie split-horizontal = Podziel w poziomie
split-vertical = Podziel w pionie split-vertical = Podziel w pionie
pane-toggle-maximize = Maksymalizuj lub przywróć panel pane-toggle-maximize = Przełącznik maksymalizacji
menu-color-schemes = Schemat kolorów... menu-color-schemes = Schemat kolorów...
menu-settings = Ustawienia… menu-settings = Ustawienia…
menu-about = O Terminalu COSMIC... menu-about = O Terminalu COSMIC...
@ -149,5 +149,3 @@ focus-pane-up = Aktywuj panel wyżej
copy-link = Skopiuj Odnośnik copy-link = Skopiuj Odnośnik
tab-new-inherit-working-directory = Nowe karty i okna użyją obecnego katalogu 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 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
advanced = Avançado advanced = Avançado
show-headerbar = Mostrar cabeçalho show-headerbar = Mostrar a barra de título
show-header-description = Mostrar o cabeçalho ao clicar no menu com o botão direito do mouse show-header-description = Mostrar o cabeçalho ao clicar no menu com o botão direito do mouse
# Find # Find
find-placeholder = Localizar... find-placeholder = Localizar...
@ -88,7 +88,7 @@ new-window = Nova janela
profile = Perfil profile = Perfil
menu-profiles = Perfis... menu-profiles = Perfis...
close-tab = Fechar aba close-tab = Fechar aba
quit = Encerrar quit = Sair
## Edit ## Edit
@ -109,7 +109,7 @@ next-tab = Próxima aba
previous-tab = Aba anterior previous-tab = Aba anterior
split-horizontal = Dividir horizontalmente split-horizontal = Dividir horizontalmente
split-vertical = Dividir verticalmente split-vertical = Dividir verticalmente
pane-toggle-maximize = Maximizar ou restaurar o quadro pane-toggle-maximize = Alternar maximização
menu-color-schemes = Esquemas de cores... menu-color-schemes = Esquemas de cores...
menu-settings = Configurações... menu-settings = Configurações...
menu-about = Sobre o Terminal... menu-about = Sobre o Terminal...
@ -149,7 +149,3 @@ type-to-search = Digite para pesquisar...
copy-link = Copiar link copy-link = Copiar link
tab-new-inherit-working-directory = Novas abas e janelas usam o diretório atual 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 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 = Терминал cosmic-terminal = Терминал
comment = Эмулятор терминала для среды COSMIC comment = Эмулятор терминала для рабочей среды COSMIC
keywords = Команда;Оболочка;Терминал;Консоль; keywords = Команда;Оболочка;Терминал;Консоль;
new-terminal = Новый терминал new-terminal = Новый терминал
@ -86,7 +86,7 @@ new-window = Новое окно
profile = Профиль profile = Профиль
menu-profiles = Профили... menu-profiles = Профили...
close-tab = Закрыть вкладку close-tab = Закрыть вкладку
quit = Завершить quit = Выйти
## Edit ## Edit
@ -106,7 +106,7 @@ next-tab = Следующая вкладка
previous-tab = Предыдущая вкладка previous-tab = Предыдущая вкладка
split-horizontal = Разделение по горизонтали split-horizontal = Разделение по горизонтали
split-vertical = Разделение по вертикали split-vertical = Разделение по вертикали
pane-toggle-maximize = Развернуть/восстановить область pane-toggle-maximize = Переключить на весь экран
menu-color-schemes = Цветовые схемы... menu-color-schemes = Цветовые схемы...
menu-settings = Параметры... menu-settings = Параметры...
menu-about = О Терминале COSMIC... menu-about = О Терминале COSMIC...
@ -141,9 +141,5 @@ tab-activate = Активировать вкладку { $number }
toggle-fullscreen = Вкл./выкл. полноэкранный режим toggle-fullscreen = Вкл./выкл. полноэкранный режим
type-to-search = Введите для поиска... type-to-search = Введите для поиска...
copy-link = Копировать ссылку copy-link = Копировать ссылку
tab-new-inherit-working-directory = Текущий каталог в новых вкладках и окнах tab-new-inherit-working-directory = В новых вкладках исп. текущий каталог
tab-new-inherit-working-directory-description = Открывать новые вкладки и окна в рабочем каталоге активной вкладки tab-new-inherit-working-directory-description = Открывать новые вкладки в рабочем каталоге активной вкладки
show-pane-borders = Показывать границы областей
window-header = Заголовок окна
move-tab-left = Перемещение вкладки налево
move-tab-right = Перемещение вкладки направо

View file

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

View file

@ -110,10 +110,10 @@ next-tab = Nästa flik
previous-tab = Föregående flik previous-tab = Föregående flik
split-horizontal = Dela horisontellt split-horizontal = Dela horisontellt
split-vertical = Dela vertikalt split-vertical = Dela vertikalt
pane-toggle-maximize = Maximera eller återställ fält pane-toggle-maximize = Växla maximerad
clear-scrollback = Töm tillbakarullning clear-scrollback = Töm tillbakarullning
menu-color-schemes = Färgscheman… menu-color-schemes = Färgscheman…
menu-settings = Inställningar... menu-settings = Inställningar…
menu-about = Om COSMIC Terminal… menu-about = Om COSMIC Terminal…
repository = Källkod repository = Källkod
support = Support support = Support
@ -153,7 +153,3 @@ shortcut-group-zoom = Zooma
copy-link = Kopiera länk 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-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 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 = ลักษณะ appearance = ลักษณะ
theme = ธีม theme = ธีม
match-desktop = ใช้ตามธีมหน้าจอหลัก match-desktop = ใช้ตามธีมหน้าจอหลัก
dark = มืด dark = ธีมมืด
light = สว่าง light = ธีมสว่าง
syntax-dark = โทนสีมืด syntax-dark = โทนสีมืด
syntax-light = โทนสีสว่าง syntax-light = โทนสีสว่าง
default-zoom-step = ระยะซูม default-zoom-step = ระยะซูม
@ -109,8 +109,3 @@ pane-toggle-maximize = ขยายให้ใหญ่สุด
menu-color-schemes = โทนสี... menu-color-schemes = โทนสี...
menu-settings = การตั้งค่า... menu-settings = การตั้งค่า...
menu-about = เกี่ยวกับ เทอร์มินัล COSMIC... menu-about = เกี่ยวกับ เทอร์มินัล COSMIC...
cancel = ยกเลิก
reset-to-default = รีเซ็ตเป็นค่าเริ่มต้น
type-to-search = พิมพ์เพื่อค้นหา
add-another-keybinding = เพิ่มคีย์ลัดเพิ่มเติม
replace = แทนที่

View file

@ -38,7 +38,7 @@ settings = Налаштування
appearance = Вигляд appearance = Вигляд
theme = Тема theme = Тема
match-desktop = Підігнати під стільницю match-desktop = Системна
dark = Темна dark = Темна
light = Світла light = Світла
syntax-dark = Темна колірна схема syntax-dark = Темна колірна схема
@ -84,7 +84,7 @@ new-window = Нове вікно
profile = Профіль profile = Профіль
menu-profiles = Профілі... menu-profiles = Профілі...
close-tab = Закрити вкладку close-tab = Закрити вкладку
quit = Завершити quit = Вийти
## Edit ## Edit
@ -147,9 +147,5 @@ focus-pane-up = Фокус на верхню панель
comment = Емулятор терміналу для середовища COSMIC comment = Емулятор терміналу для середовища COSMIC
keywords = Команда;Оболонка;Термінал;Інтерфейс;CLI; keywords = Команда;Оболонка;Термінал;Інтерфейс;CLI;
copy-link = Копіювати ланку copy-link = Копіювати ланку
tab-new-inherit-working-directory = Нові вкладки та вікна відкривати в поточному каталозі tab-new-inherit-working-directory = У нових вкладках використовується поточний каталог
tab-new-inherit-working-directory-description = Відкривати нові вкладки та вікна в робочому каталозі активної вкладки 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 = 上一个标签 previous-tab = 上一个标签
split-horizontal = 水平分割 split-horizontal = 水平分割
split-vertical = 垂直分割 split-vertical = 垂直分割
pane-toggle-maximize = 将窗格最大化或还原 pane-toggle-maximize = 切换最大化
menu-color-schemes = 配色方案… menu-color-schemes = 配色方案…
menu-settings = 设置… menu-settings = 设置…
menu-about = 关于 COSMIC 终端… menu-about = 关于 COSMIC 终端…
@ -149,7 +149,3 @@ keywords = 指令;外壳;终端;命令行界面;
copy-link = 复制链接 copy-link = 复制链接
tab-new-inherit-working-directory = 新标签和窗口使用当前目录 tab-new-inherit-working-directory = 新标签和窗口使用当前目录
tab-new-inherit-working-directory-description = 在活动标签的工作目录中打开新标签和窗口 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
color-schemes = 配色方案 color-schemes = 配色方案
rename = 重新命名 rename = 重命名
export = 匯出 export = 匯出
delete = 刪除 delete = 刪除
import = 匯入 import = 匯入
@ -24,7 +24,7 @@ command-line = 命令列
tab-title = 分頁標題 tab-title = 分頁標題
tab-title-description = 覆寫預設分頁標題 tab-title-description = 覆寫預設分頁標題
add-profile = 新增設定檔 add-profile = 新增設定檔
new-profile = 新增設定檔 new-profile = 新建設定檔
make-default = 設為預設值 make-default = 設為預設值
working-directory = 工作目錄 working-directory = 工作目錄
hold = 保留 hold = 保留
@ -79,12 +79,12 @@ find-next = 下一個
## File ## File
file = 檔案 file = 檔案
new-tab = 新增分頁 new-tab = 新建分頁
new-window = 新增視窗 new-window = 新建視窗
profile = 設定檔案 profile = 設定檔案
menu-profiles = 設定檔案... menu-profiles = 設定檔案...
close-tab = 關閉分頁 close-tab = 關閉分頁
quit = 結束 quit = 退出
## Edit ## Edit
@ -104,16 +104,16 @@ next-tab = 下個分頁
previous-tab = 上個分頁 previous-tab = 上個分頁
split-horizontal = 水平分割 split-horizontal = 水平分割
split-vertical = 垂直分割 split-vertical = 垂直分割
pane-toggle-maximize = 將窗格最大化或還原 pane-toggle-maximize = 切換最大化
menu-color-schemes = 配色方案... menu-color-schemes = 配色方案...
menu-settings = 設定... menu-settings = 設定...
menu-about = 關於 COSMIC 終端機… menu-about = 關於 COSMIC 終端機…
cancel = 取消 cancel = 取消
repository = 軟體庫 repository = 軟體庫源
support = 支援 support = 支援
keyboard-shortcuts = 鍵盤快速鍵 keyboard-shortcuts = 鍵盤快速鍵
keywords = 指令;殼層;終端;指令行介面CLI; keywords = 指令;殼層;終端;指令行介面CLI;
add-another-keybinding = 新增另一個鍵盤快速鍵 add-another-keybinding = 添加另一個按鍵組合
close-window = 關閉視窗 close-window = 關閉視窗
copy-or-sigint = 複製或 SIGINT copy-or-sigint = 複製或 SIGINT
disable = 停用 disable = 停用
@ -136,7 +136,7 @@ focus-pane-up = 聚焦上方窗格
open-link = 開啟連結 open-link = 開啟連結
menu-password-manager = 密碼… menu-password-manager = 密碼…
passwords-title = 密碼 passwords-title = 密碼
add-password = 新增密碼 add-password = 添增密碼
password-input = 密碼 password-input = 密碼
password-input-description = 描述 password-input-description = 描述
shortcut-replace-title = 取代快速鍵? shortcut-replace-title = 取代快速鍵?
@ -149,7 +149,3 @@ clear-scrollback = 清除捲動回朔
comment = COSMIC 桌面終端機模擬器 comment = COSMIC 桌面終端機模擬器
tab-new-inherit-working-directory = 新分頁和視窗使用目前目錄 tab-new-inherit-working-directory = 新分頁和視窗使用目前目錄
tab-new-inherit-working-directory-description = 在作用中分頁的工作目錄開啟新分頁和視窗 tab-new-inherit-working-directory-description = 在作用中分頁的工作目錄開啟新分頁和視窗
show-pane-borders = 顯示窗格邊框
window-header = 視窗標題列
move-tab-left = 將分頁向左移動
move-tab-right = 將分頁向右移動

View file

@ -231,34 +231,17 @@ pub struct Config {
pub opacity: u8, pub opacity: u8,
pub profiles: BTreeMap<ProfileId, Profile>, pub profiles: BTreeMap<ProfileId, Profile>,
pub show_headerbar: bool, 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 use_bright_bold: bool,
pub syntax_theme_dark: String, pub syntax_theme_dark: String,
pub syntax_theme_light: String, pub syntax_theme_light: String,
pub focus_follow_mouse: bool, pub focus_follow_mouse: bool,
#[serde(default)] #[serde(default)]
pub tab_new_inherit_working_directory: bool, 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>, pub default_profile: Option<ProfileId>,
#[serde(default)] #[serde(default)]
pub shortcuts_custom: Shortcuts, 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 { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -269,7 +252,6 @@ impl Default for Config {
dim_font_weight: Weight::NORMAL.0, dim_font_weight: Weight::NORMAL.0,
focus_follow_mouse: false, focus_follow_mouse: false,
tab_new_inherit_working_directory: false, tab_new_inherit_working_directory: false,
copy_on_select_clipboard: false,
font_name: "Noto Sans Mono".to_string(), font_name: "Noto Sans Mono".to_string(),
font_size: 14, font_size: 14,
font_size_zoom_step_mul_100: 100, font_size_zoom_step_mul_100: 100,
@ -278,8 +260,6 @@ impl Default for Config {
opacity: 100, opacity: 100,
profiles: BTreeMap::new(), profiles: BTreeMap::new(),
show_headerbar: true, show_headerbar: true,
show_pane_borders: false,
unfocused_pane_dim: default_unfocused_pane_dim(),
syntax_theme_dark: COSMIC_THEME_DARK.to_string(), syntax_theme_dark: COSMIC_THEME_DARK.to_string(),
syntax_theme_light: COSMIC_THEME_LIGHT.to_string(), syntax_theme_light: COSMIC_THEME_LIGHT.to_string(),
use_bright_bold: false, use_bright_bold: false,
@ -361,12 +341,6 @@ impl Config {
Metrics::new(font_size, line_height) 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 { pub fn opacity_ratio(&self) -> f32 {
f32::from(self.opacity) / 100.0 f32::from(self.opacity) / 100.0
} }
@ -428,22 +402,3 @@ impl Config {
})[&self.font_stretch] })[&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)
);
}
}

View file

@ -1,185 +0,0 @@
// 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('>', "›")
}

View file

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

View file

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

View file

@ -27,15 +27,13 @@ use indexmap::IndexSet;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::HashMap, collections::HashMap,
fs, fs, io, mem,
hash::{DefaultHasher, Hash, Hasher},
io, mem,
path::PathBuf, path::PathBuf,
sync::{ sync::{
Arc, Mutex, Weak, Arc, Mutex, Weak,
atomic::{AtomicU32, Ordering}, atomic::{AtomicU32, Ordering},
}, },
time::{Duration, Instant}, time::Instant,
}; };
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -45,7 +43,6 @@ use crate::{
config::{ColorSchemeKind, Config as AppConfig, ProfileId}, config::{ColorSchemeKind, Config as AppConfig, ProfileId},
menu::MenuState, menu::MenuState,
mouse_reporter::MouseReporter, mouse_reporter::MouseReporter,
osc_tap::OscEvent,
}; };
/// Minimum contrast between a fixed cursor color and the cell's background. /// Minimum contrast between a fixed cursor color and the cell's background.
@ -68,28 +65,6 @@ pub enum MonitorKind {
/// Seconds without output before a silence alert fires (Konsole default). /// Seconds without output before a silence alert fires (Konsole default).
pub const MONITOR_SILENCE_SECONDS: u64 = 10; 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 /// https://github.com/alacritty/alacritty/blob/4a7728bf7fac06a35f27f6c4f31e0d9214e5152b/alacritty/src/config/ui_config.rs#L36-L39
fn url_regex_search() -> RegexSearch { fn url_regex_search() -> RegexSearch {
let url_regex = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)\ let url_regex = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)\
@ -134,13 +109,13 @@ impl From<Size> for WindowSize {
pub struct EventProxy( pub struct EventProxy(
pane_grid::Pane, pane_grid::Pane,
segmented_button::Entity, segmented_button::Entity,
mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>, mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, Event)>,
); );
impl EventListener for EventProxy { impl EventListener for EventProxy {
fn send_event(&self, event: Event) { fn send_event(&self, event: Event) {
//TODO: handle error //TODO: handle error
let _ = self.2.send((self.0, self.1, PtyEvent::Term(event))); let _ = self.2.send((self.0, self.1, event));
} }
} }
@ -221,63 +196,34 @@ impl TerminalPaneGrid {
self.focus self.focus
} }
pub fn update_terminal_focus(&mut self) { pub fn update_terminal_focus(&self) {
let focus = self.focus; for (pane, tab_model) in self.panes.panes.iter() {
for (pane, tab_model) in self.panes.panes.iter_mut() { let entity = tab_model.active();
let active = tab_model.active(); if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let entities: Vec<segmented_button::Entity> = tab_model.iter().collect(); let mut terminal = terminal.lock().unwrap();
for entity in entities { terminal.set_focused(self.focus == *pane);
let mut icon_update = None; terminal.update();
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
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) { pub fn unfocus_all_terminals(&self) {
for tab_model in self.panes.panes.values() { for (_pane, tab_model) in self.panes.panes.iter() {
let active = tab_model.active();
for entity in tab_model.iter() { for entity in tab_model.iter() {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) { if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap(); let mut terminal = terminal.lock().unwrap();
terminal.set_focused(false); terminal.set_focused(false);
if entity == active { terminal.update();
terminal.update();
}
} }
} }
} }
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Metadata { pub struct Metadata {
pub bg: cosmic_text::Color, pub bg: cosmic_text::Color,
pub underline_color: cosmic_text::Color, pub underline_color: cosmic_text::Color,
pub flags: Flags, 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 { impl Metadata {
@ -287,8 +233,6 @@ impl Metadata {
bg, bg,
underline_color, underline_color,
flags, flags,
block: None,
block_color: underline_color,
} }
} }
@ -302,73 +246,6 @@ impl Metadata {
fn with_flags(self, flags: Flags) -> Self { fn with_flags(self, flags: Flags) -> Self {
Self { 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 { pub struct Terminal {
@ -380,11 +257,6 @@ pub struct Terminal {
pub monitor_alert: Option<MonitorKind>, pub monitor_alert: Option<MonitorKind>,
pub monitor_last_output: Instant, pub monitor_last_output: Instant,
pub monitor_job_running: bool, 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 needs_update: bool,
pub profile_id_opt: Option<ProfileId>, pub profile_id_opt: Option<ProfileId>,
pub tab_title_override: Option<String>, pub tab_title_override: Option<String>,
@ -415,7 +287,7 @@ impl Terminal {
pub fn new( pub fn new(
pane: pane_grid::Pane, pane: pane_grid::Pane,
entity: segmented_button::Entity, entity: segmented_button::Entity,
event_tx: mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>, event_tx: mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, Event)>,
config: Config, config: Config,
options: Options, options: Options,
app_config: &AppConfig, app_config: &AppConfig,
@ -467,8 +339,6 @@ impl Terminal {
cell_width, cell_width,
cell_height, cell_height,
}; };
#[cfg(unix)]
let osc_tx = event_tx.clone();
let event_proxy = EventProxy(pane, entity, event_tx); let event_proxy = EventProxy(pane, entity, event_tx);
let term = Arc::new(FairMutex::new(Term::new( let term = Arc::new(FairMutex::new(Term::new(
config, config,
@ -483,15 +353,6 @@ impl Terminal {
#[cfg(windows)] #[cfg(windows)]
let shell_pid = pty.child_watcher().pid().map(|pid| pid.get()); 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 = let pty_event_loop =
EventLoop::new(term.clone(), event_proxy, pty, options.drain_on_exit, false)?; EventLoop::new(term.clone(), event_proxy, pty, options.drain_on_exit, false)?;
let notifier = Notifier(pty_event_loop.channel()); let notifier = Notifier(pty_event_loop.channel());
@ -515,9 +376,6 @@ impl Terminal {
monitor_alert: None, monitor_alert: None,
monitor_last_output: Instant::now(), monitor_last_output: Instant::now(),
monitor_job_running: false, monitor_job_running: false,
attention: None,
notify_last: None,
read_only: false,
mouse_reporter: Default::default(), mouse_reporter: Default::default(),
needs_update: true, needs_update: true,
notifier, notifier,
@ -581,25 +439,6 @@ impl Terminal {
self.zoom_adj = value; 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) { fn set_focused(&mut self, is_focused: bool) {
let focus_changed = self.is_focused != is_focused; let focus_changed = self.is_focused != is_focused;
self.is_focused = is_focused; self.is_focused = is_focused;
@ -611,7 +450,7 @@ impl Terminal {
const FOCUS_OUT: &[u8] = b"\x1b[O"; const FOCUS_OUT: &[u8] = b"\x1b[O";
let input = if is_focused { FOCUS_IN } else { FOCUS_OUT }; let input = if is_focused { FOCUS_IN } else { FOCUS_OUT };
self.respond(input); self.input_no_scroll(input);
} }
} }
} }
@ -624,18 +463,7 @@ impl Terminal {
self.with_buffer_mut(|buffer| buffer.set_redraw(redraw)); 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) { 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); self.notifier.notify(input);
} }
@ -653,10 +481,6 @@ impl Terminal {
} }
pub fn input_scroll<I: Into<Cow<'static, [u8]>>>(&self, input: I) { 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.input_no_scroll(input);
self.scroll(TerminalScroll::Bottom); self.scroll(TerminalScroll::Bottom);
} }
@ -1082,14 +906,9 @@ impl Terminal {
} }
let start = text.len(); 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 { text.push(match indexed.cell.c {
'\t' => ' ', '\t' => ' ',
c if block.is_some() => ' ',
c => c, c => c,
}); });
if let Some(zerowidth) = indexed.cell.zerowidth() { if let Some(zerowidth) = indexed.cell.zerowidth() {
@ -1207,7 +1026,6 @@ impl Terminal {
} }
let metadata = Metadata::new(bg, fg) let metadata = Metadata::new(bg, fg)
.with_block(block, fg)
.with_flags(flags) .with_flags(flags)
.with_underline_color(underline_color); .with_underline_color(underline_color);
let (meta_idx, _) = self.metadata_set.insert_full(metadata); let (meta_idx, _) = self.metadata_set.insert_full(metadata);
@ -1318,11 +1136,8 @@ impl Terminal {
y, y,
); );
// Saisie de l'utilisateur : rien en lecture seule for code in codes {
if !self.read_only { self.notifier.notify(code);
for code in codes {
self.notifier.notify(code);
}
} }
} else { } else {
self.scroll_as_arrows(delta); self.scroll_as_arrows(delta);
@ -1530,108 +1345,3 @@ 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,7 +12,6 @@ use cosmic::{
cosmic_theme::palette::{WithAlpha, blend::Compose}, cosmic_theme::palette::{WithAlpha, blend::Compose},
iced::core::{ iced::core::{
Border, Shell, Border, Shell,
border::Radius,
clipboard::Clipboard, clipboard::Clipboard,
input_method::{self, InputMethod}, input_method::{self, InputMethod},
keyboard::key::Named, keyboard::key::Named,
@ -46,11 +45,7 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use crate::{ use crate::{Action, Terminal, TerminalScroll, menu::MenuState, terminal::Metadata};
Action, Terminal, TerminalScroll, kitty_keyboard,
menu::MenuState,
terminal::{BlockFill, Metadata},
};
const AUTOSCROLL_INTERVAL: Duration = Duration::from_millis(100); const AUTOSCROLL_INTERVAL: Duration = Duration::from_millis(100);
@ -114,16 +109,13 @@ pub struct TerminalBox<'a, Message> {
border: Border, border: Border,
padding: Padding, padding: Padding,
show_headerbar: bool, show_headerbar: bool,
pane_border_radius: Option<Radius>,
click_timing: Duration, click_timing: Duration,
context_menu_open: bool, context_menu: Option<Point>,
on_context_menu: Option<Box<dyn Fn(Option<MenuState>) -> Message + 'a>>, on_context_menu: Option<Box<dyn Fn(Option<MenuState>) -> Message + 'a>>,
on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>, on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>,
opacity: Option<f32>, opacity: Option<f32>,
dim: Option<f32>,
mouse_inside_boundary: Option<bool>, mouse_inside_boundary: Option<bool>,
on_middle_click: Option<Box<dyn Fn() -> Message + 'a>>, 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_open_hyperlink: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_window_focused: Option<Box<dyn Fn() -> Message + 'a>>, on_window_focused: Option<Box<dyn Fn() -> Message + 'a>>,
on_window_unfocused: Option<Box<dyn Fn() -> Message + 'a>>, on_window_unfocused: Option<Box<dyn Fn() -> Message + 'a>>,
@ -143,16 +135,13 @@ where
border: Border::default(), border: Border::default(),
padding: Padding::new(0.0), padding: Padding::new(0.0),
show_headerbar: true, show_headerbar: true,
pane_border_radius: None,
click_timing: Duration::from_millis(500), click_timing: Duration::from_millis(500),
context_menu_open: false, context_menu: None,
on_context_menu: None, on_context_menu: None,
on_mouse_enter: None, on_mouse_enter: None,
opacity: None, opacity: None,
dim: None,
mouse_inside_boundary: None, mouse_inside_boundary: None,
on_middle_click: None, on_middle_click: None,
on_selection_done: None,
key_binds, key_binds,
on_open_hyperlink: None, on_open_hyperlink: None,
on_window_focused: None, on_window_focused: None,
@ -182,18 +171,13 @@ where
self 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 { pub fn click_timing(mut self, click_timing: Duration) -> Self {
self.click_timing = click_timing; self.click_timing = click_timing;
self self
} }
pub fn context_menu_open(mut self, open: bool) -> Self { pub fn context_menu(mut self, position: Point) -> Self {
self.context_menu_open = open; self.context_menu = Some(position);
self self
} }
@ -210,14 +194,6 @@ where
self 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 { 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.on_middle_click = Some(Box::new(on_middle_click));
self self
@ -228,13 +204,6 @@ where
self 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 { pub fn sharp_corners(mut self, sharp_corners: bool) -> Self {
self.sharp_corners = sharp_corners; self.sharp_corners = sharp_corners;
self self
@ -272,7 +241,7 @@ where
if !state.is_focused { if !state.is_focused {
return InputMethod::Disabled; return InputMethod::Disabled;
} }
if self.context_menu_open { if self.context_menu.is_some() {
return InputMethod::Disabled; return InputMethod::Disabled;
} }
@ -440,22 +409,13 @@ where
let state = tree.state.downcast_ref::<State>(); let state = tree.state.downcast_ref::<State>();
let cosmic_theme = theme.cosmic(); let cosmic_theme = theme.cosmic();
let corner_radius = if let Some(r) = self.pane_border_radius { // matches the corners to the window border
r.into() let corner_radius = if self.sharp_corners {
} else if self.sharp_corners {
// matches the corners to the window border
cosmic_theme.radius_0() cosmic_theme.radius_0()
} else { } else {
cosmic_theme cosmic_theme.radius_s()
.radius_s() }
.map(|x| if x < 4.0 { x - 1.0 } else { x + 3.0 }) .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 scrollbar_w = f32::from(cosmic_theme.spacing.space_xxs);
let view_position = layout.position() + [self.padding.left, self.padding.top].into(); let view_position = layout.position() + [self.padding.left, self.padding.top].into();
@ -469,11 +429,16 @@ where
// Pane too small for content, but still fill background // Pane too small for content, but still fill background
let terminal = self.terminal.lock().unwrap(); let terminal = self.terminal.lock().unwrap();
let meta = &terminal.metadata_set[terminal.default_attrs().metadata]; let meta = &terminal.metadata_set[terminal.default_attrs().metadata];
let background_color = shade(meta.bg, state.is_focused && !self.disabled);
renderer.fill_quad( renderer.fill_quad(
Quad { Quad {
bounds: layout.bounds(), bounds: layout.bounds(),
border: Border { border: Border {
radius: border_radius, radius: if self.show_headerbar {
[0.0, 0.0, corner_radius[2], corner_radius[3]].into()
} else {
corner_radius.into()
},
width: self.border.width, width: self.border.width,
color: self.border.color, color: self.border.color,
}, },
@ -481,12 +446,12 @@ where
..Default::default() ..Default::default()
}, },
Color::from_rgba( Color::from_rgba(
f32::from(meta.bg.r()) / 255.0, f32::from(background_color.r()) / 255.0,
f32::from(meta.bg.g()) / 255.0, f32::from(background_color.g()) / 255.0,
f32::from(meta.bg.b()) / 255.0, f32::from(background_color.b()) / 255.0,
match self.opacity { match self.opacity {
Some(opacity) => opacity, Some(opacity) => opacity,
None => f32::from(meta.bg.a()) / 255.0, None => f32::from(background_color.a()) / 255.0,
}, },
), ),
); );
@ -507,11 +472,17 @@ where
// Render default background // Render default background
{ {
let meta = &terminal.metadata_set[terminal.default_attrs().metadata]; let meta = &terminal.metadata_set[terminal.default_attrs().metadata];
let background_color = shade(meta.bg, state.is_focused && !self.disabled);
renderer.fill_quad( renderer.fill_quad(
Quad { Quad {
bounds: layout.bounds(), bounds: layout.bounds(),
border: Border { border: Border {
radius: border_radius, radius: if self.show_headerbar {
[0.0, 0.0, corner_radius[2], corner_radius[3]].into()
} else {
corner_radius.into()
},
width: self.border.width, width: self.border.width,
color: self.border.color, color: self.border.color,
}, },
@ -519,12 +490,12 @@ where
..Default::default() ..Default::default()
}, },
Color::from_rgba( Color::from_rgba(
f32::from(meta.bg.r()) / 255.0, f32::from(background_color.r()) / 255.0,
f32::from(meta.bg.g()) / 255.0, f32::from(background_color.g()) / 255.0,
f32::from(meta.bg.b()) / 255.0, f32::from(background_color.b()) / 255.0,
match self.opacity { match self.opacity {
Some(opacity) => opacity, Some(opacity) => opacity,
None => f32::from(meta.bg.a()) / 255.0, None => f32::from(background_color.a()) / 255.0,
}, },
), ),
); );
@ -566,7 +537,7 @@ where
fn fill<Renderer: renderer::Renderer>( fn fill<Renderer: renderer::Renderer>(
&mut self, &mut self,
renderer: &mut Renderer, renderer: &mut Renderer,
_is_focused: bool, is_focused: bool,
) { ) {
let cosmic_text_to_iced_color = |color: cosmic_text::Color| { let cosmic_text_to_iced_color = |color: cosmic_text::Color| {
Color::from_rgba( Color::from_rgba(
@ -580,7 +551,7 @@ where
macro_rules! mk_pos_offset { macro_rules! mk_pos_offset {
($x_offset:expr, $bottom_offset:expr) => { ($x_offset:expr, $bottom_offset:expr) => {
Vector::new( Vector::new(
(self.start_x + $x_offset).floor(), self.start_x + $x_offset,
self.line_top + self.line_height - $bottom_offset, self.line_top + self.line_height - $bottom_offset,
) )
}; };
@ -597,57 +568,19 @@ where
} }
}; };
($pos_offset:expr, $style_line_height:expr) => { ($pos_offset:expr, $style_line_height:expr) => {
mk_quad!( mk_quad!($pos_offset, $style_line_height, self.end_x - self.start_x)
$pos_offset,
$style_line_height,
(self.end_x - self.start_x).ceil()
)
}; };
} }
let metadata = &self.metadata_set[self.metadata]; let metadata = &self.metadata_set[self.metadata];
if metadata.bg != self.metadata_set[self.default_metadata].bg { if metadata.bg != self.metadata_set[self.default_metadata].bg {
let color = shade(metadata.bg, is_focused);
renderer.fill_quad( renderer.fill_quad(
mk_quad!(mk_pos_offset!(0.0, self.line_height), self.line_height), mk_quad!(mk_pos_offset!(0.0, self.line_height), self.line_height),
cosmic_text_to_iced_color(metadata.bg), cosmic_text_to_iced_color(color),
); );
} }
// É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() { if !metadata.flags.is_empty() {
let style_line_height = (self.glyph_font_size / 10.0).clamp(1.0, 16.0); let style_line_height = (self.glyph_font_size / 10.0).clamp(1.0, 16.0);
@ -823,7 +756,7 @@ where
let scrollbar_color: Color = if pressed { let scrollbar_color: Color = if pressed {
// pressed_state_color, 0.5 // pressed_state_color, 0.5
cosmic_theme cosmic_theme
.background(theme.transparent) .background(false)
.component .component
.pressed .pressed
.without_alpha() .without_alpha()
@ -833,7 +766,7 @@ where
} else if hover { } else if hover {
// hover_state_color, 0.2 // hover_state_color, 0.2
cosmic_theme cosmic_theme
.background(theme.transparent) .background(false)
.component .component
.hover .hover
.without_alpha() .without_alpha()
@ -934,32 +867,6 @@ 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(); let duration = instant.elapsed();
log::trace!("redraw {}, {}: {:?}", view_w, view_h, duration); log::trace!("redraw {}, {}: {:?}", view_w, view_h, duration);
} }
@ -984,11 +891,7 @@ where
let buffer_size = terminal.with_buffer(|buffer| buffer.size()); let buffer_size = terminal.with_buffer(|buffer| buffer.size());
let is_app_cursor = terminal.term.lock().mode().contains(TermMode::APP_CURSOR); let is_app_cursor = terminal.term.lock().mode().contains(TermMode::APP_CURSOR);
// [yoda] En lecture seule, la souris revient au terminal : sélection et let is_mouse_mode = terminal.term.lock().mode().intersects(TermMode::MOUSE_MODE);
// 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 { match event {
Event::Window(event) => match event { Event::Window(event) => match event {
cosmic::iced::window::Event::Focused => { cosmic::iced::window::Event::Focused => {
@ -1028,101 +931,6 @@ 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 { Event::Keyboard(KeyEvent::KeyPressed {
key: Key::Named(named), key: Key::Named(named),
physical_key, physical_key,
@ -1238,15 +1046,7 @@ where
shell.capture_event(); shell.capture_event();
} }
Named::Enter => { Named::Enter => {
// [yoda] Shift+Entrée envoie LF (`\n`, comme Ctrl+J) : saut de terminal.input_scroll(format!("{}{}", alt_prefix, "\x0D").into_bytes());
// 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(); shell.capture_event();
} }
Named::Escape => { Named::Escape => {
@ -1544,11 +1344,11 @@ where
} }
// Update context menu state // Update context menu state
if let Some(on_context_menu) = &self.on_context_menu { if let Some(on_context_menu) = &self.on_context_menu {
match self.context_menu_open { match self.context_menu {
true => { Some(_) => {
shell.publish(on_context_menu(None)); shell.publish(on_context_menu(None));
} }
false => { None => {
if *button == Button::Right { if *button == Button::Right {
let x = p.x - self.padding.left; let x = p.x - self.padding.left;
let y = p.y - self.padding.top; let y = p.y - self.padding.top;
@ -1566,13 +1366,23 @@ where
None, None,
); );
let link = get_hyperlink(&terminal, location); let link = get_hyperlink(&terminal, location);
shell.publish(on_context_menu(Some(MenuState { link }))); 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.capture_event(); shell.capture_event();
} }
} else {
state.is_focused = false;
} }
} }
Event::Mouse(MouseEvent::ButtonReleased(Button::Left)) => { Event::Mouse(MouseEvent::ButtonReleased(Button::Left)) => {
@ -1591,9 +1401,6 @@ where
} }
} }
terminal.needs_update = true; 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()) { if let Some(p) = cursor_position.position_in(layout.bounds()) {
let x = p.x - self.padding.left; let x = p.x - self.padding.left;
@ -1801,10 +1608,6 @@ where
} }
_ => (), _ => (),
} }
if terminal.needs_update {
shell.request_redraw();
}
} }
} }
@ -1951,6 +1754,20 @@ fn accumulate_wheel_lines(y: f32, accumulator: f32) -> (i32, f32) {
(lines, total - lines as 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> impl<'a, Message> From<TerminalBox<'a, Message>> for Element<'a, Message, cosmic::Theme, Renderer>
where where
Message: Clone + 'a, Message: Clone + 'a,