Compare commits
61 commits
pre-1.6-me
...
local/yoda
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a614b42a7 | |||
| d4ea340be0 | |||
|
|
3bfca1eb80 | ||
|
|
3bf0b8e469 | ||
|
|
16a9ef5a23 | ||
|
|
3ef31d750d | ||
| 9b10e98ab1 | |||
| a035b47e98 | |||
| db9ee02d8d | |||
| 52b37cc2f8 | |||
| 2414aaece3 | |||
|
|
da80dcd744 | ||
|
|
5139e36f56 | ||
|
|
487d890e7e | ||
|
|
003a9b3b06 | ||
| 81dc7d3eeb | |||
| 083e09f97e | |||
|
|
9129277bed | ||
|
|
78eaba9db1 | ||
|
|
d25c2108c0 | ||
|
|
120b4fdb12 | ||
|
|
4cd8f8d605 | ||
|
|
1d9e5784d2 | ||
|
|
327febcc92 | ||
| 99842bde03 | |||
| 5fc081e257 | |||
| b8fce5a272 | |||
|
|
4f0f0b6e4e | ||
|
|
7945b9ffc6 | ||
| 41203ab5f1 | |||
| f3f87e59da | |||
|
|
909a0d31aa | ||
|
|
7df9265243 | ||
|
|
dfa59f3ca4 | ||
| 78fb2be076 | |||
| eda2fb1d58 | |||
|
|
032a10796b | ||
|
|
f8ccd51d4a | ||
|
|
ad9a5bcc40 | ||
|
|
67b00d6392 | ||
|
|
c6ab857444 | ||
|
|
ba9c1aa69c | ||
|
|
5095059f90 | ||
|
|
69cdcbd1c1 | ||
|
|
7daf10e3b5 | ||
|
|
6bf0c65ca9 | ||
|
|
543cbb0cb8 | ||
|
|
33c3869c40 | ||
|
|
e28eebbfb2 | ||
|
|
17678baf55 | ||
|
|
5db18f3fac | ||
|
|
117644e9c1 | ||
|
|
01af42aff5 | ||
|
|
44d042f45d | ||
|
|
d05ed099b6 | ||
|
|
46f8b1a9cf | ||
|
|
1019e04b82 | ||
|
|
a43b5ae500 | ||
|
|
10ca3f1025 | ||
|
|
e3b1cda8ab | ||
|
|
6bd07a6363 |
49 changed files with 3943 additions and 1463 deletions
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,8 +1,10 @@
|
||||||
- [ ] I have disclosed use of any AI generated code in my commit messages.
|
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.
|
||||||
- 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).
|
||||||
|
|
|
||||||
14
.github/workflows/require-checklist.yml
vendored
Normal file
14
.github/workflows/require-checklist.yml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# From https://github.com/mheap/require-checklist-action, license MIT
|
||||||
|
name: Require Checklist
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, edited, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: mheap/require-checklist-action@v2
|
||||||
|
with:
|
||||||
|
requireChecklist: true
|
||||||
1856
Cargo.lock
generated
1856
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
16
Cargo.toml
16
Cargo.toml
|
|
@ -1,10 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-term"
|
name = "cosmic-term"
|
||||||
version = "1.3.0"
|
version = "1.10.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.90"
|
rust-version = "1.93"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
alacritty_terminal = "0.25.1"
|
alacritty_terminal = "0.25.1"
|
||||||
|
|
@ -18,6 +18,9 @@ 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 }
|
||||||
|
|
@ -51,7 +54,14 @@ 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 = ["about", "autosize", "multi-window", "tokio", "winit", "surface-message"]
|
features = [
|
||||||
|
"about",
|
||||||
|
"autosize",
|
||||||
|
"multi-window",
|
||||||
|
"tokio",
|
||||||
|
"winit",
|
||||||
|
"surface-message",
|
||||||
|
]
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
fork = "0.4"
|
fork = "0.4"
|
||||||
|
|
|
||||||
70
cargo.just
Normal file
70
cargo.just
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
|
||||||
|
|
||||||
|
# Compile with debug profile
|
||||||
|
[no-cd]
|
||||||
|
build-debug *args:
|
||||||
|
cargo build {{args}}
|
||||||
|
|
||||||
|
# Compile with release profile
|
||||||
|
[no-cd]
|
||||||
|
build-release *args: (build-debug '--release' args)
|
||||||
|
|
||||||
|
# Compile with a vendored tarball
|
||||||
|
[no-cd]
|
||||||
|
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
|
||||||
|
|
||||||
|
# Check for errors and linter warnings
|
||||||
|
[no-cd]
|
||||||
|
check *args:
|
||||||
|
cargo clippy --all-features {{args}} -- -W clippy::pedantic
|
||||||
|
|
||||||
|
# Runs a check with JSON message format for IDE integration
|
||||||
|
[no-cd]
|
||||||
|
check-json: (check '--message-format=json')
|
||||||
|
|
||||||
|
# Remove Cargo build artifacts
|
||||||
|
[no-cd]
|
||||||
|
clean:
|
||||||
|
cargo clean
|
||||||
|
|
||||||
|
# Also remove .cargo and vendored dependencies
|
||||||
|
[no-cd]
|
||||||
|
clean-dist: clean
|
||||||
|
rm -rf .cargo vendor vendor.tar target
|
||||||
|
|
||||||
|
# Run the application for testing purposes
|
||||||
|
[no-cd]
|
||||||
|
run *args:
|
||||||
|
cargo run {{args}}
|
||||||
|
|
||||||
|
# Run `cargo test`
|
||||||
|
[no-cd]
|
||||||
|
test *args:
|
||||||
|
cargo test {{args}}
|
||||||
|
|
||||||
|
# Vendor Cargo dependencies locally
|
||||||
|
[no-cd]
|
||||||
|
vendor:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
mkdir -p .cargo
|
||||||
|
cargo vendor --locked | head -n -1 > .cargo/config.toml
|
||||||
|
echo 'directory = "vendor"' >> .cargo/config.toml
|
||||||
|
echo >> .cargo/config.toml
|
||||||
|
echo '[env]' >> .cargo/config.toml
|
||||||
|
if [ -n "${SOURCE_DATE_EPOCH}" ]
|
||||||
|
then
|
||||||
|
source_date="$(date -d "@${SOURCE_DATE_EPOCH}" "+%Y-%m-%d")"
|
||||||
|
echo "VERGEN_GIT_COMMIT_DATE = \"${source_date}\"" >> .cargo/config.toml
|
||||||
|
fi
|
||||||
|
if [ -n "${SOURCE_GIT_HASH}" ]
|
||||||
|
then
|
||||||
|
echo "VERGEN_GIT_SHA = \"${SOURCE_GIT_HASH}\"" >> .cargo/config.toml
|
||||||
|
fi
|
||||||
|
tar pcf vendor.tar .cargo vendor
|
||||||
|
rm -rf .cargo vendor
|
||||||
|
|
||||||
|
# Extracts vendored dependencies
|
||||||
|
[no-cd]
|
||||||
|
vendor-extract:
|
||||||
|
rm -rf vendor
|
||||||
|
tar pxf vendor.tar
|
||||||
48
debian/changelog
vendored
48
debian/changelog
vendored
|
|
@ -1,3 +1,51 @@
|
||||||
|
cosmic-term (1.10.0) noble; urgency=medium
|
||||||
|
|
||||||
|
* Epoch 1.10.0 version update
|
||||||
|
|
||||||
|
-- Michael Murphy <michael@mmurphy.dev> Mon, 05 Oct 2026 21:28:17 +0200
|
||||||
|
|
||||||
|
cosmic-term (1.9.0) noble; urgency=medium
|
||||||
|
|
||||||
|
* Epoch 1.9.0 version update
|
||||||
|
|
||||||
|
-- Michael Murphy <michael@mmurphy.dev> Mon, 14 Sep 2026 19:50:47 +0200
|
||||||
|
|
||||||
|
cosmic-term (1.8.0) noble; urgency=medium
|
||||||
|
|
||||||
|
* Epoch 1.8.0 version update
|
||||||
|
|
||||||
|
-- Jeremy Soller <jeremy@system76.com> Wed, 02 Sep 2026 08:15:55 -0600
|
||||||
|
|
||||||
|
cosmic-term (1.7.0) noble; urgency=medium
|
||||||
|
|
||||||
|
* Epoch 1.7.0 version update
|
||||||
|
|
||||||
|
-- Jeremy Soller <jeremy@system76.com> Tue, 25 Aug 2026 09:21:20 -0600
|
||||||
|
|
||||||
|
cosmic-term (1.6.0) noble; urgency=medium
|
||||||
|
|
||||||
|
* Epoch 1.6.0 version update
|
||||||
|
|
||||||
|
-- Jeremy Soller <jeremy@system76.com> Tue, 18 Aug 2026 07:15:01 -0600
|
||||||
|
|
||||||
|
cosmic-term (1.5.0) noble; urgency=medium
|
||||||
|
|
||||||
|
* Epoch 1.5.0 version update
|
||||||
|
|
||||||
|
-- Jeremy Soller <jeremy@system76.com> Tue, 28 Jul 2026 09:15:04 -0600
|
||||||
|
|
||||||
|
cosmic-term (1.4.0) noble; urgency=medium
|
||||||
|
|
||||||
|
* Epoch 1.4.0 version update
|
||||||
|
|
||||||
|
-- Jeremy Soller <jeremy@system76.com> Mon, 20 Jul 2026 14:36:58 -0600
|
||||||
|
|
||||||
|
cosmic-term (1.3.0) noble; urgency=medium
|
||||||
|
|
||||||
|
* Epoch 1.3.0 version update
|
||||||
|
|
||||||
|
-- Jeremy Soller <jeremy@system76.com> Sun, 12 Jul 2026 13:22:28 -0600
|
||||||
|
|
||||||
cosmic-term (1.2.0) noble; urgency=medium
|
cosmic-term (1.2.0) noble; urgency=medium
|
||||||
|
|
||||||
* Epoch 1.2.0 version update
|
* Epoch 1.2.0 version update
|
||||||
|
|
|
||||||
6
debian/source/options
vendored
6
debian/source/options
vendored
|
|
@ -1,4 +1,6 @@
|
||||||
tar-ignore=.github
|
tar-ignore=.github
|
||||||
tar-ignore=.vscode
|
tar-ignore=target/.rustc_info.json
|
||||||
|
tar-ignore=target/CACHEDIR.TAG
|
||||||
|
tar-ignore=target/debug
|
||||||
|
tar-ignore=target/release
|
||||||
tar-ignore=vendor
|
tar-ignore=vendor
|
||||||
tar-ignore=target
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
cancel = Cancel·lar
|
cancel = Cancel·lar
|
||||||
settings = Configuració
|
settings = Configuració
|
||||||
quit = Surt
|
quit = Surt
|
||||||
|
delete = Suprimeix
|
||||||
|
rename = Canvia el nom
|
||||||
|
name = Nom
|
||||||
|
|
|
||||||
|
|
@ -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 = Normální tloušťka písma
|
default-font-weight = Tloušťka normálního písma
|
||||||
default-dim-font-weight = Tenká tloušťka písma
|
default-dim-font-weight = Tloušťka zeslabeného písma
|
||||||
default-bold-font-weight = Tučná tloušťka písma
|
default-bold-font-weight = Tloušťka tučného 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 hlavičku
|
show-headerbar = Zobrazit záhlaví
|
||||||
show-header-description = Hlavičku je možné odkrýt z kontextového menu
|
show-header-description = Záhlaví lze odkrýt v nabídce zobrazené po kliknutí pravým tlačítkem myši
|
||||||
find-placeholder = Najít...
|
find-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 = Přepnout maximalizaci
|
pane-toggle-maximize = Maximalizovat nebo obnovit podokno
|
||||||
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,13 +99,17 @@ 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 panel dole
|
focus-pane-down = Zaměřit podokno dole
|
||||||
focus-pane-left = Zaměřit panel vlevo
|
focus-pane-left = Zaměřit podokno vlevo
|
||||||
focus-pane-right = Zaměřit panel vpravo
|
focus-pane-right = Zaměřit podokno vpravo
|
||||||
focus-pane-up = Zaměřit panel nahoře
|
focus-pane-up = Zaměřit podokno 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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
rename = Omdøb
|
||||||
|
delete = Slet
|
||||||
|
name = Navn
|
||||||
|
cancel = Afbryd
|
||||||
|
quit = Afslut
|
||||||
|
find-next = Find næste
|
||||||
|
select-all = Vælg alle
|
||||||
|
new-window = Nyt vindue
|
||||||
|
edit = Redigér
|
||||||
|
copy = Kopiér
|
||||||
|
theme = Tema
|
||||||
|
appearance = Udseende
|
||||||
|
light = Lyst
|
||||||
|
repository = Pakkearkiv
|
||||||
|
paste = Indsæt
|
||||||
|
menu-settings = Indstillinger...
|
||||||
|
view = Vis
|
||||||
|
dark = Mørkt
|
||||||
|
match-desktop = Følg skrivebordets indstillinger
|
||||||
|
find-previous = Find forrige
|
||||||
|
find-placeholder = Find...
|
||||||
|
default-zoom-step = Zoom-trin
|
||||||
|
file = Fil
|
||||||
|
find = Find
|
||||||
|
support = Support
|
||||||
|
settings = Indstillinger
|
||||||
|
keyboard-shortcuts = Tastaturgenveje
|
||||||
|
menu-keyboard-shortcuts = Tastaturgenveje…
|
||||||
|
replace = Erstat
|
||||||
|
close-tab = Luk fane
|
||||||
|
new-tab = Ny fane
|
||||||
|
|
@ -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 = Maximierung umschalten
|
pane-toggle-maximize = Bereich maximieren oder wiederherstellen
|
||||||
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 = Repository
|
repository = Repositorium
|
||||||
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,3 +149,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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,3 +152,7 @@ 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 = Μετακίνηση καρτέλας στα δεξιά
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,14 @@ 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 = Toggle maximized
|
pane-toggle-maximize = Maximise or restore pane
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,18 @@ 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
|
||||||
|
|
@ -137,6 +142,10 @@ 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
|
||||||
|
|
@ -149,9 +158,11 @@ 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 = Toggle maximized
|
pane-toggle-maximize = Maximize or restore pane
|
||||||
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...
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
name = نام
|
||||||
|
cancel = لغو
|
||||||
|
quit = خروج
|
||||||
|
rename = تغییر نام
|
||||||
|
delete = حذف
|
||||||
|
|
@ -38,7 +38,7 @@ settings = Asetukset
|
||||||
|
|
||||||
appearance = Ulkoasu
|
appearance = Ulkoasu
|
||||||
theme = Teema
|
theme = Teema
|
||||||
match-desktop = Sovita työpöytään
|
match-desktop = Sama kuin työpöytä
|
||||||
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 = Suurennus päällä tai pois
|
pane-toggle-maximize = Suurenna tai palauta paneeli
|
||||||
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,3 +151,7 @@ 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
|
||||||
|
|
|
||||||
0
i18n/fil/cosmic_term.ftl
Normal file
0
i18n/fil/cosmic_term.ftl
Normal file
|
|
@ -96,7 +96,7 @@ quit = Quitter
|
||||||
|
|
||||||
## Edit
|
## Edit
|
||||||
|
|
||||||
edit = Modifier
|
edit = Édition
|
||||||
copy = Copier
|
copy = Copier
|
||||||
paste = Coller
|
paste = Coller
|
||||||
select-all = Sélectionner tout
|
select-all = Sélectionner tout
|
||||||
|
|
@ -108,6 +108,10 @@ 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
|
||||||
|
|
||||||
|
|
@ -119,7 +123,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 = Vue unique
|
pane-toggle-maximize = Agrandir ou restaurer le panneau
|
||||||
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...
|
||||||
|
|
@ -168,3 +172,10 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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 = Scoránaigh uasmhéadú
|
pane-toggle-maximize = Uasmhéadaigh nó athchóirigh an phána
|
||||||
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,5 +147,7 @@ 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í nua an eolaire reatha
|
tab-new-inherit-working-directory = Úsáideann cluaisíní agus fuinneoga nua an t-eolaire reatha
|
||||||
tab-new-inherit-working-directory-description = Oscail cluaisíní nua in eolaire oibre an chluaisín ghníomhaigh
|
tab-new-inherit-working-directory-description = Oscail cluaisíní agus fuinneoga nua in eolaire oibre an chluaisín ghníomhaigh
|
||||||
|
show-pane-borders = Taispeáin teorainneacha na pána
|
||||||
|
window-header = Ceanntásc na fuinneoige
|
||||||
|
|
|
||||||
115
i18n/gl/cosmic_term.ftl
Normal file
115
i18n/gl/cosmic_term.ftl
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
name = Nome
|
||||||
|
cancel = Cancelar
|
||||||
|
delete = Eliminar
|
||||||
|
rename = Renomear
|
||||||
|
repository = Repositorio
|
||||||
|
support = Soporte
|
||||||
|
settings = Axustes
|
||||||
|
appearance = Apariencia
|
||||||
|
theme = Tema
|
||||||
|
match-desktop = Emparexar co escritorio
|
||||||
|
dark = Escuro
|
||||||
|
light = Claro
|
||||||
|
default-zoom-step = Pasos do zoom
|
||||||
|
keyboard-shortcuts = Atallos de teclado
|
||||||
|
find-placeholder = Atopar…
|
||||||
|
find-previous = Atopar antes
|
||||||
|
find-next = Atopar seguinte
|
||||||
|
replace = Remplazar
|
||||||
|
file = Ficheiro
|
||||||
|
new-window = Nova xanela
|
||||||
|
quit = Pechar
|
||||||
|
edit = Editar
|
||||||
|
copy = Copiar
|
||||||
|
paste = Pegar
|
||||||
|
select-all = Seleccionar todo
|
||||||
|
find = Atopar
|
||||||
|
view = Ver
|
||||||
|
menu-settings = Axustes...
|
||||||
|
menu-keyboard-shortcuts = Atallos de teclado…
|
||||||
|
new-tab = Nova lapela
|
||||||
|
close-tab = Pechar lapela
|
||||||
|
export = Exportar
|
||||||
|
import = Importar
|
||||||
|
reset-to-default = Restablecer aos valores predeterminados
|
||||||
|
type-to-search = Escribe para buscar...
|
||||||
|
add-another-keybinding = Engadir outro atallo de teclado
|
||||||
|
cosmic-terminal = Terminal de COSMIC
|
||||||
|
comment = Emulador de terminal para o escritorio COSMIC
|
||||||
|
keywords = Orde; Shell; Terminal; CLI;
|
||||||
|
new-terminal = Novo terminal
|
||||||
|
color-schemes = Esquemas de cores
|
||||||
|
import-errors = Importar erros
|
||||||
|
profiles = Perfís
|
||||||
|
command-line = Liña de comandos
|
||||||
|
tab-title = Titulo da lapela
|
||||||
|
tab-title-description = Substituír o título predeterminado da lapela
|
||||||
|
add-profile = Engadir perfil
|
||||||
|
new-profile = Novo perfil
|
||||||
|
make-default = Facer predeterminado
|
||||||
|
working-directory = Directorio de traballo
|
||||||
|
hold = Manter
|
||||||
|
remain-open = Permanecer aberto despois de que remate o proceso fillo.
|
||||||
|
syntax-dark = Esquema de cores escuro
|
||||||
|
syntax-light = Esquema de cores claras
|
||||||
|
opacity = Opacidade do fondo
|
||||||
|
font = Tipografía
|
||||||
|
advanced-font-settings = Configuración avanzada da tipografía
|
||||||
|
default-font = Tipografía
|
||||||
|
default-font-size = Tamaño da tipografía
|
||||||
|
default-font-stretch = Ancho da tipografía
|
||||||
|
default-font-weight = Grosor da tipografía normal
|
||||||
|
default-dim-font-weight = Grosor da tipografía fina
|
||||||
|
default-bold-font-weight = Grosor da tipografía en negriña
|
||||||
|
use-bright-bold = Facer máis brillante o texto en negriña
|
||||||
|
splits = Divisións
|
||||||
|
focus-follow-mouse = O foco de escritura segue o rato
|
||||||
|
show-pane-borders = Mostrar os bordos dos paneis
|
||||||
|
advanced = Avanzado
|
||||||
|
show-headerbar = Mostrar a cabeceira
|
||||||
|
show-header-description = Mostrar a cabeceira desde o menú contextual
|
||||||
|
tab-new-inherit-working-directory = As novas lapelas e xanelas usan o directorio actual
|
||||||
|
tab-new-inherit-working-directory-description = Abrir novas lapelas e xanelas no directorio de traballo da pestana activa
|
||||||
|
close-window = Pechar xanela
|
||||||
|
copy-or-sigint = Copiar ou SIGINT
|
||||||
|
disable = Desactivar
|
||||||
|
focus-pane-down = Poñer o foco no panel inferior
|
||||||
|
focus-pane-left = Poñer o foco no panel esquerdo
|
||||||
|
focus-pane-right = Poñer o foco no panel dereito
|
||||||
|
focus-pane-up = Poñer o foco no panel superior
|
||||||
|
no-shortcuts = Sen atallos de teclado
|
||||||
|
password-manager = Xestor de contrasinais
|
||||||
|
paste-primary = Pegar o contido primario
|
||||||
|
shortcut-capture-hint = Preme a combinación de teclas
|
||||||
|
shortcut-group-clipboard = Portapapeis
|
||||||
|
shortcut-group-other = Outro
|
||||||
|
shortcut-group-tabs = Lapelas
|
||||||
|
shortcut-group-window = Xanela
|
||||||
|
shortcut-group-zoom = Zoom
|
||||||
|
shortcut-replace-body = { $binding } xa está asignado a { $existing }. Substituílo por { $new_action }?
|
||||||
|
shortcut-replace-title = Remplazar atallo de teclado ?
|
||||||
|
tab-activate = Activar a lapela { $number }
|
||||||
|
toggle-fullscreen = Cambiar a pantalla completa
|
||||||
|
profile = Perfil
|
||||||
|
menu-profiles = Perfís...
|
||||||
|
clear-scrollback = Limpar o historial de desprazamento
|
||||||
|
open-link = Abrir enderezo
|
||||||
|
copy-link = Copiar enderezo
|
||||||
|
zoom-in = Texto máis grande
|
||||||
|
zoom-reset = Tamaño de texto predeterminado
|
||||||
|
zoom-out = Texto máis pequeno
|
||||||
|
next-tab = Lapela seguinte
|
||||||
|
previous-tab = Lapela previa
|
||||||
|
split-horizontal = Dividir horizontalmente
|
||||||
|
split-vertical = Dividir verticalmente
|
||||||
|
pane-toggle-maximize = Alternar maximizado
|
||||||
|
menu-color-schemes = Esquema de cores...
|
||||||
|
menu-about = Sobre o Terminal de COSMIC...
|
||||||
|
menu-password-manager = Contrasinais...
|
||||||
|
passwords-title = Contrasinais
|
||||||
|
add-password = Engadir contrasinal
|
||||||
|
password-input = Contrasinal
|
||||||
|
password-input-description = Descripción
|
||||||
|
window-header = Cabeceira da xanela
|
||||||
|
move-tab-left = Mover a pestaña á esquerda
|
||||||
|
move-tab-right = Mover a pestaña á dereita
|
||||||
|
|
@ -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,7 +104,11 @@ 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 = कीबोर्ड शॉर्टकट्स..।
|
||||||
|
|
|
||||||
|
|
@ -52,14 +52,14 @@ opacity = Háttér átlátszósága
|
||||||
|
|
||||||
### Font
|
### Font
|
||||||
|
|
||||||
font = Betűtípus
|
font = Betűkészlet
|
||||||
advanced-font-settings = Speciális betűtípus-beállítások
|
advanced-font-settings = Speciális betűkészlet-beállítások
|
||||||
default-font = Betűtípus
|
default-font = Betűkészlet
|
||||||
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űsúly
|
default-font-weight = Normál betűvastagság
|
||||||
default-dim-font-weight = Vékony betűsúly
|
default-dim-font-weight = Vékony betűvastagság
|
||||||
default-bold-font-weight = Félkövér betűsúly
|
default-bold-font-weight = Félkövér betűvastagság
|
||||||
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
|
pane-toggle-maximize = Panel maximalizálása vagy visszaállítá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,3 +149,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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 = Cocokkan desktop
|
match-desktop = Sesuaikan 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 = Ubah ke maksimal
|
pane-toggle-maximize = Maksimalkan atau pulihkan panel
|
||||||
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,3 +109,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ settings = Impostazioni
|
||||||
|
|
||||||
appearance = Aspetto
|
appearance = Aspetto
|
||||||
theme = Tema
|
theme = Tema
|
||||||
match-desktop = Sistema
|
match-desktop = Abbina al desktop
|
||||||
dark = Scuro
|
dark = Scuro
|
||||||
light = Chiaro
|
light = Chiaro
|
||||||
syntax-dark = Schema colori scuro
|
syntax-dark = Schema colori scuro
|
||||||
|
|
@ -115,3 +115,7 @@ 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...
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
cancel = Sefsex
|
cancel = Semmet
|
||||||
name = Isem
|
name = Isem
|
||||||
delete = kkes
|
delete = kkes
|
||||||
rename = Snifel isem
|
rename = Snifel isem
|
||||||
quit = Tuffɣa
|
quit = Ffeɣ
|
||||||
support = Tallalt
|
support = Tallelt
|
||||||
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 = Amṣada d tnarit
|
match-desktop = Semṣada akked tnarit
|
||||||
file = Afaylu
|
file = Afaylu
|
||||||
new-window = Asfaylu amaynut
|
new-window = Asfaylu amaynut
|
||||||
edit = Ẓreg
|
edit = Ẓreg
|
||||||
|
|
|
||||||
|
|
@ -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,5 +107,9 @@ 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 = Бетті оңға жылжыту
|
||||||
|
|
|
||||||
|
|
@ -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 = Li gorî sermaseyê
|
match-desktop = Bi sermaseyê re guncav bike
|
||||||
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 rengê ronî
|
syntax-light = Şêwaza rengan a 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,3 +109,7 @@ 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ê
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name = Pavadinimas
|
name = Pavadinimas
|
||||||
delete = Ištrinti
|
delete = Ištrinti
|
||||||
cosmic-terminal = COSMIC Terminal
|
cosmic-terminal = COSMIC Terminalas
|
||||||
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 = Nuostatos
|
settings = Nuostatai
|
||||||
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šeiti
|
quit = Išjungti
|
||||||
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 = Nuostatos...
|
menu-settings = Nuostatai...
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1 +1,14 @@
|
||||||
settings = Paramètres
|
settings = Configuracion
|
||||||
|
appearance = Aparéncia
|
||||||
|
theme = Tèma
|
||||||
|
match-desktop = Assortir al burèu
|
||||||
|
dark = Escur
|
||||||
|
light = Clar
|
||||||
|
file = Fichièr
|
||||||
|
name = Nom
|
||||||
|
repository = Depaus
|
||||||
|
cancel = Anullar
|
||||||
|
menu-settings = Configuracion...
|
||||||
|
quit = Quitar
|
||||||
|
delete = Suprimir
|
||||||
|
replace = Remplaçar
|
||||||
|
|
|
||||||
|
|
@ -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 = ਚੇਪੋ
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
cosmic-terminal = Terminal COSMIC
|
cosmic-terminal = Terminal COSMIC
|
||||||
comment = Emulator terminala dla pulpitu COSMIC
|
comment = Emulator terminala dla środowiska 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 = Szukaj…
|
find-placeholder = Wyszukaj…
|
||||||
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 = Zamknij
|
quit = Zakończ
|
||||||
|
|
||||||
## 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 = Szukaj
|
find = Wyszukaj
|
||||||
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 = Przełącznik maksymalizacji
|
pane-toggle-maximize = Maksymalizuj lub przywróć panel
|
||||||
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,3 +149,5 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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 a barra de título
|
show-headerbar = Mostrar cabeçalho
|
||||||
show-header-description = Mostrar o cabeçalho ao clicar no menu com o botão direito do mouse
|
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 = Sair
|
quit = Encerrar
|
||||||
|
|
||||||
## 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 = Alternar maximização
|
pane-toggle-maximize = Maximizar ou restaurar o quadro
|
||||||
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,3 +149,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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,5 +141,9 @@ 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 = Перемещение вкладки направо
|
||||||
|
|
|
||||||
2
i18n/sat/cosmic_term.ftl
Normal file
2
i18n/sat/cosmic_term.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
cancel = ᱵᱟᱹᱛᱤᱞ
|
||||||
|
name = ᱧᱩᱛᱩᱢ
|
||||||
|
|
@ -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 = Växla maximerad
|
pane-toggle-maximize = Maximera eller återställ fält
|
||||||
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,3 +153,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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,3 +109,8 @@ 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 = แทนที่
|
||||||
|
|
|
||||||
|
|
@ -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,5 +147,9 @@ 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 = Перемістити праворуч
|
||||||
|
|
|
||||||
|
|
@ -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,3 +149,7 @@ 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 = 将标签向右移动
|
||||||
|
|
|
||||||
|
|
@ -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,3 +149,7 @@ 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 = 將分頁向右移動
|
||||||
|
|
|
||||||
|
|
@ -231,17 +231,34 @@ 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 {
|
||||||
|
|
@ -252,6 +269,7 @@ 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,
|
||||||
|
|
@ -260,6 +278,8 @@ 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,
|
||||||
|
|
@ -341,6 +361,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|
@ -402,3 +428,22 @@ 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
185
src/desktop_notify.rs
Normal file
185
src/desktop_notify.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
//! Notifications de bureau des OSC 9 / 777 (yoda).
|
||||||
|
//!
|
||||||
|
//! On parle directement à `org.freedesktop.Notifications` plutôt que de passer
|
||||||
|
//! par notify-rust : au clic, cosmic-notifications émet `ActivationToken` puis
|
||||||
|
//! `ActionInvoked`, et c'est ce jeton xdg-activation qui permet de ramener la
|
||||||
|
//! fenêtre au premier plan sur le bon onglet.
|
||||||
|
//!
|
||||||
|
//! Une notification par onglet au plus : la suivante remplace la précédente
|
||||||
|
//! (`replaces_id`).
|
||||||
|
|
||||||
|
use cosmic::{
|
||||||
|
Application,
|
||||||
|
iced::{
|
||||||
|
Subscription,
|
||||||
|
futures::{SinkExt, channel::mpsc::Sender},
|
||||||
|
stream,
|
||||||
|
},
|
||||||
|
widget::{pane_grid, segmented_button},
|
||||||
|
};
|
||||||
|
use std::{any::TypeId, collections::HashMap};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use crate::fl;
|
||||||
|
|
||||||
|
/// Onglet à l'origine d'une notification.
|
||||||
|
pub type TabKey = (pane_grid::Pane, segmented_button::Entity);
|
||||||
|
|
||||||
|
/// Demande envoyée par l'application à la tâche D-Bus.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Request {
|
||||||
|
Show {
|
||||||
|
tab: TabKey,
|
||||||
|
summary: String,
|
||||||
|
body: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Événement renvoyé par la tâche D-Bus à l'application.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
/// Canal prêt : l'application peut envoyer ses demandes.
|
||||||
|
Ready(mpsc::UnboundedSender<Request>),
|
||||||
|
/// L'utilisateur a cliqué la notification de cet onglet.
|
||||||
|
Activated { tab: TabKey, token: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus::proxy(
|
||||||
|
interface = "org.freedesktop.Notifications",
|
||||||
|
default_service = "org.freedesktop.Notifications",
|
||||||
|
default_path = "/org/freedesktop/Notifications"
|
||||||
|
)]
|
||||||
|
trait Notifications {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn notify(
|
||||||
|
&self,
|
||||||
|
app_name: &str,
|
||||||
|
replaces_id: u32,
|
||||||
|
app_icon: &str,
|
||||||
|
summary: &str,
|
||||||
|
body: &str,
|
||||||
|
actions: &[&str],
|
||||||
|
hints: HashMap<&str, zbus::zvariant::Value<'_>>,
|
||||||
|
expire_timeout: i32,
|
||||||
|
) -> zbus::Result<u32>;
|
||||||
|
|
||||||
|
#[zbus(signal)]
|
||||||
|
fn action_invoked(&self, id: u32, action_key: String) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
#[zbus(signal)]
|
||||||
|
fn activation_token(&self, id: u32, activation_token: String) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
#[zbus(signal)]
|
||||||
|
fn notification_closed(&self, id: u32, reason: u32) -> zbus::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DesktopNotifySubscription;
|
||||||
|
|
||||||
|
pub fn subscription() -> Subscription<Event> {
|
||||||
|
Subscription::run_with(TypeId::of::<DesktopNotifySubscription>(), |_| {
|
||||||
|
stream::channel(16, |mut output: Sender<Event>| async move {
|
||||||
|
let (request_tx, request_rx) = mpsc::unbounded_channel();
|
||||||
|
if output.send(Event::Ready(request_tx)).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Err(err) = run(request_rx, output).await {
|
||||||
|
log::warn!("desktop notifications unavailable: {err}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(
|
||||||
|
mut request_rx: mpsc::UnboundedReceiver<Request>,
|
||||||
|
mut output: Sender<Event>,
|
||||||
|
) -> zbus::Result<()> {
|
||||||
|
use cosmic::iced::futures::StreamExt;
|
||||||
|
|
||||||
|
let connection = zbus::Connection::session().await?;
|
||||||
|
let proxy = NotificationsProxy::new(&connection).await?;
|
||||||
|
let mut tokens = proxy.receive_activation_token().await?;
|
||||||
|
let mut invoked = proxy.receive_action_invoked().await?;
|
||||||
|
let mut closed = proxy.receive_notification_closed().await?;
|
||||||
|
|
||||||
|
let app_name = fl!("cosmic-terminal");
|
||||||
|
let show_label = fl!("notification-show");
|
||||||
|
let mut tab_by_id: HashMap<u32, TabKey> = HashMap::new();
|
||||||
|
let mut id_by_tab: HashMap<TabKey, u32> = HashMap::new();
|
||||||
|
let mut pending_tokens: HashMap<u32, String> = HashMap::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
request = request_rx.recv() => {
|
||||||
|
let Some(Request::Show { tab, summary, body }) = request else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let replaces_id = id_by_tab.get(&tab).copied().unwrap_or(0);
|
||||||
|
let body = neutralize_markup(&body);
|
||||||
|
let hints = HashMap::from([
|
||||||
|
("desktop-entry", zbus::zvariant::Value::from(crate::App::APP_ID)),
|
||||||
|
("urgency", zbus::zvariant::Value::U8(1)),
|
||||||
|
]);
|
||||||
|
match proxy
|
||||||
|
.notify(
|
||||||
|
&app_name,
|
||||||
|
replaces_id,
|
||||||
|
crate::App::APP_ID,
|
||||||
|
&summary,
|
||||||
|
&body,
|
||||||
|
&["default", &show_label],
|
||||||
|
hints,
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(id) => {
|
||||||
|
if replaces_id != 0 && replaces_id != id {
|
||||||
|
tab_by_id.remove(&replaces_id);
|
||||||
|
}
|
||||||
|
tab_by_id.insert(id, tab);
|
||||||
|
id_by_tab.insert(tab, id);
|
||||||
|
}
|
||||||
|
Err(err) => log::warn!("failed to send desktop notification: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(signal) = tokens.next() => {
|
||||||
|
if let Ok(args) = signal.args()
|
||||||
|
&& tab_by_id.contains_key(args.id())
|
||||||
|
{
|
||||||
|
pending_tokens.insert(*args.id(), args.activation_token().clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(signal) = invoked.next() => {
|
||||||
|
if let Ok(args) = signal.args()
|
||||||
|
&& let Some(tab) = tab_by_id.get(args.id()).copied()
|
||||||
|
{
|
||||||
|
let token = pending_tokens.remove(args.id());
|
||||||
|
if output.send(Event::Activated { tab, token }).await.is_err() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(signal) = closed.next() => {
|
||||||
|
if let Ok(args) = signal.args()
|
||||||
|
&& let Some(tab) = tab_by_id.remove(args.id())
|
||||||
|
{
|
||||||
|
if id_by_tab.get(&tab) == Some(args.id()) {
|
||||||
|
id_by_tab.remove(&tab);
|
||||||
|
}
|
||||||
|
pending_tokens.remove(args.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Le corps d'une notification est du balisage (`body-markup`). Échapper `<`
|
||||||
|
/// en `<` serait conforme à la spécification, mais cosmic-notifications ne
|
||||||
|
/// décode pas les entités (il afficherait `<`), et un `<` brut y avale le
|
||||||
|
/// texte qui suit. Les chevrons deviennent donc des guillemets simples
|
||||||
|
/// d'aspect voisin, lisibles sur tous les serveurs.
|
||||||
|
fn neutralize_markup(body: &str) -> String {
|
||||||
|
body.replace('<', "‹").replace('>', "›")
|
||||||
|
}
|
||||||
697
src/kitty_keyboard.rs
Normal file
697
src/kitty_keyboard.rs
Normal file
|
|
@ -0,0 +1,697 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
//! Protocole clavier kitty (yoda).
|
||||||
|
//!
|
||||||
|
//! Avec `kitty_keyboard` actif, alacritty_terminal tient la pile des modes
|
||||||
|
//! (`CSI > n u`, `CSI < n u`, `CSI = n ; m u`) et répond à la requête
|
||||||
|
//! `CSI ? u`. Il n'encode en revanche aucune touche : c'est le rôle du frontal,
|
||||||
|
//! donc de ce module. Il ne sert que quand l'application au premier plan a
|
||||||
|
//! demandé le protocole ; sinon l'encodage legacy de `terminal_box` s'applique
|
||||||
|
//! seul.
|
||||||
|
//!
|
||||||
|
//! C'est ce qui donne Shift+Entrée (`CSI 13;2u`) à Claude Code, qui interroge
|
||||||
|
//! le terminal avec `CSI ? u` au démarrage.
|
||||||
|
//!
|
||||||
|
//! Spécification : <https://sw.kovidgoyal.net/kitty/keyboard-protocol/>. La
|
||||||
|
//! découpe suit l'encodeur du frontal Alacritty (`alacritty/src/input/keyboard.rs`).
|
||||||
|
//!
|
||||||
|
//! Limites : les drapeaux 2 (types d'événements), 4 (touches alternatives) et
|
||||||
|
//! 16 (texte associé) ne sont honorés qu'avec le drapeau 1 (désambiguïsation)
|
||||||
|
//! ou 8 (toutes les touches), ce que font toutes les applications connues.
|
||||||
|
//! La touche de la disposition de base (3e champ du drapeau 4) n'est pas
|
||||||
|
//! envoyée, et Caps Lock / Num Lock ne figurent pas dans les modificateurs
|
||||||
|
//! (iced ne les expose pas).
|
||||||
|
|
||||||
|
use alacritty_terminal::term::TermMode;
|
||||||
|
use cosmic::iced::keyboard::{Key, Location, Modifiers, key::Named};
|
||||||
|
|
||||||
|
/// Nature de l'événement clavier.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum KeyAction {
|
||||||
|
Press,
|
||||||
|
Repeat,
|
||||||
|
Release,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Événement clavier tel que livré par iced.
|
||||||
|
pub struct KeyInput<'a> {
|
||||||
|
/// Touche sans aucun modificateur (`key_without_modifiers` de winit).
|
||||||
|
pub key: &'a Key,
|
||||||
|
/// Touche avec Shift (et AltGr) appliqués, mais pas Ctrl (`logical_key`).
|
||||||
|
pub modified_key: &'a Key,
|
||||||
|
pub location: Location,
|
||||||
|
pub modifiers: Modifiers,
|
||||||
|
/// Texte produit, tous modificateurs appliqués.
|
||||||
|
pub text: Option<&'a str>,
|
||||||
|
pub action: KeyAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHIFT: u8 = 1;
|
||||||
|
const ALT: u8 = 2;
|
||||||
|
const CTRL: u8 = 4;
|
||||||
|
const SUPER: u8 = 8;
|
||||||
|
|
||||||
|
/// Le protocole kitty pilote-t-il l'encodage des touches ?
|
||||||
|
pub fn active(mode: TermMode) -> bool {
|
||||||
|
mode.intersects(TermMode::DISAMBIGUATE_ESC_CODES | TermMode::REPORT_ALL_KEYS_AS_ESC)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Octets à envoyer au PTY quand [`active`] est vrai. Vide : rien à envoyer.
|
||||||
|
pub fn encode(input: &KeyInput, mode: TermMode) -> Vec<u8> {
|
||||||
|
let all = mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC);
|
||||||
|
let mods = modifier_bits(input);
|
||||||
|
let named = match input.modified_key {
|
||||||
|
Key::Named(named) => Some(*named),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if input.action == KeyAction::Release {
|
||||||
|
if !mode.contains(TermMode::REPORT_EVENT_TYPES) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
// Pas de relâchement d'Entrée, Tab et Retour arrière hors « toutes les
|
||||||
|
// touches » : le shell qui reprend la main après un plantage les
|
||||||
|
// recevrait.
|
||||||
|
if !all && matches!(named, Some(Named::Enter | Named::Tab | Named::Backspace)) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
return build_sequence(input, mode, mods).unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sans modificateur, Entrée, Tab et Retour arrière gardent leurs octets
|
||||||
|
// legacy (spécification : un `reset` doit rester tapable). L'Entrée du
|
||||||
|
// pavé numérique, elle, a son code dédié.
|
||||||
|
if !all && mods == 0 && input.location != Location::Numpad {
|
||||||
|
match named {
|
||||||
|
Some(Named::Enter) => return b"\r".to_vec(),
|
||||||
|
Some(Named::Tab) => return b"\t".to_vec(),
|
||||||
|
Some(Named::Backspace) => return b"\x7f".to_vec(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = printable_text(input);
|
||||||
|
let as_sequence = all
|
||||||
|
|| text.is_none()
|
||||||
|
|| named == Some(Named::Escape)
|
||||||
|
|| (mods != 0 && mods != SHIFT)
|
||||||
|
|| (mods == SHIFT && matches!(named, Some(Named::Enter | Named::Tab | Named::Backspace)));
|
||||||
|
|
||||||
|
if as_sequence {
|
||||||
|
build_sequence(input, mode, mods).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
text.unwrap_or_default().as_bytes().to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Texte imprimable produit par la touche, s'il y en a un.
|
||||||
|
fn printable_text<'a>(input: &KeyInput<'a>) -> Option<&'a str> {
|
||||||
|
input
|
||||||
|
.text
|
||||||
|
.filter(|text| !text.is_empty() && !text.chars().any(char::is_control))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modifier_bits(input: &KeyInput) -> u8 {
|
||||||
|
let mut bits = 0;
|
||||||
|
if input.modifiers.shift() {
|
||||||
|
bits |= SHIFT;
|
||||||
|
}
|
||||||
|
if input.modifiers.alt() {
|
||||||
|
bits |= ALT;
|
||||||
|
}
|
||||||
|
if input.modifiers.control() {
|
||||||
|
bits |= CTRL;
|
||||||
|
}
|
||||||
|
if input.modifiers.logo() {
|
||||||
|
bits |= SUPER;
|
||||||
|
}
|
||||||
|
// Pour une touche de modification, l'état rapporté est celui qui suit
|
||||||
|
// l'événement : le bit est posé à l'appui, retiré au relâchement.
|
||||||
|
let own = match input.modified_key {
|
||||||
|
Key::Named(Named::Shift) => SHIFT,
|
||||||
|
Key::Named(Named::Alt) => ALT,
|
||||||
|
Key::Named(Named::Control) => CTRL,
|
||||||
|
Key::Named(Named::Super) => SUPER,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
if input.action == KeyAction::Release {
|
||||||
|
bits & !own
|
||||||
|
} else {
|
||||||
|
bits | own
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terminateur d'une séquence : `u` pour les codes kitty, sinon le final
|
||||||
|
/// legacy (`A`, `~`…) que le protocole conserve pour les touches de fonction.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum Final {
|
||||||
|
Kitty,
|
||||||
|
Legacy(char),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_sequence(input: &KeyInput, mode: TermMode, mods: u8) -> Option<Vec<u8>> {
|
||||||
|
let event_type = mode.contains(TermMode::REPORT_EVENT_TYPES)
|
||||||
|
&& matches!(input.action, KeyAction::Repeat | KeyAction::Release);
|
||||||
|
let associated_text = printable_text(input).filter(|_| {
|
||||||
|
mode.contains(TermMode::REPORT_ASSOCIATED_TEXT) && input.action != KeyAction::Release
|
||||||
|
});
|
||||||
|
// Le paramètre par défaut vaut 1 : `CSI A` plutôt que `CSI 1 A`.
|
||||||
|
let explicit = mods != 0 || event_type || associated_text.is_some();
|
||||||
|
|
||||||
|
let (payload, terminator) = numpad(input)
|
||||||
|
.or_else(|| functional(input, explicit))
|
||||||
|
.or_else(|| special(input, mode))
|
||||||
|
.or_else(|| textual(input, mode, associated_text.is_some()))?;
|
||||||
|
|
||||||
|
let mut sequence = format!("\x1b[{payload}");
|
||||||
|
if explicit {
|
||||||
|
sequence.push_str(&format!(";{}", mods + 1));
|
||||||
|
}
|
||||||
|
if event_type {
|
||||||
|
let kind = match input.action {
|
||||||
|
KeyAction::Press => '1',
|
||||||
|
KeyAction::Repeat => '2',
|
||||||
|
KeyAction::Release => '3',
|
||||||
|
};
|
||||||
|
sequence.push(':');
|
||||||
|
sequence.push(kind);
|
||||||
|
}
|
||||||
|
if let Some(text) = associated_text {
|
||||||
|
let codepoints: Vec<String> = text.chars().map(|c| u32::from(c).to_string()).collect();
|
||||||
|
sequence.push(';');
|
||||||
|
sequence.push_str(&codepoints.join(":"));
|
||||||
|
}
|
||||||
|
sequence.push(match terminator {
|
||||||
|
Final::Kitty => 'u',
|
||||||
|
Final::Legacy(c) => c,
|
||||||
|
});
|
||||||
|
Some(sequence.into_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pavé numérique : codes kitty dédiés (57399 et suivants).
|
||||||
|
fn numpad(input: &KeyInput) -> Option<(String, Final)> {
|
||||||
|
if input.location != Location::Numpad {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let code = match input.modified_key {
|
||||||
|
Key::Character(c) => match c.as_str() {
|
||||||
|
"0" => 57399,
|
||||||
|
"1" => 57400,
|
||||||
|
"2" => 57401,
|
||||||
|
"3" => 57402,
|
||||||
|
"4" => 57403,
|
||||||
|
"5" => 57404,
|
||||||
|
"6" => 57405,
|
||||||
|
"7" => 57406,
|
||||||
|
"8" => 57407,
|
||||||
|
"9" => 57408,
|
||||||
|
"." | "," => 57409,
|
||||||
|
"/" => 57410,
|
||||||
|
"*" => 57411,
|
||||||
|
"-" => 57412,
|
||||||
|
"+" => 57413,
|
||||||
|
"=" => 57415,
|
||||||
|
_ => return None,
|
||||||
|
},
|
||||||
|
Key::Named(named) => match named {
|
||||||
|
Named::Enter => 57414,
|
||||||
|
Named::ArrowLeft => 57417,
|
||||||
|
Named::ArrowRight => 57418,
|
||||||
|
Named::ArrowUp => 57419,
|
||||||
|
Named::ArrowDown => 57420,
|
||||||
|
Named::PageUp => 57421,
|
||||||
|
Named::PageDown => 57422,
|
||||||
|
Named::Home => 57423,
|
||||||
|
Named::End => 57424,
|
||||||
|
Named::Insert => 57425,
|
||||||
|
Named::Delete => 57426,
|
||||||
|
Named::Clear => 57427,
|
||||||
|
_ => return None,
|
||||||
|
},
|
||||||
|
Key::Unidentified => return None,
|
||||||
|
};
|
||||||
|
Some((code.to_string(), Final::Kitty))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Touches de fonction : finals legacy conservés par le protocole, sauf F3
|
||||||
|
/// (`CSI R` serait pris pour un rapport de position du curseur) et F13+.
|
||||||
|
fn functional(input: &KeyInput, explicit: bool) -> Option<(String, Final)> {
|
||||||
|
let Key::Named(named) = input.modified_key else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let one = if explicit { "1" } else { "" };
|
||||||
|
let (payload, terminator) = match named {
|
||||||
|
Named::ArrowUp => (one, Final::Legacy('A')),
|
||||||
|
Named::ArrowDown => (one, Final::Legacy('B')),
|
||||||
|
Named::ArrowRight => (one, Final::Legacy('C')),
|
||||||
|
Named::ArrowLeft => (one, Final::Legacy('D')),
|
||||||
|
Named::End => (one, Final::Legacy('F')),
|
||||||
|
Named::Home => (one, Final::Legacy('H')),
|
||||||
|
Named::F1 => (one, Final::Legacy('P')),
|
||||||
|
Named::F2 => (one, Final::Legacy('Q')),
|
||||||
|
Named::F3 => ("13", Final::Legacy('~')),
|
||||||
|
Named::F4 => (one, Final::Legacy('S')),
|
||||||
|
Named::Insert => ("2", Final::Legacy('~')),
|
||||||
|
Named::Delete => ("3", Final::Legacy('~')),
|
||||||
|
Named::PageUp => ("5", Final::Legacy('~')),
|
||||||
|
Named::PageDown => ("6", Final::Legacy('~')),
|
||||||
|
Named::F5 => ("15", Final::Legacy('~')),
|
||||||
|
Named::F6 => ("17", Final::Legacy('~')),
|
||||||
|
Named::F7 => ("18", Final::Legacy('~')),
|
||||||
|
Named::F8 => ("19", Final::Legacy('~')),
|
||||||
|
Named::F9 => ("20", Final::Legacy('~')),
|
||||||
|
Named::F10 => ("21", Final::Legacy('~')),
|
||||||
|
Named::F11 => ("23", Final::Legacy('~')),
|
||||||
|
Named::F12 => ("24", Final::Legacy('~')),
|
||||||
|
_ => {
|
||||||
|
let code = match named {
|
||||||
|
Named::F13 => 57376,
|
||||||
|
Named::F14 => 57377,
|
||||||
|
Named::F15 => 57378,
|
||||||
|
Named::F16 => 57379,
|
||||||
|
Named::F17 => 57380,
|
||||||
|
Named::F18 => 57381,
|
||||||
|
Named::F19 => 57382,
|
||||||
|
Named::F20 => 57383,
|
||||||
|
Named::F21 => 57384,
|
||||||
|
Named::F22 => 57385,
|
||||||
|
Named::F23 => 57386,
|
||||||
|
Named::F24 => 57387,
|
||||||
|
Named::CapsLock => 57358,
|
||||||
|
Named::ScrollLock => 57359,
|
||||||
|
Named::NumLock => 57360,
|
||||||
|
Named::PrintScreen => 57361,
|
||||||
|
Named::Pause => 57362,
|
||||||
|
Named::ContextMenu => 57363,
|
||||||
|
Named::MediaPlay => 57428,
|
||||||
|
Named::MediaPause => 57429,
|
||||||
|
Named::MediaPlayPause => 57430,
|
||||||
|
Named::MediaStop => 57432,
|
||||||
|
Named::MediaFastForward => 57433,
|
||||||
|
Named::MediaRewind => 57434,
|
||||||
|
Named::MediaTrackNext => 57435,
|
||||||
|
Named::MediaTrackPrevious => 57436,
|
||||||
|
Named::MediaRecord => 57437,
|
||||||
|
Named::AudioVolumeDown => 57438,
|
||||||
|
Named::AudioVolumeUp => 57439,
|
||||||
|
Named::AudioVolumeMute => 57440,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
return Some((code.to_string(), Final::Kitty));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some((payload.to_string(), terminator))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Touches de contrôle (Entrée, Tab, Échap, Retour arrière, Espace) et, avec
|
||||||
|
/// « toutes les touches », les touches de modification elles-mêmes.
|
||||||
|
fn special(input: &KeyInput, mode: TermMode) -> Option<(String, Final)> {
|
||||||
|
let code = match input.modified_key {
|
||||||
|
Key::Named(Named::Enter) => 13,
|
||||||
|
Key::Named(Named::Tab) => 9,
|
||||||
|
Key::Named(Named::Escape) => 27,
|
||||||
|
Key::Named(Named::Backspace) => 127,
|
||||||
|
Key::Character(c) if c == " " => 32,
|
||||||
|
Key::Named(modifier) if mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) => {
|
||||||
|
let right = input.location == Location::Right;
|
||||||
|
match (modifier, right) {
|
||||||
|
(Named::Shift, false) => 57441,
|
||||||
|
(Named::Control, false) => 57442,
|
||||||
|
(Named::Alt, false) => 57443,
|
||||||
|
(Named::Super, false) => 57444,
|
||||||
|
(Named::Hyper, false) => 57445,
|
||||||
|
(Named::Meta, false) => 57446,
|
||||||
|
(Named::Shift, true) => 57447,
|
||||||
|
(Named::Control, true) => 57448,
|
||||||
|
(Named::Alt, true) => 57449,
|
||||||
|
(Named::Super, true) => 57450,
|
||||||
|
(Named::Hyper, true) => 57451,
|
||||||
|
(Named::Meta, true) => 57452,
|
||||||
|
(Named::AltGraph, _) => 57453,
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some((code.to_string(), Final::Kitty))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Touches de caractère : code de la touche non décalée, suivi de la touche
|
||||||
|
/// décalée si le drapeau 4 le demande (`CSI 97:65;6u` pour Ctrl+Shift+A).
|
||||||
|
fn textual(input: &KeyInput, mode: TermMode, has_text: bool) -> Option<(String, Final)> {
|
||||||
|
let Key::Character(shifted) = input.modified_key else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let mut chars = shifted.chars();
|
||||||
|
let (Some(shifted_char), None) = (chars.next(), chars.next()) else {
|
||||||
|
// Plusieurs caractères pour une touche : seul « toutes les touches »
|
||||||
|
// avec texte associé sait le dire, par le code 0.
|
||||||
|
return (mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) && has_text)
|
||||||
|
.then(|| ("0".to_string(), Final::Kitty));
|
||||||
|
};
|
||||||
|
|
||||||
|
let unshifted_char = if input.modifiers.shift() {
|
||||||
|
shifted_char.to_lowercase().next().unwrap_or(shifted_char)
|
||||||
|
} else {
|
||||||
|
shifted_char
|
||||||
|
};
|
||||||
|
let mut code = u32::from(unshifted_char);
|
||||||
|
let alternate = u32::from(shifted_char);
|
||||||
|
// `!` n'a pas de minuscule : la touche de base est celle sans modificateur
|
||||||
|
// (`1` en QWERTY ; `&` pour le `1` de l'AZERTY).
|
||||||
|
if code == alternate
|
||||||
|
&& let Key::Character(base) = input.key
|
||||||
|
&& let Some(base_char) = base.chars().next()
|
||||||
|
{
|
||||||
|
code = u32::from(base_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = if mode.contains(TermMode::REPORT_ALTERNATE_KEYS) && alternate != code {
|
||||||
|
format!("{code}:{alternate}")
|
||||||
|
} else {
|
||||||
|
code.to_string()
|
||||||
|
};
|
||||||
|
Some((payload, Final::Kitty))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const DISAMBIGUATE: TermMode = TermMode::DISAMBIGUATE_ESC_CODES;
|
||||||
|
|
||||||
|
fn ch(c: &str) -> Key {
|
||||||
|
Key::Character(c.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn named(n: Named) -> Key {
|
||||||
|
Key::Named(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn press(
|
||||||
|
key: Key,
|
||||||
|
modified: Key,
|
||||||
|
modifiers: Modifiers,
|
||||||
|
text: Option<&str>,
|
||||||
|
mode: TermMode,
|
||||||
|
) -> String {
|
||||||
|
event(key, modified, modifiers, text, KeyAction::Press, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(
|
||||||
|
key: Key,
|
||||||
|
modified: Key,
|
||||||
|
modifiers: Modifiers,
|
||||||
|
text: Option<&str>,
|
||||||
|
action: KeyAction,
|
||||||
|
mode: TermMode,
|
||||||
|
) -> String {
|
||||||
|
let input = KeyInput {
|
||||||
|
key: &key,
|
||||||
|
modified_key: &modified,
|
||||||
|
location: Location::Standard,
|
||||||
|
modifiers,
|
||||||
|
text,
|
||||||
|
action,
|
||||||
|
};
|
||||||
|
String::from_utf8(encode(&input, mode)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shift_enter_for_claude_code() {
|
||||||
|
let enter = || named(Named::Enter);
|
||||||
|
assert_eq!(
|
||||||
|
press(enter(), enter(), Modifiers::SHIFT, Some("\r"), DISAMBIGUATE),
|
||||||
|
"\x1b[13;2u"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
press(enter(), enter(), Modifiers::CTRL, Some("\r"), DISAMBIGUATE),
|
||||||
|
"\x1b[13;5u"
|
||||||
|
);
|
||||||
|
// Entrée seule reste `\r`
|
||||||
|
assert_eq!(
|
||||||
|
press(enter(), enter(), Modifiers::empty(), Some("\r"), DISAMBIGUATE),
|
||||||
|
"\r"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_tab_backspace() {
|
||||||
|
let esc = || named(Named::Escape);
|
||||||
|
assert_eq!(
|
||||||
|
press(esc(), esc(), Modifiers::empty(), Some("\x1b"), DISAMBIGUATE),
|
||||||
|
"\x1b[27u"
|
||||||
|
);
|
||||||
|
let tab = || named(Named::Tab);
|
||||||
|
assert_eq!(
|
||||||
|
press(tab(), tab(), Modifiers::empty(), Some("\t"), DISAMBIGUATE),
|
||||||
|
"\t"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
press(tab(), tab(), Modifiers::SHIFT, Some("\t"), DISAMBIGUATE),
|
||||||
|
"\x1b[9;2u"
|
||||||
|
);
|
||||||
|
let bs = || named(Named::Backspace);
|
||||||
|
assert_eq!(
|
||||||
|
press(bs(), bs(), Modifiers::empty(), Some("\x08"), DISAMBIGUATE),
|
||||||
|
"\x7f"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
press(bs(), bs(), Modifiers::CTRL, Some("\x08"), DISAMBIGUATE),
|
||||||
|
"\x1b[127;5u"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_keys() {
|
||||||
|
// Texte simple ou décalé : envoyé tel quel
|
||||||
|
assert_eq!(
|
||||||
|
press(ch("a"), ch("a"), Modifiers::empty(), Some("a"), DISAMBIGUATE),
|
||||||
|
"a"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
press(ch("a"), ch("A"), Modifiers::SHIFT, Some("A"), DISAMBIGUATE),
|
||||||
|
"A"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
press(ch("é"), ch("é"), Modifiers::empty(), Some("é"), DISAMBIGUATE),
|
||||||
|
"é"
|
||||||
|
);
|
||||||
|
// Ctrl et Alt : séquences
|
||||||
|
assert_eq!(
|
||||||
|
press(ch("c"), ch("c"), Modifiers::CTRL, Some("\x03"), DISAMBIGUATE),
|
||||||
|
"\x1b[99;5u"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
press(ch("a"), ch("a"), Modifiers::ALT, Some("a"), DISAMBIGUATE),
|
||||||
|
"\x1b[97;3u"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
press(ch(" "), ch(" "), Modifiers::CTRL, Some("\0"), DISAMBIGUATE),
|
||||||
|
"\x1b[32;5u"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alternate_keys() {
|
||||||
|
let mode = DISAMBIGUATE | TermMode::REPORT_ALTERNATE_KEYS;
|
||||||
|
assert_eq!(
|
||||||
|
press(
|
||||||
|
ch("a"),
|
||||||
|
ch("A"),
|
||||||
|
Modifiers::CTRL | Modifiers::SHIFT,
|
||||||
|
Some("\x01"),
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
"\x1b[97:65;6u"
|
||||||
|
);
|
||||||
|
// AZERTY : Ctrl+Shift+& donne « 1 »
|
||||||
|
assert_eq!(
|
||||||
|
press(
|
||||||
|
ch("&"),
|
||||||
|
ch("1"),
|
||||||
|
Modifiers::CTRL | Modifiers::SHIFT,
|
||||||
|
None,
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
"\x1b[38:49;6u"
|
||||||
|
);
|
||||||
|
// Sans le drapeau 4, pas d'alternative
|
||||||
|
assert_eq!(
|
||||||
|
press(
|
||||||
|
ch("a"),
|
||||||
|
ch("A"),
|
||||||
|
Modifiers::CTRL | Modifiers::SHIFT,
|
||||||
|
Some("\x01"),
|
||||||
|
DISAMBIGUATE
|
||||||
|
),
|
||||||
|
"\x1b[97;6u"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functional_keys() {
|
||||||
|
let up = || named(Named::ArrowUp);
|
||||||
|
assert_eq!(
|
||||||
|
press(up(), up(), Modifiers::empty(), None, DISAMBIGUATE),
|
||||||
|
"\x1b[A"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
press(up(), up(), Modifiers::CTRL, None, DISAMBIGUATE),
|
||||||
|
"\x1b[1;5A"
|
||||||
|
);
|
||||||
|
let f3 = || named(Named::F3);
|
||||||
|
assert_eq!(
|
||||||
|
press(f3(), f3(), Modifiers::empty(), None, DISAMBIGUATE),
|
||||||
|
"\x1b[13~"
|
||||||
|
);
|
||||||
|
let del = || named(Named::Delete);
|
||||||
|
assert_eq!(
|
||||||
|
press(del(), del(), Modifiers::SHIFT, None, DISAMBIGUATE),
|
||||||
|
"\x1b[3;2~"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_types() {
|
||||||
|
let mode = DISAMBIGUATE | TermMode::REPORT_EVENT_TYPES;
|
||||||
|
assert_eq!(
|
||||||
|
event(
|
||||||
|
ch("a"),
|
||||||
|
ch("a"),
|
||||||
|
Modifiers::empty(),
|
||||||
|
None,
|
||||||
|
KeyAction::Release,
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
"\x1b[97;1:3u"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
event(
|
||||||
|
ch("a"),
|
||||||
|
ch("a"),
|
||||||
|
Modifiers::CTRL,
|
||||||
|
Some("\x01"),
|
||||||
|
KeyAction::Repeat,
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
"\x1b[97;5:2u"
|
||||||
|
);
|
||||||
|
// Pas de relâchement d'Entrée hors « toutes les touches »
|
||||||
|
let enter = || named(Named::Enter);
|
||||||
|
assert_eq!(
|
||||||
|
event(
|
||||||
|
enter(),
|
||||||
|
enter(),
|
||||||
|
Modifiers::empty(),
|
||||||
|
None,
|
||||||
|
KeyAction::Release,
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
// Ni de relâchement du tout sans le drapeau 2
|
||||||
|
assert_eq!(
|
||||||
|
event(
|
||||||
|
ch("a"),
|
||||||
|
ch("a"),
|
||||||
|
Modifiers::empty(),
|
||||||
|
None,
|
||||||
|
KeyAction::Release,
|
||||||
|
DISAMBIGUATE
|
||||||
|
),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn report_all_keys() {
|
||||||
|
let mode = TermMode::REPORT_ALL_KEYS_AS_ESC | TermMode::REPORT_ASSOCIATED_TEXT;
|
||||||
|
assert_eq!(
|
||||||
|
press(ch("a"), ch("a"), Modifiers::empty(), Some("a"), mode),
|
||||||
|
"\x1b[97;1;97u"
|
||||||
|
);
|
||||||
|
let enter = || named(Named::Enter);
|
||||||
|
assert_eq!(
|
||||||
|
press(enter(), enter(), Modifiers::empty(), Some("\r"), mode),
|
||||||
|
"\x1b[13u"
|
||||||
|
);
|
||||||
|
// Touche de modification : son propre bit est posé à l'appui
|
||||||
|
let shift = || named(Named::Shift);
|
||||||
|
assert_eq!(
|
||||||
|
press(shift(), shift(), Modifiers::empty(), None, mode),
|
||||||
|
"\x1b[57441;2u"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numpad_keys() {
|
||||||
|
let input = |key: &Key, text| {
|
||||||
|
String::from_utf8(encode(
|
||||||
|
&KeyInput {
|
||||||
|
key,
|
||||||
|
modified_key: key,
|
||||||
|
location: Location::Numpad,
|
||||||
|
modifiers: Modifiers::empty(),
|
||||||
|
text,
|
||||||
|
action: KeyAction::Press,
|
||||||
|
},
|
||||||
|
DISAMBIGUATE,
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
// Un chiffre du pavé produit du texte : envoyé tel quel
|
||||||
|
assert_eq!(input(&ch("5"), Some("5")), "5");
|
||||||
|
// Entrée du pavé n'en produit pas d'imprimable : code dédié
|
||||||
|
assert_eq!(input(&named(Named::Enter), Some("\r")), "\x1b[57414u");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_with_disambiguate_or_all() {
|
||||||
|
assert!(!active(TermMode::empty()));
|
||||||
|
assert!(!active(TermMode::REPORT_EVENT_TYPES));
|
||||||
|
assert!(active(DISAMBIGUATE));
|
||||||
|
assert!(active(TermMode::REPORT_ALL_KEYS_AS_ESC));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Le terminal répond à `CSI ? u` et applique `CSI > 5 u` dès que
|
||||||
|
/// `kitty_keyboard` est actif : c'est ce que Claude Code interroge.
|
||||||
|
#[test]
|
||||||
|
fn term_reports_and_applies_kitty_modes() {
|
||||||
|
use alacritty_terminal::{
|
||||||
|
Term,
|
||||||
|
event::{Event, EventListener},
|
||||||
|
term::{Config, test::TermSize},
|
||||||
|
vte::ansi::Processor,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct Replies(Arc<Mutex<Vec<String>>>);
|
||||||
|
impl EventListener for Replies {
|
||||||
|
fn send_event(&self, event: Event) {
|
||||||
|
if let Event::PtyWrite(text) = event {
|
||||||
|
self.0.lock().unwrap().push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let replies = Replies::default();
|
||||||
|
let config = Config {
|
||||||
|
kitty_keyboard: true,
|
||||||
|
..Config::default()
|
||||||
|
};
|
||||||
|
let mut term = Term::new(config, &TermSize::new(80, 24), replies.clone());
|
||||||
|
let mut parser: Processor = Processor::new();
|
||||||
|
parser.advance(&mut term, b"\x1b[?u\x1b[>5u\x1b[?u");
|
||||||
|
|
||||||
|
assert_eq!(*replies.0.lock().unwrap(), ["\x1b[?0u", "\x1b[?5u"]);
|
||||||
|
let mode = *term.mode();
|
||||||
|
assert!(mode.contains(TermMode::DISAMBIGUATE_ESC_CODES));
|
||||||
|
assert!(mode.contains(TermMode::REPORT_ALTERNATE_KEYS));
|
||||||
|
assert!(active(mode));
|
||||||
|
|
||||||
|
parser.advance(&mut term, b"\x1b[<u");
|
||||||
|
assert!(!active(*term.mode()));
|
||||||
|
}
|
||||||
|
}
|
||||||
685
src/main.rs
685
src/main.rs
File diff suppressed because it is too large
Load diff
202
src/menu.rs
202
src/menu.rs
|
|
@ -1,17 +1,15 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use cosmic::iced::Point;
|
use cosmic::widget::menu::action::MenuAction;
|
||||||
use cosmic::widget::menu::key_bind::KeyBind;
|
use cosmic::widget::menu::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, advanced::widget::text::Style as TextStyle},
|
iced::{Background, Length},
|
||||||
theme,
|
|
||||||
widget::{
|
widget::{
|
||||||
self, divider,
|
self,
|
||||||
menu::{ItemHeight, ItemWidth},
|
menu::{ItemHeight, ItemWidth},
|
||||||
responsive_menu_bar, segmented_button,
|
responsive_menu_bar, segmented_button,
|
||||||
},
|
},
|
||||||
|
|
@ -23,147 +21,94 @@ use crate::{Action, ColorSchemeId, ColorSchemeKind, Config, Message, fl, termina
|
||||||
static MENU_ID: LazyLock<cosmic::widget::Id> =
|
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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn context_menu<'a>(
|
/// A context menu action dispatched to the tab the menu was opened on.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct TabAction(pub segmented_button::Entity, pub Action);
|
||||||
|
|
||||||
|
impl MenuAction for TabAction {
|
||||||
|
type Message = Message;
|
||||||
|
|
||||||
|
fn message(&self) -> Message {
|
||||||
|
Message::TabContextAction(self.0, self.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context_menu(
|
||||||
config: &Config,
|
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),
|
||||||
) -> Element<'a, Message> {
|
read_only: bool,
|
||||||
let find_key = |action: &Action| -> String {
|
) -> Vec<widget::menu::Tree<Message>> {
|
||||||
for (key_bind, key_action) in key_binds {
|
let item =
|
||||||
if action == key_action {
|
|label: String, action: Action| MenuItem::Button(label, None, TabAction(entity, action));
|
||||||
return key_bind.to_string();
|
let check = |label: String, value: bool, action: Action| {
|
||||||
}
|
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 menu_checkbox = |label, value, action| {
|
let mut items = vec![
|
||||||
menu_button(vec![
|
item(fl!("new-tab"), Action::TabNew),
|
||||||
widget::text(label).into(),
|
item(fl!("rename-tab"), Action::TabRename),
|
||||||
widget::space::horizontal().into(),
|
MenuItem::Divider,
|
||||||
widget::toggler(value)
|
item(fl!("copy"), Action::Copy),
|
||||||
.on_toggle(move |_| Message::TabContextAction(entity, action))
|
item(fl!("paste"), Action::Paste),
|
||||||
.size(16.0)
|
item(fl!("select-all"), Action::SelectAll),
|
||||||
.into(),
|
MenuItem::Divider,
|
||||||
])
|
item(fl!("clear-scrollback"), Action::ClearScrollback),
|
||||||
.on_press(Message::TabContextAction(entity, action))
|
item(fl!("save-output"), Action::SaveOutput),
|
||||||
};
|
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),
|
||||||
)),
|
),
|
||||||
Element::from(menu_checkbox(
|
check(
|
||||||
fl!("monitor-silence"),
|
fl!("monitor-silence"),
|
||||||
monitors.1,
|
monitors.1,
|
||||||
Action::Monitor(MonitorKind::Silence),
|
Action::Monitor(MonitorKind::Silence),
|
||||||
)),
|
),
|
||||||
Element::from(menu_checkbox(
|
check(
|
||||||
fl!("monitor-process-finished"),
|
fl!("monitor-process-finished"),
|
||||||
monitors.2,
|
monitors.2,
|
||||||
Action::Monitor(MonitorKind::ProcessFinished),
|
Action::Monitor(MonitorKind::ProcessFinished),
|
||||||
)),
|
),
|
||||||
Element::from(divider::horizontal::light()),
|
check(fl!("read-only"), read_only, Action::ToggleReadOnly),
|
||||||
Element::from(menu_item(
|
MenuItem::Divider,
|
||||||
fl!("split-horizontal"),
|
item(fl!("split-horizontal"), Action::PaneSplitHorizontal),
|
||||||
Action::PaneSplitHorizontal,
|
item(fl!("split-vertical"), Action::PaneSplitVertical),
|
||||||
)),
|
item(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized),
|
||||||
Element::from(menu_item(fl!("split-vertical"), Action::PaneSplitVertical)),
|
MenuItem::Divider,
|
||||||
Element::from(menu_item(
|
MenuItem::CheckBox(
|
||||||
fl!("pane-toggle-maximize"),
|
fl!("window-header"),
|
||||||
Action::PaneToggleMaximized,
|
None,
|
||||||
)),
|
config.show_headerbar,
|
||||||
Element::from(divider::horizontal::light()),
|
TabAction(entity, Action::ShowHeaderBar(!config.show_headerbar)),
|
||||||
Element::from(menu_item(fl!("new-tab"), Action::TabNew)),
|
),
|
||||||
Element::from(menu_item(fl!("rename-tab"), Action::TabRename)),
|
MenuItem::Divider,
|
||||||
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));
|
||||||
rows.push(Element::from(menu_item(
|
items.push(item(fl!("menu-settings"), Action::Settings));
|
||||||
fl!("menu-password-manager"),
|
items.push(item(fl!("menu-about"), Action::About));
|
||||||
Action::PasswordManager,
|
// If we have a link, prepend the link items
|
||||||
)));
|
|
||||||
}
|
|
||||||
rows.push(Element::from(menu_checkbox(
|
|
||||||
fl!("show-headerbar"),
|
|
||||||
config.show_headerbar,
|
|
||||||
Action::ShowHeaderBar(!config.show_headerbar),
|
|
||||||
)));
|
|
||||||
|
|
||||||
//If we have a link
|
|
||||||
//prepend the Open Link item
|
|
||||||
if link.is_some() {
|
if link.is_some() {
|
||||||
rows.insert(
|
items.insert(0, item(fl!("open-link"), Action::LaunchUrlByMenu));
|
||||||
0,
|
items.insert(1, item(fl!("copy-link"), Action::CopyUrlByMenu));
|
||||||
Element::from(menu_item(fl!("open-link"), Action::LaunchUrlByMenu)),
|
items.insert(2, MenuItem::Divider);
|
||||||
);
|
|
||||||
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)
|
let key_binds: HashMap<KeyBind, TabAction> = key_binds
|
||||||
.padding(1)
|
.iter()
|
||||||
//TODO: move style to libcosmic
|
.map(|(key_bind, action)| (key_bind.clone(), TabAction(entity, *action)))
|
||||||
.style(|theme| {
|
.collect();
|
||||||
let cosmic = theme.cosmic();
|
widget::menu::items(&key_binds, items)
|
||||||
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>(
|
||||||
|
|
@ -174,7 +119,8 @@ 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 = widget::column::with_capacity(if id_opt.is_some() { 3 } else { 1 });
|
let mut column =
|
||||||
|
widget::menu::menu_column::MenuColumn::with_capacity(if id_opt.is_some() { 3 } else { 1 });
|
||||||
if let Some(id) = id_opt {
|
if let Some(id) = id_opt {
|
||||||
column = column.push(menu_item(
|
column = column.push(menu_item(
|
||||||
fl!("rename"),
|
fl!("rename"),
|
||||||
|
|
@ -197,7 +143,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(false).component;
|
let component = &cosmic.background(theme.transparent).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()),
|
||||||
|
|
@ -219,6 +165,7 @@ 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());
|
||||||
|
|
@ -290,6 +237,9 @@ 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,
|
||||||
|
|
@ -320,6 +270,12 @@ 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"),
|
||||||
|
|
|
||||||
407
src/osc_tap.rs
Normal file
407
src/osc_tap.rs
Normal file
|
|
@ -0,0 +1,407 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
//! Séquences OSC que vte ignore, interceptées à la sortie du PTY (yoda).
|
||||||
|
//!
|
||||||
|
//! alacritty_terminal 0.25 / vte 0.15 jettent les OSC 9 et 777 (« unhandled
|
||||||
|
//! osc_dispatch ») : l'application ne les voit jamais. On les repère donc dans
|
||||||
|
//! les octets lus sur le PTY, juste avant que vte ne les analyse, en
|
||||||
|
//! enveloppant le `Pty` : ni alacritty_terminal ni vte ne sont forkés.
|
||||||
|
//!
|
||||||
|
//! Formes reconnues, comme dans Ghostty :
|
||||||
|
//! - `OSC 777 ; notify ; titre ; corps` (forme rxvt, canal « ghostty » de
|
||||||
|
//! Claude Code) ;
|
||||||
|
//! - `OSC 9 ; texte` (forme iTerm2), sauf les commandes ConEmu
|
||||||
|
//! `OSC 9 ; N [; …]` (barre de progression `9;4` comprise), qui ne sont pas
|
||||||
|
//! des notifications.
|
||||||
|
|
||||||
|
/// Taille maximale retenue pour le contenu d'une séquence OSC ; au-delà, la
|
||||||
|
/// séquence est ignorée jusqu'à son terminateur.
|
||||||
|
const OSC_MAX_LEN: usize = 4096;
|
||||||
|
/// Longueur maximale (en caractères) du titre et du corps d'une notification.
|
||||||
|
const TITLE_MAX_CHARS: usize = 200;
|
||||||
|
const BODY_MAX_CHARS: usize = 1000;
|
||||||
|
|
||||||
|
/// Événement extrait d'une séquence OSC.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum OscEvent {
|
||||||
|
/// Notification de bureau demandée par le programme (OSC 9 ou 777).
|
||||||
|
Notify { title: Option<String>, body: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
enum State {
|
||||||
|
#[default]
|
||||||
|
Ground,
|
||||||
|
Escape,
|
||||||
|
Osc,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Automate minimal qui suit le flux d'octets du PTY et ne retient que le
|
||||||
|
/// contenu des séquences OSC. Les séquences peuvent être coupées entre deux
|
||||||
|
/// lectures : l'état est conservé d'un appel à l'autre.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct OscScanner {
|
||||||
|
state: State,
|
||||||
|
buf: Vec<u8>,
|
||||||
|
overflow: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OscScanner {
|
||||||
|
pub fn feed(&mut self, bytes: &[u8], emit: &mut dyn FnMut(OscEvent)) {
|
||||||
|
for &byte in bytes {
|
||||||
|
match self.state {
|
||||||
|
State::Ground => {
|
||||||
|
if byte == 0x1b {
|
||||||
|
self.state = State::Escape;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::Escape => {
|
||||||
|
self.state = match byte {
|
||||||
|
b']' => {
|
||||||
|
self.buf.clear();
|
||||||
|
self.overflow = false;
|
||||||
|
State::Osc
|
||||||
|
}
|
||||||
|
0x1b => State::Escape,
|
||||||
|
_ => State::Ground,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
State::Osc => match byte {
|
||||||
|
// BEL termine la séquence.
|
||||||
|
0x07 => {
|
||||||
|
self.dispatch(emit);
|
||||||
|
self.state = State::Ground;
|
||||||
|
}
|
||||||
|
// ESC termine aussi la séquence, comme dans vte : c'est le
|
||||||
|
// début de ST (`ESC \`) ou d'une nouvelle séquence.
|
||||||
|
0x1b => {
|
||||||
|
self.dispatch(emit);
|
||||||
|
self.state = State::Escape;
|
||||||
|
}
|
||||||
|
// CAN et SUB annulent la séquence.
|
||||||
|
0x18 | 0x1a => self.state = State::Ground,
|
||||||
|
_ => {
|
||||||
|
if self.buf.len() < OSC_MAX_LEN {
|
||||||
|
self.buf.push(byte);
|
||||||
|
} else {
|
||||||
|
self.overflow = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch(&mut self, emit: &mut dyn FnMut(OscEvent)) {
|
||||||
|
if !self.overflow
|
||||||
|
&& let Some(event) = parse_osc(&self.buf)
|
||||||
|
{
|
||||||
|
emit(event);
|
||||||
|
}
|
||||||
|
self.buf.clear();
|
||||||
|
self.overflow = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interprète le contenu d'une séquence OSC (entre `ESC ]` et le terminateur).
|
||||||
|
pub fn parse_osc(payload: &[u8]) -> Option<OscEvent> {
|
||||||
|
if let Some(rest) = payload.strip_prefix(b"777;notify;") {
|
||||||
|
match rest.iter().position(|&b| b == b';') {
|
||||||
|
Some(i) => notify(&rest[..i], &rest[i + 1..]),
|
||||||
|
None => notify(b"", rest),
|
||||||
|
}
|
||||||
|
} else if let Some(rest) = payload.strip_prefix(b"9;") {
|
||||||
|
if is_conemu_command(rest) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
notify(b"", rest)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `OSC 9 ; N` ou `OSC 9 ; N ; …` avec N numérique : commande ConEmu
|
||||||
|
/// (progression, titre d'onglet, répertoire…), pas une notification.
|
||||||
|
fn is_conemu_command(rest: &[u8]) -> bool {
|
||||||
|
let digits = rest.iter().take_while(|b| b.is_ascii_digit()).count();
|
||||||
|
digits > 0 && matches!(rest.get(digits), None | Some(b';'))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify(title: &[u8], body: &[u8]) -> Option<OscEvent> {
|
||||||
|
let title = clean(title, TITLE_MAX_CHARS);
|
||||||
|
let body = clean(body, BODY_MAX_CHARS);
|
||||||
|
match (title.is_empty(), body.is_empty()) {
|
||||||
|
(true, true) => None,
|
||||||
|
// Un titre seul devient le corps.
|
||||||
|
(false, true) => Some(OscEvent::Notify {
|
||||||
|
title: None,
|
||||||
|
body: title,
|
||||||
|
}),
|
||||||
|
(true, false) => Some(OscEvent::Notify { title: None, body }),
|
||||||
|
(false, false) => Some(OscEvent::Notify {
|
||||||
|
title: Some(title),
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UTF-8 tolérant, caractères de contrôle remplacés par des espaces, longueur
|
||||||
|
/// bornée.
|
||||||
|
fn clean(bytes: &[u8], max_chars: usize) -> String {
|
||||||
|
String::from_utf8_lossy(bytes)
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c.is_control() { ' ' } else { c })
|
||||||
|
.take(max_chars)
|
||||||
|
.collect::<String>()
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub use self::unix::OscTapPty;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod unix {
|
||||||
|
use super::{OscEvent, OscScanner};
|
||||||
|
use alacritty_terminal::{
|
||||||
|
event::{OnResize, WindowSize},
|
||||||
|
tty::{ChildEvent, EventedPty, EventedReadWrite, Pty},
|
||||||
|
};
|
||||||
|
use polling::{Event, PollMode, Poller};
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{self, Read},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Lecteur du PTY qui passe chaque lecture au [`OscScanner`] avant de la
|
||||||
|
/// rendre à la boucle d'alacritty_terminal.
|
||||||
|
pub struct TapReader {
|
||||||
|
file: File,
|
||||||
|
scanner: OscScanner,
|
||||||
|
sink: Box<dyn FnMut(OscEvent) + Send>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for TapReader {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
let len = self.file.read(buf)?;
|
||||||
|
self.scanner.feed(&buf[..len], &mut *self.sink);
|
||||||
|
Ok(len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Pty` d'alacritty_terminal dont la lecture est observée par un
|
||||||
|
/// [`TapReader`]. Le lecteur lit un duplicata du descripteur maître (même
|
||||||
|
/// description de fichier, donc même mode non bloquant) ; l'enregistrement
|
||||||
|
/// auprès du `Poller`, l'écriture, le redimensionnement et la fin du
|
||||||
|
/// processus restent ceux du `Pty`.
|
||||||
|
pub struct OscTapPty {
|
||||||
|
// Déclaré avant `pty` : le duplicata est fermé avant le maître.
|
||||||
|
reader: TapReader,
|
||||||
|
pty: Pty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OscTapPty {
|
||||||
|
pub fn new(pty: Pty, sink: Box<dyn FnMut(OscEvent) + Send>) -> io::Result<Self> {
|
||||||
|
let file = pty.file().try_clone()?;
|
||||||
|
Ok(Self {
|
||||||
|
reader: TapReader {
|
||||||
|
file,
|
||||||
|
scanner: OscScanner::default(),
|
||||||
|
sink,
|
||||||
|
},
|
||||||
|
pty,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventedReadWrite for OscTapPty {
|
||||||
|
type Reader = TapReader;
|
||||||
|
type Writer = File;
|
||||||
|
|
||||||
|
unsafe fn register(
|
||||||
|
&mut self,
|
||||||
|
poll: &Arc<Poller>,
|
||||||
|
interest: Event,
|
||||||
|
mode: PollMode,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
unsafe { self.pty.register(poll, interest, mode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reregister(
|
||||||
|
&mut self,
|
||||||
|
poll: &Arc<Poller>,
|
||||||
|
interest: Event,
|
||||||
|
mode: PollMode,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
self.pty.reregister(poll, interest, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deregister(&mut self, poll: &Arc<Poller>) -> io::Result<()> {
|
||||||
|
self.pty.deregister(poll)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reader(&mut self) -> &mut TapReader {
|
||||||
|
&mut self.reader
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writer(&mut self) -> &mut File {
|
||||||
|
self.pty.writer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventedPty for OscTapPty {
|
||||||
|
fn next_child_event(&mut self) -> Option<ChildEvent> {
|
||||||
|
self.pty.next_child_event()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OnResize for OscTapPty {
|
||||||
|
fn on_resize(&mut self, window_size: WindowSize) {
|
||||||
|
self.pty.on_resize(window_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn scan(chunks: &[&[u8]]) -> Vec<OscEvent> {
|
||||||
|
let mut scanner = OscScanner::default();
|
||||||
|
let mut events = Vec::new();
|
||||||
|
for chunk in chunks {
|
||||||
|
scanner.feed(chunk, &mut |event| events.push(event));
|
||||||
|
}
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify(title: Option<&str>, body: &str) -> OscEvent {
|
||||||
|
OscEvent::Notify {
|
||||||
|
title: title.map(str::to_string),
|
||||||
|
body: body.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_777_with_bel_and_st() {
|
||||||
|
assert_eq!(
|
||||||
|
scan(&[b"x\x1b]777;notify;Claude Code;En attente\x07y"]),
|
||||||
|
[notify(Some("Claude Code"), "En attente")]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
scan(&[b"\x1b]777;notify;T;corps; avec point-virgule\x1b\\"]),
|
||||||
|
[notify(Some("T"), "corps; avec point-virgule")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_text_but_not_conemu() {
|
||||||
|
assert_eq!(
|
||||||
|
scan(&[b"\x1b]9;Claude Code: termine\x07"]),
|
||||||
|
[notify(None, "Claude Code: termine")]
|
||||||
|
);
|
||||||
|
// Commandes ConEmu : progression, avec ou sans argument.
|
||||||
|
assert!(scan(&[b"\x1b]9;4;1;50\x07\x1b]9;4;0;\x07\x1b]9;12\x07"]).is_empty());
|
||||||
|
// Un chiffre suivi d'autre chose qu'un `;` reste du texte.
|
||||||
|
assert_eq!(
|
||||||
|
scan(&[b"\x1b]9;3 tests en echec\x07"]),
|
||||||
|
[notify(None, "3 tests en echec")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sequence_split_across_reads() {
|
||||||
|
assert_eq!(
|
||||||
|
scan(&[b"\x1b", b"]777;no", b"tify;Ti", b"tre;Co", b"rps\x1b", b"\\"]),
|
||||||
|
[notify(Some("Titre"), "Corps")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn other_osc_and_noise_are_ignored() {
|
||||||
|
assert!(scan(&[b"\x1b]0;titre\x07\x1b]8;;https://x\x1b\\lien\x1b]8;;\x1b\\"]).is_empty());
|
||||||
|
assert!(scan(&[b"\x1b]777;notify;;\x07\x1b]9;\x07"]).is_empty());
|
||||||
|
// CAN annule la séquence en cours.
|
||||||
|
assert!(scan(&[b"\x1b]9;annulee\x18\x07"]).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_chars_cleaned_and_title_alone_becomes_body() {
|
||||||
|
assert_eq!(
|
||||||
|
scan(&[b"\x1b]777;notify;Seul\x07"]),
|
||||||
|
[notify(None, "Seul")]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
scan(&[b"\x1b]9;a\tb\x01c\x07"]),
|
||||||
|
[notify(None, "a b c")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// De bout en bout : un vrai processus écrit dans un vrai PTY, lu par la
|
||||||
|
/// boucle d'alacritty_terminal à travers l'enveloppe.
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn tap_sees_osc_written_by_child() {
|
||||||
|
use alacritty_terminal::{
|
||||||
|
Term,
|
||||||
|
event::{Event, EventListener, WindowSize},
|
||||||
|
event_loop::EventLoop,
|
||||||
|
sync::FairMutex,
|
||||||
|
term::{Config, test::TermSize},
|
||||||
|
tty::{self, Options, Shell},
|
||||||
|
};
|
||||||
|
use std::{sync::Arc, sync::mpsc, time::Duration};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Listener;
|
||||||
|
impl EventListener for Listener {
|
||||||
|
fn send_event(&self, _event: Event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = Options {
|
||||||
|
shell: Some(Shell::new(
|
||||||
|
"printf".into(),
|
||||||
|
vec!["avant\x1b]777;notify;Titre;Corps\x07apres".into()],
|
||||||
|
)),
|
||||||
|
drain_on_exit: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let window_size = WindowSize {
|
||||||
|
num_lines: 24,
|
||||||
|
num_cols: 80,
|
||||||
|
cell_width: 8,
|
||||||
|
cell_height: 16,
|
||||||
|
};
|
||||||
|
let pty = tty::new(&options, window_size, 0).unwrap();
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
let pty = OscTapPty::new(
|
||||||
|
pty,
|
||||||
|
Box::new(move |event| {
|
||||||
|
let _ = tx.send(event);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let term = Arc::new(FairMutex::new(Term::new(
|
||||||
|
Config::default(),
|
||||||
|
&TermSize::new(80, 24),
|
||||||
|
Listener,
|
||||||
|
)));
|
||||||
|
let event_loop = EventLoop::new(term, Listener, pty, true, false).unwrap();
|
||||||
|
let _join = event_loop.spawn();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rx.recv_timeout(Duration::from_secs(5)).unwrap(),
|
||||||
|
notify(Some("Titre"), "Corps")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oversized_sequence_is_dropped() {
|
||||||
|
let mut long = b"\x1b]9;".to_vec();
|
||||||
|
long.extend(std::iter::repeat_n(b'a', OSC_MAX_LEN + 10));
|
||||||
|
long.extend(b"\x07\x1b]9;ensuite\x07");
|
||||||
|
assert_eq!(scan(&[&long]), [notify(None, "ensuite")]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -89,7 +89,10 @@ pub enum KeyBindAction {
|
||||||
TabNext,
|
TabNext,
|
||||||
TabPrev,
|
TabPrev,
|
||||||
TabRename,
|
TabRename,
|
||||||
|
TabMoveLeft,
|
||||||
|
TabMoveRight,
|
||||||
ToggleFullscreen,
|
ToggleFullscreen,
|
||||||
|
ToggleReadOnly,
|
||||||
WindowClose,
|
WindowClose,
|
||||||
WindowNew,
|
WindowNew,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
|
|
@ -131,7 +134,10 @@ 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),
|
||||||
|
|
@ -294,7 +300,10 @@ 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"),
|
||||||
|
|
@ -329,6 +338,8 @@ 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,
|
||||||
|
|
@ -369,7 +380,11 @@ pub fn shortcut_groups() -> Vec<ShortcutGroup> {
|
||||||
KeyBindAction::ZoomReset,
|
KeyBindAction::ZoomReset,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
let mut other_actions = vec![KeyBindAction::ClearScrollback, KeyBindAction::SaveOutput];
|
let mut other_actions = vec![
|
||||||
|
KeyBindAction::ClearScrollback,
|
||||||
|
KeyBindAction::SaveOutput,
|
||||||
|
KeyBindAction::ToggleReadOnly,
|
||||||
|
];
|
||||||
#[cfg(feature = "password_manager")]
|
#[cfg(feature = "password_manager")]
|
||||||
other_actions.push(KeyBindAction::PasswordManager);
|
other_actions.push(KeyBindAction::PasswordManager);
|
||||||
groups.push(ShortcutGroup {
|
groups.push(ShortcutGroup {
|
||||||
|
|
@ -479,6 +494,10 @@ 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);
|
||||||
|
|
|
||||||
328
src/terminal.rs
328
src/terminal.rs
|
|
@ -27,13 +27,15 @@ use indexmap::IndexSet;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs, io, mem,
|
fs,
|
||||||
|
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::Instant,
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
|
@ -43,6 +45,7 @@ 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.
|
||||||
|
|
@ -65,6 +68,28 @@ 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://)\
|
||||||
|
|
@ -109,13 +134,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, Event)>,
|
mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>,
|
||||||
);
|
);
|
||||||
|
|
||||||
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, event));
|
let _ = self.2.send((self.0, self.1, PtyEvent::Term(event)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,34 +221,63 @@ impl TerminalPaneGrid {
|
||||||
self.focus
|
self.focus
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_terminal_focus(&self) {
|
pub fn update_terminal_focus(&mut self) {
|
||||||
for (pane, tab_model) in self.panes.panes.iter() {
|
let focus = self.focus;
|
||||||
let entity = tab_model.active();
|
for (pane, tab_model) in self.panes.panes.iter_mut() {
|
||||||
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
let active = tab_model.active();
|
||||||
let mut terminal = terminal.lock().unwrap();
|
let entities: Vec<segmented_button::Entity> = tab_model.iter().collect();
|
||||||
terminal.set_focused(self.focus == *pane);
|
for entity in entities {
|
||||||
terminal.update();
|
let mut icon_update = None;
|
||||||
|
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 (_pane, tab_model) in self.panes.panes.iter() {
|
for tab_model in self.panes.panes.values() {
|
||||||
|
let active = tab_model.active();
|
||||||
for entity in tab_model.iter() {
|
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);
|
||||||
terminal.update();
|
if entity == active {
|
||||||
|
terminal.update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
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 {
|
||||||
|
|
@ -233,6 +287,8 @@ impl Metadata {
|
||||||
bg,
|
bg,
|
||||||
underline_color,
|
underline_color,
|
||||||
flags,
|
flags,
|
||||||
|
block: None,
|
||||||
|
block_color: underline_color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,6 +302,73 @@ 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 {
|
||||||
|
|
@ -257,6 +380,11 @@ 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>,
|
||||||
|
|
@ -287,7 +415,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, Event)>,
|
event_tx: mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>,
|
||||||
config: Config,
|
config: Config,
|
||||||
options: Options,
|
options: Options,
|
||||||
app_config: &AppConfig,
|
app_config: &AppConfig,
|
||||||
|
|
@ -339,6 +467,8 @@ 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,
|
||||||
|
|
@ -353,6 +483,15 @@ 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());
|
||||||
|
|
@ -376,6 +515,9 @@ 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,
|
||||||
|
|
@ -439,6 +581,25 @@ 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;
|
||||||
|
|
@ -450,7 +611,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.input_no_scroll(input);
|
self.respond(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -463,7 +624,18 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -481,6 +653,10 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -906,9 +1082,14 @@ 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() {
|
||||||
|
|
@ -1026,6 +1207,7 @@ 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);
|
||||||
|
|
@ -1136,8 +1318,11 @@ impl Terminal {
|
||||||
y,
|
y,
|
||||||
);
|
);
|
||||||
|
|
||||||
for code in codes {
|
// Saisie de l'utilisateur : rien en lecture seule
|
||||||
self.notifier.notify(code);
|
if !self.read_only {
|
||||||
|
for code in codes {
|
||||||
|
self.notifier.notify(code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.scroll_as_arrows(delta);
|
self.scroll_as_arrows(delta);
|
||||||
|
|
@ -1345,3 +1530,108 @@ impl Drop for Terminal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Lecture seule, de bout en bout : un vrai `cat` dans un vrai PTY. La
|
||||||
|
/// saisie bloquée n'arrive pas ; une réponse du terminal, si ; la saisie
|
||||||
|
/// repasse quand le verrou est levé.
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn read_only_blocks_input_but_not_replies() {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
let (_panes, pane) = pane_grid::State::new(());
|
||||||
|
let mut model = segmented_button::Model::<segmented_button::SingleSelect>::default();
|
||||||
|
let entity = model.insert().id();
|
||||||
|
let (event_tx, _event_rx) = mpsc::unbounded_channel();
|
||||||
|
let options = Options {
|
||||||
|
shell: Some(tty::Shell::new("cat".into(), Vec::new())),
|
||||||
|
..Options::default()
|
||||||
|
};
|
||||||
|
let mut terminal = Terminal::new(
|
||||||
|
pane,
|
||||||
|
entity,
|
||||||
|
event_tx,
|
||||||
|
Config::default(),
|
||||||
|
options,
|
||||||
|
&AppConfig::default(),
|
||||||
|
crate::terminal_theme::cosmic_dark(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let wait_for = |terminal: &Terminal, needle: &str| {
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
|
while Instant::now() < deadline {
|
||||||
|
if terminal.scrollback_text().contains(needle) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
terminal.read_only = true;
|
||||||
|
terminal.input_scroll(&b"bloque\n"[..]);
|
||||||
|
terminal.paste("colle\n".to_string());
|
||||||
|
terminal.respond(&b"repond\n"[..]);
|
||||||
|
assert!(wait_for(&terminal, "repond"));
|
||||||
|
// Écrites avant la réponse, la saisie et le collage seraient déjà là
|
||||||
|
let text = terminal.scrollback_text();
|
||||||
|
assert!(!text.contains("bloque"), "{text}");
|
||||||
|
assert!(!text.contains("colle"), "{text}");
|
||||||
|
|
||||||
|
terminal.read_only = false;
|
||||||
|
terminal.input_scroll(&b"libre\n"[..]);
|
||||||
|
assert!(wait_for(&terminal, "libre"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_fill_couvre_toute_la_plage_unicode() {
|
||||||
|
// U+2580–U+259F : chaque élément de bloc a sa géométrie native.
|
||||||
|
for code in 0x2580..=0x259F {
|
||||||
|
let c = char::from_u32(code).unwrap();
|
||||||
|
assert!(
|
||||||
|
block_fill(c).is_some(),
|
||||||
|
"{c} (U+{code:04X}) sans remplissage"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Hors de la plage : filets, espaces, lettres — pas de remplissage.
|
||||||
|
for c in [' ', '─', '│', 'M', '●', '\u{25A0}'] {
|
||||||
|
assert!(
|
||||||
|
block_fill(c).is_none(),
|
||||||
|
"{c} ne doit pas être un bloc natif"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_fill_geometry_toujours_valide() {
|
||||||
|
// Fractions croissantes, bornées à la cellule — sinon le quad
|
||||||
|
// déborderait sur la cellule voisine ou serait rejeté au tracé.
|
||||||
|
for c in (char::from_u32(0x2580).unwrap()..=char::from_u32(0x259F).unwrap())
|
||||||
|
.chain(['░', '▒', '▓'].iter().copied())
|
||||||
|
{
|
||||||
|
let Some(fill) = block_fill(c) else { continue };
|
||||||
|
match fill {
|
||||||
|
BlockFill::Rects(rects) => {
|
||||||
|
for (x0, x1, y0, y1) in rects {
|
||||||
|
// Rectangle vide (sentinelle) ou borné à la cellule.
|
||||||
|
let empty = x0 >= x1 || y0 >= y1;
|
||||||
|
assert!(
|
||||||
|
empty || (x1 <= 8 && y1 <= 8),
|
||||||
|
"{c}: géométrie déborde la cellule"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BlockFill::Shade(quarters) => {
|
||||||
|
assert!((1..=3).contains(&quarters), "{c}: alpha de trame invalide")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ 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,
|
||||||
|
|
@ -45,7 +46,11 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{Action, Terminal, TerminalScroll, menu::MenuState, terminal::Metadata};
|
use crate::{
|
||||||
|
Action, Terminal, TerminalScroll, kitty_keyboard,
|
||||||
|
menu::MenuState,
|
||||||
|
terminal::{BlockFill, Metadata},
|
||||||
|
};
|
||||||
|
|
||||||
const AUTOSCROLL_INTERVAL: Duration = Duration::from_millis(100);
|
const AUTOSCROLL_INTERVAL: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
|
|
@ -109,13 +114,16 @@ 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: Option<Point>,
|
context_menu_open: bool,
|
||||||
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>>,
|
||||||
|
|
@ -135,13 +143,16 @@ 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: None,
|
context_menu_open: false,
|
||||||
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,
|
||||||
|
|
@ -171,13 +182,18 @@ 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(mut self, position: Point) -> Self {
|
pub fn context_menu_open(mut self, open: bool) -> Self {
|
||||||
self.context_menu = Some(position);
|
self.context_menu_open = open;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,6 +210,14 @@ 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
|
||||||
|
|
@ -204,6 +228,13 @@ 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
|
||||||
|
|
@ -241,7 +272,7 @@ where
|
||||||
if !state.is_focused {
|
if !state.is_focused {
|
||||||
return InputMethod::Disabled;
|
return InputMethod::Disabled;
|
||||||
}
|
}
|
||||||
if self.context_menu.is_some() {
|
if self.context_menu_open {
|
||||||
return InputMethod::Disabled;
|
return InputMethod::Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,13 +440,22 @@ 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();
|
||||||
// matches the corners to the window border
|
let corner_radius = if let Some(r) = self.pane_border_radius {
|
||||||
let corner_radius = if self.sharp_corners {
|
r.into()
|
||||||
|
} else if self.sharp_corners {
|
||||||
|
// matches the corners to the window border
|
||||||
cosmic_theme.radius_0()
|
cosmic_theme.radius_0()
|
||||||
} else {
|
} else {
|
||||||
cosmic_theme.radius_s()
|
cosmic_theme
|
||||||
}
|
.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();
|
||||||
|
|
@ -429,16 +469,11 @@ 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: if self.show_headerbar {
|
radius: border_radius,
|
||||||
[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,
|
||||||
},
|
},
|
||||||
|
|
@ -446,12 +481,12 @@ where
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Color::from_rgba(
|
Color::from_rgba(
|
||||||
f32::from(background_color.r()) / 255.0,
|
f32::from(meta.bg.r()) / 255.0,
|
||||||
f32::from(background_color.g()) / 255.0,
|
f32::from(meta.bg.g()) / 255.0,
|
||||||
f32::from(background_color.b()) / 255.0,
|
f32::from(meta.bg.b()) / 255.0,
|
||||||
match self.opacity {
|
match self.opacity {
|
||||||
Some(opacity) => opacity,
|
Some(opacity) => opacity,
|
||||||
None => f32::from(background_color.a()) / 255.0,
|
None => f32::from(meta.bg.a()) / 255.0,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -472,17 +507,11 @@ where
|
||||||
// Render default background
|
// 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: if self.show_headerbar {
|
radius: border_radius,
|
||||||
[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,
|
||||||
},
|
},
|
||||||
|
|
@ -490,12 +519,12 @@ where
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Color::from_rgba(
|
Color::from_rgba(
|
||||||
f32::from(background_color.r()) / 255.0,
|
f32::from(meta.bg.r()) / 255.0,
|
||||||
f32::from(background_color.g()) / 255.0,
|
f32::from(meta.bg.g()) / 255.0,
|
||||||
f32::from(background_color.b()) / 255.0,
|
f32::from(meta.bg.b()) / 255.0,
|
||||||
match self.opacity {
|
match self.opacity {
|
||||||
Some(opacity) => opacity,
|
Some(opacity) => opacity,
|
||||||
None => f32::from(background_color.a()) / 255.0,
|
None => f32::from(meta.bg.a()) / 255.0,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -537,7 +566,7 @@ where
|
||||||
fn fill<Renderer: renderer::Renderer>(
|
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(
|
||||||
|
|
@ -551,7 +580,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,
|
(self.start_x + $x_offset).floor(),
|
||||||
self.line_top + self.line_height - $bottom_offset,
|
self.line_top + self.line_height - $bottom_offset,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -568,19 +597,57 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
($pos_offset:expr, $style_line_height:expr) => {
|
($pos_offset:expr, $style_line_height:expr) => {
|
||||||
mk_quad!($pos_offset, $style_line_height, self.end_x - self.start_x)
|
mk_quad!(
|
||||||
|
$pos_offset,
|
||||||
|
$style_line_height,
|
||||||
|
(self.end_x - self.start_x).ceil()
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = &self.metadata_set[self.metadata];
|
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(color),
|
cosmic_text_to_iced_color(metadata.bg),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Éléments de bloc peints nativement : fractions de la
|
||||||
|
// cellule (ou de la course fusionnée, à géométrie
|
||||||
|
// séparable), bords calés sur la grille — zéro glyphe,
|
||||||
|
// zéro interligne.
|
||||||
|
if let Some(block) = metadata.block {
|
||||||
|
let span_w = self.end_x - self.start_x;
|
||||||
|
let mut color = cosmic_text_to_iced_color(metadata.block_color);
|
||||||
|
let rects = match block {
|
||||||
|
BlockFill::Rects(rects) => rects,
|
||||||
|
BlockFill::Shade(quarters) => {
|
||||||
|
color.a *= f32::from(quarters) / 4.0;
|
||||||
|
[(0, 8, 0, 8), (8, 8, 8, 8)]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (x0, x1, y0, y1) in rects {
|
||||||
|
if x0 >= x1 || y0 >= y1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let left = self.start_x + span_w * f32::from(x0) / 8.0;
|
||||||
|
let right = self.start_x + span_w * f32::from(x1) / 8.0;
|
||||||
|
let top = self.line_top + self.line_height * f32::from(y0) / 8.0;
|
||||||
|
let bottom = self.line_top + self.line_height * f32::from(y1) / 8.0;
|
||||||
|
renderer.fill_quad(
|
||||||
|
Quad {
|
||||||
|
bounds: Rectangle::new(
|
||||||
|
self.view_position + Vector::new(left.floor(), top),
|
||||||
|
Size::new(right.ceil() - left.floor(), bottom - top),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !metadata.flags.is_empty() {
|
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);
|
||||||
|
|
||||||
|
|
@ -756,7 +823,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(false)
|
.background(theme.transparent)
|
||||||
.component
|
.component
|
||||||
.pressed
|
.pressed
|
||||||
.without_alpha()
|
.without_alpha()
|
||||||
|
|
@ -766,7 +833,7 @@ where
|
||||||
} else if hover {
|
} else if hover {
|
||||||
// hover_state_color, 0.2
|
// hover_state_color, 0.2
|
||||||
cosmic_theme
|
cosmic_theme
|
||||||
.background(false)
|
.background(theme.transparent)
|
||||||
.component
|
.component
|
||||||
.hover
|
.hover
|
||||||
.without_alpha()
|
.without_alpha()
|
||||||
|
|
@ -867,6 +934,32 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [yoda] Panneau non focalisé : voile de la couleur du fond, comme
|
||||||
|
// `unfocused-split-opacity` de Ghostty. Calque à part, sinon le texte
|
||||||
|
// (primitive du même calque) serait tracé par-dessus.
|
||||||
|
if let Some(dim) = self.dim {
|
||||||
|
let bg = terminal.metadata_set[terminal.default_attrs().metadata].bg;
|
||||||
|
renderer.with_layer(layout.bounds(), |renderer| {
|
||||||
|
renderer.fill_quad(
|
||||||
|
Quad {
|
||||||
|
bounds: layout.bounds(),
|
||||||
|
border: Border {
|
||||||
|
radius: border_radius,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
snap: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Color::from_rgba(
|
||||||
|
f32::from(bg.r()) / 255.0,
|
||||||
|
f32::from(bg.g()) / 255.0,
|
||||||
|
f32::from(bg.b()) / 255.0,
|
||||||
|
dim,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let duration = instant.elapsed();
|
let duration = instant.elapsed();
|
||||||
log::trace!("redraw {}, {}: {:?}", view_w, view_h, duration);
|
log::trace!("redraw {}, {}: {:?}", view_w, view_h, duration);
|
||||||
}
|
}
|
||||||
|
|
@ -891,7 +984,11 @@ 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);
|
||||||
let is_mouse_mode = terminal.term.lock().mode().intersects(TermMode::MOUSE_MODE);
|
// [yoda] En lecture seule, la souris revient au terminal : sélection et
|
||||||
|
// défilement locaux, même dans une application qui la capture.
|
||||||
|
let is_mouse_mode = terminal.term.lock().mode().intersects(TermMode::MOUSE_MODE)
|
||||||
|
&& !terminal.read_only;
|
||||||
|
let term_mode = *terminal.term.lock().mode();
|
||||||
match event {
|
match event {
|
||||||
Event::Window(event) => match event {
|
Event::Window(event) => match event {
|
||||||
cosmic::iced::window::Event::Focused => {
|
cosmic::iced::window::Event::Focused => {
|
||||||
|
|
@ -931,6 +1028,101 @@ where
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
// [yoda] Protocole clavier kitty, quand l'application au premier plan
|
||||||
|
// l'a demandé (voir `kitty_keyboard`) ; sinon encodage legacy plus bas.
|
||||||
|
Event::Keyboard(KeyEvent::KeyPressed {
|
||||||
|
key,
|
||||||
|
modified_key,
|
||||||
|
physical_key,
|
||||||
|
location,
|
||||||
|
modifiers,
|
||||||
|
text,
|
||||||
|
repeat,
|
||||||
|
}) if state.is_focused && kitty_keyboard::active(term_mode) => {
|
||||||
|
for key_bind in self.key_binds.keys() {
|
||||||
|
if key_bind.matches(*modifiers, key, Some(physical_key)) {
|
||||||
|
shell.capture_event();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comportements locaux conservés, comme en legacy : Shift+PageUp…
|
||||||
|
// fait défiler l'historique, Échap annule une sélection.
|
||||||
|
let local_scroll = match modified_key {
|
||||||
|
Key::Named(Named::PageUp) if modifiers.shift() => Some(TerminalScroll::PageUp),
|
||||||
|
Key::Named(Named::PageDown) if modifiers.shift() => {
|
||||||
|
Some(TerminalScroll::PageDown)
|
||||||
|
}
|
||||||
|
Key::Named(Named::Home) if modifiers.shift() => Some(TerminalScroll::Top),
|
||||||
|
Key::Named(Named::End) if modifiers.shift() => Some(TerminalScroll::Bottom),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(scroll) = local_scroll {
|
||||||
|
terminal.scroll(scroll);
|
||||||
|
shell.capture_event();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if *modified_key == Key::Named(Named::Escape) {
|
||||||
|
let had_selection = terminal.term.lock().selection.take().is_some();
|
||||||
|
if had_selection {
|
||||||
|
terminal.update();
|
||||||
|
shell.capture_event();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = kitty_keyboard::KeyInput {
|
||||||
|
key,
|
||||||
|
modified_key,
|
||||||
|
location: *location,
|
||||||
|
modifiers: *modifiers,
|
||||||
|
text: text.as_deref(),
|
||||||
|
action: if *repeat {
|
||||||
|
kitty_keyboard::KeyAction::Repeat
|
||||||
|
} else {
|
||||||
|
kitty_keyboard::KeyAction::Press
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let bytes = kitty_keyboard::encode(&input, term_mode);
|
||||||
|
if !bytes.is_empty() {
|
||||||
|
// Une touche de modification seule (« toutes les touches »)
|
||||||
|
// ne ramène pas la vue en bas.
|
||||||
|
let modifier_only = matches!(
|
||||||
|
modified_key,
|
||||||
|
Key::Named(Named::Shift | Named::Control | Named::Alt | Named::Super)
|
||||||
|
);
|
||||||
|
if modifier_only {
|
||||||
|
terminal.input_no_scroll(bytes);
|
||||||
|
} else {
|
||||||
|
terminal.input_scroll(bytes);
|
||||||
|
}
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Keyboard(KeyEvent::KeyReleased {
|
||||||
|
key,
|
||||||
|
modified_key,
|
||||||
|
location,
|
||||||
|
modifiers,
|
||||||
|
..
|
||||||
|
}) if state.is_focused
|
||||||
|
&& kitty_keyboard::active(term_mode)
|
||||||
|
&& term_mode.contains(TermMode::REPORT_EVENT_TYPES) =>
|
||||||
|
{
|
||||||
|
let input = kitty_keyboard::KeyInput {
|
||||||
|
key,
|
||||||
|
modified_key,
|
||||||
|
location: *location,
|
||||||
|
modifiers: *modifiers,
|
||||||
|
text: None,
|
||||||
|
action: kitty_keyboard::KeyAction::Release,
|
||||||
|
};
|
||||||
|
let bytes = kitty_keyboard::encode(&input, term_mode);
|
||||||
|
if !bytes.is_empty() {
|
||||||
|
terminal.input_no_scroll(bytes);
|
||||||
|
shell.capture_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
Event::Keyboard(KeyEvent::KeyPressed {
|
Event::Keyboard(KeyEvent::KeyPressed {
|
||||||
key: Key::Named(named),
|
key: Key::Named(named),
|
||||||
physical_key,
|
physical_key,
|
||||||
|
|
@ -1046,7 +1238,15 @@ where
|
||||||
shell.capture_event();
|
shell.capture_event();
|
||||||
}
|
}
|
||||||
Named::Enter => {
|
Named::Enter => {
|
||||||
terminal.input_scroll(format!("{}{}", alt_prefix, "\x0D").into_bytes());
|
// [yoda] Shift+Entrée envoie LF (`\n`, comme Ctrl+J) : saut de
|
||||||
|
// ligne dans les éditeurs qui ne parlent pas le protocole kitty
|
||||||
|
// (rustyline de Modjo/Goose) ; pour un shell, c'est Entrée.
|
||||||
|
let code = if modifiers.shift() && !modifiers.control() {
|
||||||
|
"\x0A"
|
||||||
|
} else {
|
||||||
|
"\x0D"
|
||||||
|
};
|
||||||
|
terminal.input_scroll(format!("{alt_prefix}{code}").into_bytes());
|
||||||
shell.capture_event();
|
shell.capture_event();
|
||||||
}
|
}
|
||||||
Named::Escape => {
|
Named::Escape => {
|
||||||
|
|
@ -1344,11 +1544,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 {
|
match self.context_menu_open {
|
||||||
Some(_) => {
|
true => {
|
||||||
shell.publish(on_context_menu(None));
|
shell.publish(on_context_menu(None));
|
||||||
}
|
}
|
||||||
None => {
|
false => {
|
||||||
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;
|
||||||
|
|
@ -1366,23 +1566,13 @@ where
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
let link = get_hyperlink(&terminal, location);
|
let link = get_hyperlink(&terminal, location);
|
||||||
let abs = cosmic::iced::Point::new(
|
shell.publish(on_context_menu(Some(MenuState { link })));
|
||||||
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)) => {
|
||||||
|
|
@ -1401,6 +1591,9 @@ 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;
|
||||||
|
|
@ -1608,6 +1801,10 @@ where
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if terminal.needs_update {
|
||||||
|
shell.request_redraw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1754,20 +1951,6 @@ fn accumulate_wheel_lines(y: f32, accumulator: f32) -> (i32, f32) {
|
||||||
(lines, total - lines as f32)
|
(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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue