diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e6ca28b..63abe52 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,10 @@ -- [ ] I have disclosed use of any AI generated code in my commit messages. - - If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR. - - In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment. -- [ ] I understand these changes in full and will be able to respond to review comments. -- [ ] My change is accurately described in the commit message. -- [ ] My contribution is tested and working as described. -- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions. +Replace this text with a description of your changes, and why these changes are important. Read and acknowledge the list below. PRs without a completed checkbox will be closed. +--- + +- I have not included any LLM (also known as AI) generated content in this PR, including code, comments, and descriptions. +- I understand these changes in full and will be able to respond to review comments. +- My change is accurately described in the commit message. +- My contribution is tested and working as described. +- I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions. +- [ ] I have read and followed all of the above requirements. For more information, see our [contributor guide](https://github.com/pop-os/pop/blob/master/CONTRIBUTING.md). diff --git a/.github/workflows/require-checklist.yml b/.github/workflows/require-checklist.yml new file mode 100644 index 0000000..fd4ca8c --- /dev/null +++ b/.github/workflows/require-checklist.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index a8eed3d..6f0f6cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,7 +55,7 @@ dependencies = [ "accesskit", "accesskit_consumer", "hashbrown 0.16.1", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -156,7 +156,7 @@ dependencies = [ "piper", "polling", "regex-automata", - "rustix 1.1.4", + "rustix 1.1.5", "rustix-openpty", "serde", "signal-hook", @@ -232,7 +232,7 @@ dependencies = [ "ndk-context", "ndk-sys", "num_enum", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -416,7 +416,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.4", + "rustix 1.1.5", "slab", "windows-sys 0.61.2", ] @@ -447,18 +447,18 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.4", + "rustix 1.1.5", ] [[package]] name = "async-recursion" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -473,7 +473,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.4", + "rustix 1.1.5", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -493,7 +493,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -564,7 +564,7 @@ dependencies = [ "derive_utils", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -588,7 +588,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.20", + "thiserror 2.0.21", "v_frame", "y4m", ] @@ -722,7 +722,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2 0.5.2", + "objc2 0.5.3", ] [[package]] @@ -731,7 +731,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -829,7 +829,7 @@ checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -871,14 +871,13 @@ dependencies = [ [[package]] name = "calloop" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" dependencies = [ "bitflags 2.13.2", "polling", - "rustix 1.1.4", - "slab", + "rustix 1.1.5", "tracing", ] @@ -889,7 +888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ "calloop", - "rustix 1.1.4", + "rustix 1.1.5", "wayland-backend", "wayland-client", ] @@ -905,9 +904,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.6" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" dependencies = [ "find-msvc-tools", "jobserver", @@ -928,9 +927,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" @@ -1338,7 +1337,7 @@ dependencies = [ [[package]] name = "cosmic-files" -version = "1.9.0" +version = "1.10.0" dependencies = [ "anyhow", "atomic_float", @@ -1381,7 +1380,7 @@ dependencies = [ "slotmap", "tar", "tempfile", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tracing", "tracing-subscriber", @@ -1403,7 +1402,7 @@ dependencies = [ "btoi", "memchr", "memmap2 0.9.11", - "thiserror 2.0.20", + "thiserror 2.0.21", "tracing", "xdg", ] @@ -1430,7 +1429,7 @@ dependencies = [ [[package]] name = "cosmic-term" -version = "1.9.0" +version = "1.10.0" dependencies = [ "alacritty_terminal", "clap_lex", @@ -1446,6 +1445,7 @@ dependencies = [ "log", "open", "paste", + "polling", "regex", "reqwest", "ron 0.11.0", @@ -1455,10 +1455,11 @@ dependencies = [ "serde", "serde_json", "shlex 1.3.0", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "url", "xdgen", + "zbus", ] [[package]] @@ -1498,7 +1499,7 @@ dependencies = [ "ron 0.12.2", "serde", "serde_json", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -1712,7 +1713,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -1741,7 +1742,7 @@ checksum = "dc05a5d33db20c784f873e84934ad94bb209a090987ac5f62fede2c178234f23" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1815,7 +1816,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", + "redox_users 0.5.3", "windows-sys 0.61.2", ] @@ -1839,7 +1840,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.6.2", "libc", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -1850,7 +1851,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -2088,9 +2089,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "fixed_decimal" @@ -2177,7 +2178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ "memchr", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -2218,9 +2219,9 @@ dependencies = [ [[package]] name = "font-types" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6" +checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274" dependencies = [ "bytemuck", ] @@ -2275,7 +2276,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -2405,7 +2406,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -2439,9 +2440,9 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +checksum = "54ade96dc9003043bce7c035c85a9df5a858bfb2039c5a2e6fdf00f324f6c551" dependencies = [ "cc", "cfg-if", @@ -2585,7 +2586,7 @@ dependencies = [ "hashbrown 0.16.1", "log", "presser", - "thiserror 2.0.20", + "thiserror 2.0.21", "windows 0.62.2", ] @@ -2627,9 +2628,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" dependencies = [ "atomic-waker", "bytes", @@ -2807,9 +2808,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", @@ -2991,7 +2992,7 @@ dependencies = [ "iced_winit", "image", "mime 0.1.0", - "thiserror 2.0.20", + "thiserror 2.0.21", "window_clipboard", ] @@ -3022,7 +3023,7 @@ dependencies = [ "rustc-hash 2.1.3", "serde", "smol_str", - "thiserror 2.0.20", + "thiserror 2.0.21", "unicode-segmentation", "web-time", "window_clipboard", @@ -3066,7 +3067,7 @@ dependencies = [ "lyon_path", "raw-window-handle", "rustc-hash 2.1.3", - "thiserror 2.0.20", + "thiserror 2.0.21", "unicode-segmentation", ] @@ -3086,7 +3087,7 @@ dependencies = [ "iced_tiny_skia", "iced_wgpu", "log", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -3100,7 +3101,7 @@ dependencies = [ "iced_core", "iced_futures", "raw-window-handle", - "thiserror 2.0.20", + "thiserror 2.0.21", "window_clipboard", ] @@ -3140,7 +3141,7 @@ dependencies = [ "resvg", "rustc-hash 2.1.3", "rustix 0.38.44", - "thiserror 2.0.20", + "thiserror 2.0.21", "wayland-backend", "wayland-client", "wayland-protocols", @@ -3161,7 +3162,7 @@ dependencies = [ "num-traits", "ouroboros", "rustc-hash 2.1.3", - "thiserror 2.0.20", + "thiserror 2.0.21", "unicode-segmentation", "window_clipboard", ] @@ -3183,7 +3184,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 2.1.3", "rustix 0.38.44", - "thiserror 2.0.20", + "thiserror 2.0.21", "wasm-bindgen-futures", "wayland-backend", "wayland-client", @@ -3616,9 +3617,9 @@ dependencies = [ [[package]] name = "icu_time_data" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ff86fa5851a77fc0a977b72d717f777bd9230415a247219d7898aefb1517a" +checksum = "d9e21be527807cda7562cbfbdb04179f9cca46c12691d6a8073ffae6cfd7aeec" [[package]] name = "ident_case" @@ -3913,7 +3914,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.20", + "thiserror 2.0.21", "walkdir", "windows-link 0.2.1", ] @@ -3971,9 +3972,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", @@ -4248,9 +4249,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] name = "lebe" @@ -4266,9 +4267,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libcosmic-yoda" @@ -4310,7 +4311,7 @@ dependencies = [ "serde", "slotmap", "taffy", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tracing", "unicode-segmentation", @@ -4346,9 +4347,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ "bitflags 2.13.2", "libc", @@ -4637,9 +4638,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "log", @@ -4700,7 +4701,7 @@ dependencies = [ "once_cell", "rustc-hash 1.1.0", "spirv", - "thiserror 2.0.20", + "thiserror 2.0.21", "unicode-ident", ] @@ -4991,9 +4992,9 @@ checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] name = "objc2" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" dependencies = [ "objc-sys", "objc2-encode", @@ -5001,9 +5002,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" dependencies = [ "objc2-encode", ] @@ -5017,7 +5018,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.5.1", "libc", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-core-data", "objc2-core-image", "objc2-foundation 0.2.2", @@ -5032,7 +5033,7 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.2", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", ] @@ -5045,7 +5046,7 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.13.2", "block2 0.5.1", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-foundation 0.2.2", ] @@ -5058,7 +5059,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.6.2", "dispatch2", - "objc2 0.6.4", + "objc2 0.6.5", ] [[package]] @@ -5079,7 +5080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2 0.5.1", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-foundation 0.2.2", "objc2-metal", ] @@ -5110,7 +5111,7 @@ dependencies = [ "bitflags 2.13.2", "block2 0.5.1", "libc", - "objc2 0.5.2", + "objc2 0.5.3", ] [[package]] @@ -5121,7 +5122,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.2", "block2 0.6.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", ] @@ -5133,7 +5134,7 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.13.2", "block2 0.5.1", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-foundation 0.2.2", ] @@ -5145,7 +5146,7 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.13.2", "block2 0.5.1", - "objc2 0.5.2", + "objc2 0.5.3", "objc2-foundation 0.2.2", "objc2-metal", ] @@ -5157,7 +5158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.13.2", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", ] @@ -5486,9 +5487,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "13bee6c73da26345c729282832b60b0363cf3dd9f4bfd81d8551b7a1c889a113" [[package]] name = "piper" @@ -5549,7 +5550,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.4", + "rustix 1.1.5", "windows-sys 0.61.2", ] @@ -5587,9 +5588,9 @@ dependencies = [ [[package]] name = "powerfmt" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" [[package]] name = "ppmd-rust" @@ -5640,7 +5641,7 @@ dependencies = [ "proc-macro-error-attr3", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -5675,7 +5676,7 @@ dependencies = [ "chrono", "flate2", "procfs-core", - "rustix 1.1.4", + "rustix 1.1.5", ] [[package]] @@ -5893,7 +5894,7 @@ dependencies = [ "rand 0.9.5", "rand_chacha 0.9.0", "simd_helpers", - "thiserror 2.0.20", + "thiserror 2.0.21", "v_frame", "wasm-bindgen", ] @@ -5966,7 +5967,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" dependencies = [ "bytemuck", - "font-types 0.12.5", + "font-types 0.12.6", "once_cell", ] @@ -6033,13 +6034,12 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" dependencies = [ - "getrandom 0.2.17", "libredox", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -6148,7 +6148,7 @@ dependencies = [ "dispatch2", "js-sys", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -6288,9 +6288,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ "bitflags 2.13.2", "errno", @@ -6307,7 +6307,7 @@ checksum = "1de16c7c59892b870a6336f185dc10943517f1327447096bbb7bb32cd85e2393" dependencies = [ "errno", "libc", - "rustix 1.1.4", + "rustix 1.1.5", ] [[package]] @@ -6506,7 +6506,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -6531,7 +6531,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -6662,9 +6662,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" [[package]] name = "skrifa" @@ -6703,9 +6703,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.16.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" [[package]] name = "smithay-client-toolkit" @@ -6722,8 +6722,8 @@ dependencies = [ "log", "memmap2 0.9.11", "pkg-config", - "rustix 1.1.4", - "thiserror 2.0.20", + "rustix 1.1.5", + "thiserror 2.0.21", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -6883,9 +6883,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -6913,13 +6913,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -6984,7 +6984,7 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix 1.1.4", + "rustix 1.1.5", "windows-sys 0.61.2", ] @@ -7014,11 +7014,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl 2.0.20", + "thiserror-impl 2.0.21", ] [[package]] @@ -7034,13 +7034,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -7156,9 +7156,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.1" +version = "1.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" dependencies = [ "bytes", "libc", @@ -7179,7 +7179,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -7194,9 +7194,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ "rustls", "tokio", @@ -7215,9 +7215,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" dependencies = [ "bytes", "futures-core", @@ -7382,7 +7382,7 @@ dependencies = [ "chrono", "libc", "log", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-foundation 0.3.2", "once_cell", "percent-encoding", @@ -7474,9 +7474,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" [[package]] name = "unicode-bidi" @@ -7498,9 +7498,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "unicode-linebreak" @@ -7604,9 +7604,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.26.1" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" dependencies = [ "js-sys", "serde_core", @@ -7678,9 +7678,9 @@ dependencies = [ [[package]] name = "want" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" dependencies = [ "try-lock", ] @@ -7702,9 +7702,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -7715,19 +7715,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.78" +version = "0.4.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" dependencies = [ "js-sys", + "tokio", "wasm-bindgen", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7735,22 +7736,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] @@ -7790,7 +7791,7 @@ checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.4", + "rustix 1.1.5", "scoped-tls", "smallvec", "wayland-sys", @@ -7803,7 +7804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" dependencies = [ "bitflags 2.13.2", - "rustix 1.1.4", + "rustix 1.1.5", "wayland-backend", "wayland-scanner", ] @@ -7825,7 +7826,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ - "rustix 1.1.4", + "rustix 1.1.5", "wayland-client", "xcursor", ] @@ -7915,7 +7916,7 @@ checksum = "0dde9c29be0f723a573977de51ee455bf3dfa03652730a74f9dd3b337e374d75" dependencies = [ "bitflags 2.13.2", "downcast-rs", - "rustix 1.1.4", + "rustix 1.1.5", "wayland-backend", "wayland-scanner", ] @@ -7934,9 +7935,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" dependencies = [ "js-sys", "wasm-bindgen", @@ -8012,7 +8013,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.20", + "thiserror 2.0.21", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", "wgpu-core-deps-windows-linux-android", @@ -8087,7 +8088,7 @@ dependencies = [ "raw-window-handle", "renderdoc-sys", "smallvec", - "thiserror 2.0.20", + "thiserror 2.0.21", "wasm-bindgen", "web-sys", "wgpu-types", @@ -8609,7 +8610,7 @@ dependencies = [ "dpi", "libc", "raw-window-handle", - "rustix 1.1.4", + "rustix 1.1.5", "smol_str", "tracing", "winit-android", @@ -8645,7 +8646,7 @@ dependencies = [ "block2 0.6.2", "dispatch2", "dpi", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", @@ -8663,7 +8664,7 @@ name = "winit-common" version = "0.31.0-beta.2" dependencies = [ "memmap2 0.9.11", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "smol_str", "tracing", @@ -8707,7 +8708,7 @@ dependencies = [ "block2 0.6.2", "dispatch2", "dpi", - "objc2 0.6.4", + "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-ui-kit", @@ -8730,7 +8731,7 @@ dependencies = [ "libc", "memmap2 0.9.11", "raw-window-handle", - "rustix 1.1.4", + "rustix 1.1.5", "sctk-adwaita", "smithay-client-toolkit", "smol_str", @@ -8816,7 +8817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.4", + "rustix 1.1.5", ] [[package]] @@ -8957,13 +8958,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "synstructure", ] @@ -8989,7 +8990,7 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix 1.1.4", + "rustix 1.1.5", "serde", "serde_repr", "tokio", @@ -9036,7 +9037,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "zbus_names", "zvariant", "zvariant_utils", @@ -9082,18 +9083,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.57" +version = "0.8.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.57" +version = "0.8.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" dependencies = [ "proc-macro2", "quote", @@ -9111,13 +9112,13 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "synstructure", ] @@ -9159,7 +9160,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -9305,7 +9306,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "zvariant_utils", ] @@ -9318,7 +9319,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 3.0.5", + "syn 3.0.6", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index b796364..2862491 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-term" -version = "1.9.0" +version = "1.10.0" authors = ["Jeremy Soller "] edition = "2024" license = "GPL-3.0-only" @@ -18,6 +18,9 @@ ron = "0.11" serde = { version = "1", features = ["serde_derive"] } shlex = "1" tokio = { version = "1", features = ["sync", "rt", "macros", "time"] } +# Notifications de bureau (OSC 9 / 777) et enveloppe du PTY (yoda) +polling = "3.11" +zbus = "5.19" # HTTP client for Modjo AI integration reqwest = { version = "0.12", features = ["json", "stream"], optional = true } serde_json = { version = "1", optional = true } diff --git a/cargo.just b/cargo.just new file mode 100644 index 0000000..fb09e1e --- /dev/null +++ b/cargo.just @@ -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 diff --git a/debian/changelog b/debian/changelog index 0da2a95..3f4cb6f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +cosmic-term (1.10.0) noble; urgency=medium + + * Epoch 1.10.0 version update + + -- Michael Murphy Mon, 05 Oct 2026 21:28:17 +0200 + cosmic-term (1.9.0) noble; urgency=medium * Epoch 1.9.0 version update diff --git a/i18n/cs/cosmic_term.ftl b/i18n/cs/cosmic_term.ftl index 89aa747..9853ec4 100644 --- a/i18n/cs/cosmic_term.ftl +++ b/i18n/cs/cosmic_term.ftl @@ -45,8 +45,8 @@ use-bright-bold = Světlejší tučný text splits = Rozdělení focus-follow-mouse = Zaměření psaní sleduje myš advanced = Pokročilé -show-headerbar = Zobrazit hlavičku -show-header-description = Hlavičku lze odkrýt z nabídky zobrazené po kliknutí pravým tlačítkem myši +show-headerbar = Zobrazit záhlaví +show-header-description = Záhlaví lze odkrýt v nabídce zobrazené po kliknutí pravým tlačítkem myši find-placeholder = Najít... find-previous = Najít předchozí find-next = Najít další @@ -69,7 +69,7 @@ next-tab = Další karta previous-tab = Předchozí karta split-horizontal = Rozdělit horizontálně split-vertical = Rozdělit vertikálně -pane-toggle-maximize = Přepnout maximalizaci +pane-toggle-maximize = Maximalizovat nebo obnovit podokno menu-color-schemes = Barevná schémata... menu-settings = Nastavení... menu-about = O aplikaci Terminál COSMIC... @@ -99,14 +99,17 @@ shortcut-group-zoom = Přiblížení toggle-fullscreen = Přepnout režim celé obrazovky type-to-search = Pište pro vyhledávání... copy-or-sigint = Kopírovat nebo SIGINT -focus-pane-down = Zaměřit panel dole -focus-pane-left = Zaměřit panel vlevo -focus-pane-right = Zaměřit panel vpravo -focus-pane-up = Zaměřit panel nahoře +focus-pane-down = Zaměřit podokno dole +focus-pane-left = Zaměřit podokno vlevo +focus-pane-right = Zaměřit podokno vpravo +focus-pane-up = Zaměřit podokno nahoře shortcut-replace-body = { $binding } je již přiřazena k „{ $existing }“. Nahradit za „{ $new_action }“? shortcut-capture-hint = Stiskněte kombinaci kláves paste-primary = Vložit primární copy-link = Kopírovat odkaz tab-new-inherit-working-directory = Nové karty a okna používají aktuální adresář tab-new-inherit-working-directory-description = Otevírat nové karty a okna v pracovním adresáři aktivní karty -show-pane-borders = Zobrazit ohraničení panelu +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 diff --git a/i18n/de/cosmic_term.ftl b/i18n/de/cosmic_term.ftl index 21d29da..3dcb828 100644 --- a/i18n/de/cosmic_term.ftl +++ b/i18n/de/cosmic_term.ftl @@ -105,7 +105,7 @@ next-tab = Nächster Tab previous-tab = Vorheriger Tab split-horizontal = Horizontal teilen split-vertical = Vertikal teilen -pane-toggle-maximize = Maximierung umschalten +pane-toggle-maximize = Bereich maximieren oder wiederherstellen menu-color-schemes = Farbschemen ... menu-settings = Einstellungen ... menu-about = Über COSMIC Terminal ... @@ -150,3 +150,6 @@ keywords = Befehl;Shell;Terminal;CLI; tab-new-inherit-working-directory = Neue Tabs und Fenster verwenden das aktuelle Verzeichnis tab-new-inherit-working-directory-description = Neue Tabs und Fenster im Arbeitsverzeichnis des aktiven Tabs öffnen show-pane-borders = Bereichsrahmen anzeigen +window-header = Fenstertitel +move-tab-left = Tab nach links verschieben +move-tab-right = Tab nach rechts verschieben diff --git a/i18n/el/cosmic_term.ftl b/i18n/el/cosmic_term.ftl index 1511a5d..f2a6414 100644 --- a/i18n/el/cosmic_term.ftl +++ b/i18n/el/cosmic_term.ftl @@ -107,7 +107,7 @@ next-tab = Επόμενη καρτέλα previous-tab = Προηγούμενη καρτέλα split-horizontal = Οριζόντιος διαχωρισμός split-vertical = Κάθετος διαχωρισμός -pane-toggle-maximize = Εναλλαγή μεγιστοποίησης +pane-toggle-maximize = Μεγιστοποίηση ή επαναφορά προβολής menu-color-schemes = Χρωματικοί συνδυασμοί... menu-settings = Ρυθμίσεις... menu-about = Σχετικά με το Τερματικό COSMIC... @@ -153,3 +153,6 @@ shortcut-replace-body = Ο συνδυασμός { $binding } έχει ήδη α tab-new-inherit-working-directory = Οι νέες καρτέλες και παράθυρα χρησιμοποιούν τον τρέχοντα κατάλογο tab-new-inherit-working-directory-description = Άνοιγμα νέων καρτελών και παραθύρων στον τρέχοντα κατάλογο της ενεργής καρτέλας show-pane-borders = Εμφάνιση περιγραμμάτων πλαισίων +window-header = Κεφαλίδα παραθύρου +move-tab-left = Μετακίνηση καρτέλας στα αριστερά +move-tab-right = Μετακίνηση καρτέλας στα δεξιά diff --git a/i18n/en-GB/cosmic_term.ftl b/i18n/en-GB/cosmic_term.ftl index 5714c25..c966e6f 100644 --- a/i18n/en-GB/cosmic_term.ftl +++ b/i18n/en-GB/cosmic_term.ftl @@ -105,7 +105,7 @@ next-tab = Next tab previous-tab = Previous tab split-horizontal = Split horizontal split-vertical = Split vertical -pane-toggle-maximize = Toggle maximized +pane-toggle-maximize = Maximise or restore pane menu-color-schemes = Colour schemes... menu-settings = Settings... menu-about = About COSMIC Terminal... @@ -115,3 +115,4 @@ cancel = Cancel keyboard-shortcuts = Keyboard shortcuts replace = Replace menu-keyboard-shortcuts = Keyboard shortcuts... +window-header = Window header diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index 2ff6c3c..a5f5e91 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -59,6 +59,7 @@ use-bright-bold = Make bold text brighter splits = Splits focus-follow-mouse = Typing focus follows mouse show-pane-borders = Show pane borders +unfocused-pane-dim = Dim unfocused panes ### Advanced advanced = Advanced @@ -67,6 +68,8 @@ window-header = Window header show-header-description = Reveal the header from the right-click menu tab-new-inherit-working-directory = New tabs and windows use current directory tab-new-inherit-working-directory-description = Open new tabs and windows in the active tab's working directory +copy-on-select-clipboard = Copy selection to clipboard +copy-on-select-clipboard-description = In addition to the primary selection (middle click), like copy-on-select = clipboard in Ghostty ### Keyboard shortcuts add-another-keybinding = Add another keybinding @@ -139,6 +142,10 @@ save-output = Save output as text... monitor-activity = Monitor for activity monitor-silence = Monitor for silence monitor-process-finished = Monitor for process finishing +read-only = Read-only + +## Notifications +notification-show = Show tab ## Open open-link = Open Link @@ -151,6 +158,8 @@ zoom-reset = Default text size zoom-out = Smaller text next-tab = Next tab previous-tab = Previous tab +move-tab-left = Move tab left +move-tab-right = Move tab right split-horizontal = Split horizontal split-vertical = Split vertical pane-toggle-maximize = Maximize or restore pane diff --git a/i18n/fi/cosmic_term.ftl b/i18n/fi/cosmic_term.ftl index 0686f39..1fc8b6b 100644 --- a/i18n/fi/cosmic_term.ftl +++ b/i18n/fi/cosmic_term.ftl @@ -107,7 +107,7 @@ next-tab = Seuraava välilehti previous-tab = Edellinen välilehti split-horizontal = Halkaise näkymä vaakasuunnassa split-vertical = Halkaise näkymä pystysuunnassa -pane-toggle-maximize = Suurennus päällä tai pois +pane-toggle-maximize = Suurenna tai palauta paneeli menu-color-schemes = Väriteemat… menu-settings = Asetukset… menu-about = Tietoa COSMICin päätteestä… @@ -152,3 +152,6 @@ shortcut-group-other = Muut tab-new-inherit-working-directory = Uudet välilehdet käyttävät nykyistä hakemistoa tab-new-inherit-working-directory-description = Avaa uudet välilehdet aktiivisen välilehden työskentelyhakemistossa show-pane-borders = Näytä paneelin rajat +window-header = Ikkunan otsake +move-tab-left = Siirrä välilehti vasemmalle +move-tab-right = Siirrä välilehti oikealle diff --git a/i18n/fr/cosmic_term.ftl b/i18n/fr/cosmic_term.ftl index 834c800..12248af 100644 --- a/i18n/fr/cosmic_term.ftl +++ b/i18n/fr/cosmic_term.ftl @@ -96,7 +96,7 @@ quit = Quitter ## Edit -edit = Modifier +edit = Édition copy = Copier paste = Coller select-all = Sélectionner tout @@ -108,6 +108,10 @@ save-output = Enregistrer la sortie comme texte... monitor-activity = Surveiller l'activité monitor-silence = Surveiller le silence monitor-process-finished = Surveiller la fin du processus +read-only = Lecture seule + +## Notifications +notification-show = Afficher l'onglet ## View @@ -119,7 +123,7 @@ next-tab = Onglet suivant previous-tab = Onglet précédent split-horizontal = Scinder en horizontal split-vertical = Scinder en vertical -pane-toggle-maximize = Vue unique +pane-toggle-maximize = Agrandir ou restaurer le panneau menu-color-schemes = Palettes de couleurs... menu-settings = Paramètres... menu-about = À propos de terminal COSMIC... @@ -168,4 +172,10 @@ comment = Émulateur de terminal pour le bureau COSMIC keywords = Commande;Shell;Terminal;CLI; tab-new-inherit-working-directory = Les nouveaux onglets et fenêtres utilisent le répertoire actuel tab-new-inherit-working-directory-description = Ouvrir les nouveaux onglets et fenêtres dans le répertoire de travail de l'onglet actif +copy-on-select-clipboard = Copier la sélection dans le presse-papiers +copy-on-select-clipboard-description = En plus de la sélection primaire (clic du milieu), comme copy-on-select = clipboard dans Ghostty show-pane-borders = Afficher les bordures des panneaux +unfocused-pane-dim = Atténuation des panneaux inactifs +window-header = Barre de titre de la fenêtre +move-tab-left = Déplacer l'onglet à gauche +move-tab-right = Déplacer l'onglet à droite diff --git a/i18n/ga/cosmic_term.ftl b/i18n/ga/cosmic_term.ftl index 03f37ce..acd4585 100644 --- a/i18n/ga/cosmic_term.ftl +++ b/i18n/ga/cosmic_term.ftl @@ -105,7 +105,7 @@ next-tab = An cluaisín ar aghaidh previous-tab = An cluaisín roimhe seo split-horizontal = Scoilt cothrománach split-vertical = Scoilt ingearach -pane-toggle-maximize = Scoránaigh uasmhéadú +pane-toggle-maximize = Uasmhéadaigh nó athchóirigh an phána menu-color-schemes = Scéimeanna dathanna... menu-settings = Socruithe... menu-about = Maidir le COSMIC Teirminéal... @@ -149,3 +149,5 @@ keywords = Ordú;Blaosc;Teirminéal;CLI; copy-link = Cóipeáil Nasc tab-new-inherit-working-directory = Úsáideann cluaisíní agus fuinneoga nua an t-eolaire reatha tab-new-inherit-working-directory-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 diff --git a/i18n/gl/cosmic_term.ftl b/i18n/gl/cosmic_term.ftl index 30e6ecc..ece23fd 100644 --- a/i18n/gl/cosmic_term.ftl +++ b/i18n/gl/cosmic_term.ftl @@ -7,7 +7,7 @@ support = Soporte settings = Axustes appearance = Apariencia theme = Tema -match-desktop = Axustar ao escritorio +match-desktop = Emparexar co escritorio dark = Escuro light = Claro default-zoom-step = Pasos do zoom @@ -110,3 +110,6 @@ 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 diff --git a/i18n/hu/cosmic_term.ftl b/i18n/hu/cosmic_term.ftl index 9c1e9df..daa12f2 100644 --- a/i18n/hu/cosmic_term.ftl +++ b/i18n/hu/cosmic_term.ftl @@ -52,14 +52,14 @@ opacity = Háttér átlátszósága ### Font -font = Betűtípus -advanced-font-settings = Speciális betűtípus-beállítások -default-font = Betűtípus +font = Betűkészlet +advanced-font-settings = Speciális betűkészlet-beállítások +default-font = Betűkészlet default-font-size = Betűméret default-font-stretch = Betűszélesség -default-font-weight = Normál betűsúly -default-dim-font-weight = Vékony betűsúly -default-bold-font-weight = Félkövér betűsúly +default-font-weight = Normál betűvastagság +default-dim-font-weight = Vékony betűvastagság +default-bold-font-weight = Félkövér betűvastagság use-bright-bold = Félkövér szöveg világosítása ### Splits @@ -109,7 +109,7 @@ next-tab = Következő lap previous-tab = Előző lap split-horizontal = Vízszintes felosztás split-vertical = Függőleges felosztás -pane-toggle-maximize = Panel maximalizálása +pane-toggle-maximize = Panel maximalizálása vagy visszaállítása menu-color-schemes = Színsémák… menu-settings = Beállítások… menu-about = A COSMIC Terminál névjegye… @@ -150,3 +150,6 @@ copy-link = Hivatkozás másolása tab-new-inherit-working-directory = Az új lapok és ablakok az aktuális könyvtárat használják tab-new-inherit-working-directory-description = Az új lapok és ablakok az aktív lap munkakönyvtárában nyílnak meg show-pane-borders = Ablaktábla-szegélyek megjelenítése +window-header = Ablakfejléc +move-tab-left = Lap mozgatása balra +move-tab-right = Lap mozgatása jobbra diff --git a/i18n/id/cosmic_term.ftl b/i18n/id/cosmic_term.ftl index 6b139ad..4a179d4 100644 --- a/i18n/id/cosmic_term.ftl +++ b/i18n/id/cosmic_term.ftl @@ -23,7 +23,7 @@ remain-open = Tetap buka setelah proses anak berakhir. settings = Pengaturan appearance = Tampilan theme = Tema -match-desktop = Cocok desktop +match-desktop = Sesuaikan desktop dark = Gelap light = Terang syntax-dark = Skema warna gelap @@ -68,7 +68,7 @@ next-tab = Tab selanjutnya previous-tab = Tab sebelumnya split-horizontal = Pemisahan horisontal split-vertical = Pemisahan vertikal -pane-toggle-maximize = Ubah ke maksimal +pane-toggle-maximize = Maksimalkan atau pulihkan panel menu-color-schemes = Skema warna... menu-settings = Pengaturan... menu-about = Tentang Terminal COSMIC... @@ -110,3 +110,6 @@ copy-link = Salin Tautan tab-new-inherit-working-directory = Tab dan jendela baru menggunakan direktori saat ini tab-new-inherit-working-directory-description = Buka tab dan jendela baru di direktori pekerjaan tab aktif show-pane-borders = Tampilkan batas panel +window-header = Header jendela +move-tab-left = Pindahkan tab ke kiri +move-tab-right = Pindahkan tab ke kanan diff --git a/i18n/kk/cosmic_term.ftl b/i18n/kk/cosmic_term.ftl index 682dd96..7560358 100644 --- a/i18n/kk/cosmic_term.ftl +++ b/i18n/kk/cosmic_term.ftl @@ -5,8 +5,8 @@ support = Қолдау settings = Баптаулар appearance = Сыртқы көрініс theme = Тема -match-desktop = Жұмыс үстеліне сәйкес болу -dark = Қою +match-desktop = Жұмыс үстеліне сәйкес +dark = Қараңғы light = Ашық file = Файл new-tab = Жаңа бет @@ -29,7 +29,7 @@ find = Табу cosmic-terminal = COSMIC терминалы new-terminal = Жаңа терминал color-schemes = Түстер схемалары -rename = Атын өзгерту +rename = Атауын өзгерту import-errors = Импорттау қателері profiles = Профильдер command-line = Командалық жол @@ -69,7 +69,7 @@ next-tab = Келесі бет previous-tab = Алдыңғы бет split-horizontal = Горизонталды бөлу split-vertical = Вертикалды бөлу -pane-toggle-maximize = Толық экран режимін ауыстыру +pane-toggle-maximize = Панельді үлкейту немесе қалпына келтіру menu-color-schemes = Түс схемалары... menu-about = COSMIC терминалы туралы... menu-password-manager = Парольдер... @@ -110,3 +110,6 @@ copy-link = Сілтемені көшіру tab-new-inherit-working-directory = Жаңа беттерде және терезелерде ағымдағы буманы қолдану tab-new-inherit-working-directory-description = Жаңа беттерді және терезелерді белсенді беттің жұмыс бумасында ашу show-pane-borders = Панель жақтауларын көрсету +window-header = Терезе тақырыптамасы +move-tab-left = Бетті солға жылжыту +move-tab-right = Бетті оңға жылжыту diff --git a/i18n/kmr/cosmic_term.ftl b/i18n/kmr/cosmic_term.ftl index 4348691..1c764c6 100644 --- a/i18n/kmr/cosmic_term.ftl +++ b/i18n/kmr/cosmic_term.ftl @@ -33,7 +33,7 @@ command-line = Rêza fermanê view = Dîtin dark = Tarî hold = Ragire -match-desktop = Bi sermaseyê re li hev bîne +match-desktop = Bi sermaseyê re guncav bike find-previous = Yê paş bibîne cosmic-terminal = Termînalê COSMIC find-placeholder = Bibîne... @@ -110,3 +110,6 @@ toggle-fullscreen = Dîmendera tijî biguhêrîne type-to-search = Ji bo lêgerînê binivîse... copy-link = Girêdanê jê bigire show-pane-borders = Sînorên kilîtdankê nîşan bide +window-header = Sernavê çarçoveyê +move-tab-left = Rûgerê bilivîne çepê +move-tab-right = Rûgerê bilivîne rastê diff --git a/i18n/pl/cosmic_term.ftl b/i18n/pl/cosmic_term.ftl index 25c3027..3b1ea6d 100644 --- a/i18n/pl/cosmic_term.ftl +++ b/i18n/pl/cosmic_term.ftl @@ -109,7 +109,7 @@ next-tab = Następna karta previous-tab = Poprzednia karta split-horizontal = Podziel w poziomie split-vertical = Podziel w pionie -pane-toggle-maximize = Przełącznik maksymalizacji +pane-toggle-maximize = Maksymalizuj lub przywróć panel menu-color-schemes = Schemat kolorów... menu-settings = Ustawienia… menu-about = O Terminalu COSMIC... @@ -150,3 +150,4 @@ copy-link = Skopiuj Odnośnik tab-new-inherit-working-directory = Nowe karty i okna użyją obecnego katalogu tab-new-inherit-working-directory-description = Otwieraj nowe karty i okna w katalogu roboczym aktywnej karty show-pane-borders = Pokaż obramowania paneli +window-header = Nagłówek okna diff --git a/i18n/pt-BR/cosmic_term.ftl b/i18n/pt-BR/cosmic_term.ftl index 98a7f87..25df23c 100644 --- a/i18n/pt-BR/cosmic_term.ftl +++ b/i18n/pt-BR/cosmic_term.ftl @@ -70,7 +70,7 @@ focus-follow-mouse = Foco de digitação segue o mouse ### Advanced advanced = Avançado -show-headerbar = Mostrar a barra de título +show-headerbar = Mostrar cabeçalho show-header-description = Mostrar o cabeçalho ao clicar no menu com o botão direito do mouse # Find find-placeholder = Localizar... @@ -109,7 +109,7 @@ next-tab = Próxima aba previous-tab = Aba anterior split-horizontal = Dividir horizontalmente split-vertical = Dividir verticalmente -pane-toggle-maximize = Alternar maximização +pane-toggle-maximize = Maximizar ou restaurar o quadro menu-color-schemes = Esquemas de cores... menu-settings = Configurações... menu-about = Sobre o Terminal... @@ -150,3 +150,6 @@ copy-link = Copiar link tab-new-inherit-working-directory = Novas abas e janelas usam o diretório atual tab-new-inherit-working-directory-description = Abrir novas abas e janelas no diretório de trabalho da aba ativa show-pane-borders = Mostrar bordas do quadro +window-header = Cabeçalho da janela +move-tab-left = Mover a aba para a esquerda +move-tab-right = Mover a aba para a direita diff --git a/i18n/ru/cosmic_term.ftl b/i18n/ru/cosmic_term.ftl index c168100..7fcb36e 100644 --- a/i18n/ru/cosmic_term.ftl +++ b/i18n/ru/cosmic_term.ftl @@ -106,7 +106,7 @@ next-tab = Следующая вкладка previous-tab = Предыдущая вкладка split-horizontal = Разделение по горизонтали split-vertical = Разделение по вертикали -pane-toggle-maximize = Переключить на весь экран +pane-toggle-maximize = Развернуть/восстановить область menu-color-schemes = Цветовые схемы... menu-settings = Параметры... menu-about = О Терминале COSMIC... @@ -143,4 +143,7 @@ type-to-search = Введите для поиска... copy-link = Копировать ссылку tab-new-inherit-working-directory = Текущий каталог в новых вкладках и окнах tab-new-inherit-working-directory-description = Открывать новые вкладки и окна в рабочем каталоге активной вкладки -show-pane-borders = Показывать границы панелей +show-pane-borders = Показывать границы областей +window-header = Заголовок окна +move-tab-left = Перемещение вкладки налево +move-tab-right = Перемещение вкладки направо diff --git a/i18n/sv-SE/cosmic_term.ftl b/i18n/sv-SE/cosmic_term.ftl index 8167a39..e8e2d69 100644 --- a/i18n/sv-SE/cosmic_term.ftl +++ b/i18n/sv-SE/cosmic_term.ftl @@ -110,7 +110,7 @@ next-tab = Nästa flik previous-tab = Föregående flik split-horizontal = Dela horisontellt split-vertical = Dela vertikalt -pane-toggle-maximize = Växla maximerad +pane-toggle-maximize = Maximera eller återställ fält clear-scrollback = Töm tillbakarullning menu-color-schemes = Färgscheman… menu-settings = Inställningar... @@ -154,3 +154,6 @@ copy-link = Kopiera länk tab-new-inherit-working-directory-description = Öppna nya flikar och fönster i den aktiva flikens arbetskatalog tab-new-inherit-working-directory = Nya flikar och fönster använder aktuell katalog show-pane-borders = Visa ram runt fält +window-header = Fönstertitel +move-tab-left = Flytta flik vänster +move-tab-right = Flytta flik höger diff --git a/i18n/uk/cosmic_term.ftl b/i18n/uk/cosmic_term.ftl index 753eda5..b495cc9 100644 --- a/i18n/uk/cosmic_term.ftl +++ b/i18n/uk/cosmic_term.ftl @@ -38,7 +38,7 @@ settings = Налаштування appearance = Вигляд theme = Тема -match-desktop = Системна +match-desktop = Підігнати під стільницю dark = Темна light = Світла syntax-dark = Темна колірна схема @@ -150,3 +150,6 @@ copy-link = Копіювати ланку tab-new-inherit-working-directory = Нові вкладки та вікна відкривати в поточному каталозі tab-new-inherit-working-directory-description = Відкривати нові вкладки та вікна в робочому каталозі активної вкладки show-pane-borders = Показувати межі панелей +window-header = Заголовок вікна +move-tab-left = Перемістити ліворуч +move-tab-right = Перемістити праворуч diff --git a/i18n/zh-CN/cosmic_term.ftl b/i18n/zh-CN/cosmic_term.ftl index d06d5d5..8a8116b 100644 --- a/i18n/zh-CN/cosmic_term.ftl +++ b/i18n/zh-CN/cosmic_term.ftl @@ -105,7 +105,7 @@ next-tab = 下一个标签 previous-tab = 上一个标签 split-horizontal = 水平分割 split-vertical = 垂直分割 -pane-toggle-maximize = 切换最大化 +pane-toggle-maximize = 将窗格最大化或还原 menu-color-schemes = 配色方案… menu-settings = 设置… menu-about = 关于 COSMIC 终端… @@ -150,3 +150,6 @@ copy-link = 复制链接 tab-new-inherit-working-directory = 新标签和窗口使用当前目录 tab-new-inherit-working-directory-description = 在活动标签的工作目录中打开新标签和窗口 show-pane-borders = 显示窗格边框 +window-header = 窗口标题栏 +move-tab-left = 将标签向左移动 +move-tab-right = 将标签向右移动 diff --git a/i18n/zh-TW/cosmic_term.ftl b/i18n/zh-TW/cosmic_term.ftl index 7cebc95..ecae885 100644 --- a/i18n/zh-TW/cosmic_term.ftl +++ b/i18n/zh-TW/cosmic_term.ftl @@ -104,7 +104,7 @@ next-tab = 下個分頁 previous-tab = 上個分頁 split-horizontal = 水平分割 split-vertical = 垂直分割 -pane-toggle-maximize = 切換最大化 +pane-toggle-maximize = 將窗格最大化或還原 menu-color-schemes = 配色方案... menu-settings = 設定... menu-about = 關於 COSMIC 終端機… @@ -150,3 +150,6 @@ comment = COSMIC 桌面終端機模擬器 tab-new-inherit-working-directory = 新分頁和視窗使用目前目錄 tab-new-inherit-working-directory-description = 在作用中分頁的工作目錄開啟新分頁和視窗 show-pane-borders = 顯示窗格邊框 +window-header = 視窗標題列 +move-tab-left = 將分頁向左移動 +move-tab-right = 將分頁向右移動 diff --git a/src/config.rs b/src/config.rs index caedf4d..2137165 100644 --- a/src/config.rs +++ b/src/config.rs @@ -232,17 +232,33 @@ pub struct Config { pub profiles: BTreeMap, pub show_headerbar: bool, pub show_pane_borders: bool, + /// [yoda] Voile posé sur les panneaux non focalisés, en pourcentage + /// (`unfocused-split-opacity` de Ghostty : 0,7 d'opacité = 30 % de voile). + #[serde(default = "default_unfocused_pane_dim")] + pub unfocused_pane_dim: u8, pub use_bright_bold: bool, pub syntax_theme_dark: String, pub syntax_theme_light: String, pub focus_follow_mouse: bool, #[serde(default)] pub tab_new_inherit_working_directory: bool, + /// [yoda] Copier aussi la sélection dans le presse-papiers, et pas + /// seulement dans la sélection primaire (`copy-on-select = clipboard` de + /// Ghostty). + #[serde(default)] + pub copy_on_select_clipboard: bool, pub default_profile: Option, #[serde(default)] pub shortcuts_custom: Shortcuts, } +/// Voile maximal des panneaux non focalisés : Ghostty borne leur opacité à 0,15. +pub const UNFOCUSED_PANE_DIM_MAX: u8 = 85; + +fn default_unfocused_pane_dim() -> u8 { + 30 +} + impl Default for Config { fn default() -> Self { Self { @@ -253,6 +269,7 @@ impl Default for Config { dim_font_weight: Weight::NORMAL.0, focus_follow_mouse: false, tab_new_inherit_working_directory: false, + copy_on_select_clipboard: false, font_name: "Noto Sans Mono".to_string(), font_size: 14, font_size_zoom_step_mul_100: 100, @@ -262,6 +279,7 @@ impl Default for Config { profiles: BTreeMap::new(), show_headerbar: true, show_pane_borders: false, + unfocused_pane_dim: default_unfocused_pane_dim(), syntax_theme_dark: COSMIC_THEME_DARK.to_string(), syntax_theme_light: COSMIC_THEME_LIGHT.to_string(), use_bright_bold: false, @@ -343,6 +361,12 @@ impl Config { Metrics::new(font_size, line_height) } + /// Opacité du voile des panneaux non focalisés ; `None` si désactivé. + pub fn unfocused_pane_dim_ratio(&self) -> Option { + let dim = self.unfocused_pane_dim.min(UNFOCUSED_PANE_DIM_MAX); + (dim > 0).then(|| f32::from(dim) / 100.0) + } + pub fn opacity_ratio(&self) -> f32 { f32::from(self.opacity) / 100.0 } @@ -404,3 +428,22 @@ impl Config { })[&self.font_stretch] } } + +#[cfg(test)] +mod tests { + use super::{Config, UNFOCUSED_PANE_DIM_MAX}; + + #[test] + fn unfocused_pane_dim_ratio() { + let mut config = Config::default(); + // Défaut de Ghostty : opacité 0,7, soit 30 % de voile + assert_eq!(config.unfocused_pane_dim_ratio(), Some(0.3)); + config.unfocused_pane_dim = 0; + assert_eq!(config.unfocused_pane_dim_ratio(), None); + config.unfocused_pane_dim = 100; + assert_eq!( + config.unfocused_pane_dim_ratio(), + Some(f32::from(UNFOCUSED_PANE_DIM_MAX) / 100.0) + ); + } +} diff --git a/src/desktop_notify.rs b/src/desktop_notify.rs new file mode 100644 index 0000000..4ad9821 --- /dev/null +++ b/src/desktop_notify.rs @@ -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), + /// L'utilisateur a cliqué la notification de cet onglet. + Activated { tab: TabKey, token: Option }, +} + +#[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; + + #[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 { + Subscription::run_with(TypeId::of::(), |_| { + stream::channel(16, |mut output: Sender| 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, + mut output: Sender, +) -> 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 = HashMap::new(); + let mut id_by_tab: HashMap = HashMap::new(); + let mut pending_tokens: HashMap = 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('>', "›") +} diff --git a/src/kitty_keyboard.rs b/src/kitty_keyboard.rs new file mode 100644 index 0000000..df24628 --- /dev/null +++ b/src/kitty_keyboard.rs @@ -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 : . 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 { + 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> { + 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 = 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>>); + 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[ widget::icon::Icon { /// Tab bar indicator for the monitor state of a terminal: /// an alert icon if one fired, an "armed" icon while monitoring, else none. +/// Un appel d'attention (cloche, notification) passe avant les moniteurs. fn monitor_icon_name(terminal: &Terminal) -> Option<&'static str> { + match terminal.attention { + Some(AttentionKind::Bell) => return Some("notification-alert-symbolic"), + Some(AttentionKind::Notification) => return Some("notification-new-symbolic"), + None => {} + } match terminal.monitor_alert { Some(MonitorKind::Activity) => Some("dialog-information-symbolic"), Some(MonitorKind::Silence) => Some("alarm-symbolic"), Some(MonitorKind::ProcessFinished) => Some("emblem-ok-symbolic"), + None if terminal.read_only => Some("changes-prevent-symbolic"), None if terminal.monitoring_enabled() => Some("view-reveal-symbolic"), None => None, } @@ -297,6 +314,9 @@ fn main() -> Result<(), Box> { // Terminal config setup let term_config = term::Config { scrolling_history: 100_000, + // [yoda] Protocole clavier kitty : alacritty_terminal tient la pile des + // modes et répond à `CSI ? u`, l'encodage est dans `kitty_keyboard`. + kitty_keyboard: true, ..term::Config::default() }; // Set up environmental variables for terminal @@ -371,6 +391,7 @@ pub enum Action { LayoutLoad, LayoutSave, Monitor(MonitorKind), + ToggleReadOnly, PaneFocusDown, PaneFocusLeft, PaneFocusRight, @@ -404,6 +425,8 @@ pub enum Action { TabNext, TabPrev, TabRename, + TabMoveLeft, + TabMoveRight, ToggleFullscreen, WindowClose, WindowNew, @@ -430,6 +453,7 @@ impl Action { Self::LayoutLoad => Message::LayoutLoad, Self::LayoutSave => Message::LayoutSave, Self::Monitor(monitor_kind) => Message::MonitorToggle(*monitor_kind, entity_opt), + Self::ToggleReadOnly => Message::ReadOnlyToggle(entity_opt), Self::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down), Self::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left), Self::PaneFocusRight => Message::PaneFocusAdjacent(pane_grid::Direction::Right), @@ -463,6 +487,8 @@ impl Action { Self::TabNext => Message::TabNext, Self::TabPrev => Message::TabPrev, Self::TabRename => Message::TabRenameOpen(entity_opt), + Self::TabMoveLeft => Message::TabMoveLeft, + Self::TabMoveRight => Message::TabMoveRight, Self::ToggleFullscreen => Message::ToggleFullscreen, Self::WindowClose => Message::WindowClose, Self::WindowNew => Message::WindowNew, @@ -537,6 +563,7 @@ pub enum Message { ShortcutSearch(String), MonitorTick, MonitorToggle(MonitorKind, Option), + ReadOnlyToggle(Option), MouseEnter(pane_grid::Pane), Opacity(u8), PaneClicked(pane_grid::Pane), @@ -572,10 +599,13 @@ pub enum Message { ShowHeaderBar(bool), SshConnect(usize), ShowPaneBorders(bool), + UnfocusedPaneDim(u8), SyntaxTheme(ColorSchemeKind, usize), SystemThemeChange, TabNewInheritWorkingDirectory(bool), + CopyOnSelectClipboard(bool), + SelectionDone(pane_grid::Pane, segmented_button::Entity), TabActivate(segmented_button::Entity), TabActivateJump(usize), TabClose(Option), @@ -591,8 +621,12 @@ pub enum Message { TabRenameCommit, TabRenameCancel, TabRenameResetToDefault, + TabMoveLeft, + TabMoveRight, TermEvent(pane_grid::Pane, segmented_button::Entity, TermEvent), - TermEventTx(mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, TermEvent)>), + TermEventTx(mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>), + TermOsc(pane_grid::Pane, segmented_button::Entity, OscEvent), + DesktopNotify(desktop_notify::Event), ToggleFullscreen, ToggleContextPage(ContextPage), UpdateDefaultProfile((bool, ProfileId)), @@ -658,7 +692,9 @@ pub struct App { find_search_id: widget::Id, find_search_value: String, term_event_tx_opt: - Option>, + Option>, + notify_tx_opt: Option>, + window_focused: bool, startup_options: Option, startup_profile_id: Option, term_config: term::Config, @@ -686,6 +722,51 @@ pub struct App { } impl App { + /// L'onglet est-il sous les yeux de l'utilisateur : onglet actif du panneau + /// focalisé, dans une fenêtre qui a le focus. + fn tab_in_view(&self, pane: pane_grid::Pane, entity: segmented_button::Entity) -> bool { + self.window_focused + && self.pane_model.focused() == pane + && self + .pane_model + .panes + .get(pane) + .is_some_and(|tab_model| tab_model.active() == entity) + } + + /// Allume l'indicateur d'attention d'un onglet ; il s'éteint quand + /// l'onglet reprend le focus (voir `TerminalPaneGrid::update_terminal_focus`). + fn set_tab_attention( + &mut self, + pane: pane_grid::Pane, + entity: segmented_button::Entity, + kind: AttentionKind, + ) { + let Some(tab_model) = self.pane_model.panes.get_mut(pane) else { + return; + }; + let mut icon_update = None; + if let Some(terminal) = tab_model.data::>(entity) { + let mut terminal = terminal.lock().unwrap(); + // Une notification n'est pas rétrogradée en simple cloche + if terminal.attention != Some(AttentionKind::Notification) { + terminal.attention = Some(kind); + icon_update = Some(monitor_icon_name(&terminal)); + } + } + if let Some(icon_name) = icon_update { + monitor_update_tab_icon(tab_model, entity, icon_name); + } + } + + /// Recale le focus des terminaux après un changement d'onglet : l'onglet + /// quitté reçoit son focus-out (mode 1004), le nouvel onglet son focus-in. + fn sync_terminal_focus(&mut self) { + if self.window_focused && !self.core.window.show_context && self.rename_tab.is_none() { + self.pane_model.update_terminal_focus(); + } + } + fn theme_names(&self, color_scheme_kind: ColorSchemeKind) -> &Vec { match color_scheme_kind { ColorSchemeKind::Dark => &self.theme_names_dark, @@ -1661,6 +1742,15 @@ impl App { .add( widget::settings::item::builder(fl!("show-pane-borders")) .toggler(self.config.show_pane_borders, Message::ShowPaneBorders), + ) + .add( + widget::settings::item::builder(fl!("unfocused-pane-dim")) + .description(format!("{}%", self.config.unfocused_pane_dim)) + .control(widget::slider( + 0..=config::UNFOCUSED_PANE_DIM_MAX, + self.config.unfocused_pane_dim, + Message::UnfocusedPaneDim, + )), ); let advanced_section = widget::settings::section() @@ -1677,6 +1767,14 @@ impl App { self.config.tab_new_inherit_working_directory, Message::TabNewInheritWorkingDirectory, ), + ) + .add( + widget::settings::item::builder(fl!("copy-on-select-clipboard")) + .description(fl!("copy-on-select-clipboard-description")) + .toggler( + self.config.copy_on_select_clipboard, + Message::CopyOnSelectClipboard, + ), ); widget::settings::view_column(vec![ @@ -1731,6 +1829,22 @@ impl App { } } + fn terminal_read_only(&self, pane: pane_grid::Pane, entity: segmented_button::Entity) -> bool { + self.pane_model + .panes + .get(pane) + .and_then(|tab_model| tab_model.data::>(entity)) + .is_some_and(|terminal| terminal.lock().unwrap().read_only) + } + + fn active_terminal_read_only(&self) -> bool { + let pane = self.pane_model.focused(); + self.pane_model + .panes + .get(pane) + .is_some_and(|tab_model| self.terminal_read_only(pane, tab_model.active())) + } + fn active_terminal_monitors(&self) -> (bool, bool, bool) { let pane = self.pane_model.focused(); if let Some(tab_model) = self.pane_model.panes.get(pane) { @@ -1990,6 +2104,7 @@ impl App { log::warn!("tried to create new tab before having event channel"); } } + self.sync_terminal_focus(); self.update_title(Some(pane)) } } @@ -2139,7 +2254,7 @@ impl Application for App { .comments(fl!("comment")) .license("GPL-3.0-only") .license_url("https://spdx.org/licenses/GPL-3.0-only") - .developers([("Jeremy Soller", "jeremy@system76.com")]) + .developers([("System76", "info@system76.com")]) .links([ (fl!("repository"), "https://github.com/pop-os/cosmic-term"), ( @@ -2185,6 +2300,8 @@ impl Application for App { startup_profile_id: flags.startup_profile_id, term_config: flags.term_config, term_event_tx_opt: None, + notify_tx_opt: None, + window_focused: true, color_scheme_errors: Vec::new(), color_scheme_expanded: None, color_scheme_renaming: None, @@ -2953,6 +3070,20 @@ impl Application for App { } } } + Message::ReadOnlyToggle(entity_opt) => { + if let Some(tab_model) = self.pane_model.active_mut() { + let entity = entity_opt.unwrap_or_else(|| tab_model.active()); + let mut icon_update = None; + if let Some(terminal) = tab_model.data::>(entity) { + let mut terminal = terminal.lock().unwrap(); + terminal.read_only = !terminal.read_only; + icon_update = Some(monitor_icon_name(&terminal)); + } + if let Some(icon_name) = icon_update { + monitor_update_tab_icon(tab_model, entity, icon_name); + } + } + } Message::MonitorToggle(monitor_kind, entity_opt) => { if let Some(tab_model) = self.pane_model.active_mut() { let entity = entity_opt.unwrap_or_else(|| tab_model.active()); @@ -3289,6 +3420,24 @@ impl Application for App { return self.update_config(); } } + Message::CopyOnSelectClipboard(copy_on_select_clipboard) => { + if copy_on_select_clipboard != self.config.copy_on_select_clipboard { + config_set!(copy_on_select_clipboard, copy_on_select_clipboard); + } + } + Message::SelectionDone(pane, entity) => { + // [yoda] Copie à la sélection vers le presse-papiers ; la + // sélection primaire, elle, suit le relâchement du bouton. + if self.config.copy_on_select_clipboard + && let Some(tab_model) = self.pane_model.panes.get(pane) + && let Some(terminal) = tab_model.data::>(entity) + { + let text = terminal.lock().unwrap().term.lock().selection_to_string(); + if let Some(text) = text.filter(|text| !text.is_empty()) { + return clipboard::write(text); + } + } + } Message::TabNewInheritWorkingDirectory(tab_new_inherit_working_directory) => { config_set!( tab_new_inherit_working_directory, @@ -3300,6 +3449,12 @@ impl Application for App { config_set!(show_pane_borders, show_pane_borders); } } + Message::UnfocusedPaneDim(dim) => { + config_set!( + unfocused_pane_dim, + cmp::min(config::UNFOCUSED_PANE_DIM_MAX, dim) + ); + } Message::UseBrightBold(use_bright_bold) => { if use_bright_bold != self.config.use_bright_bold { config_set!(use_bright_bold, use_bright_bold); @@ -3367,6 +3522,7 @@ impl Application for App { monitor_update_tab_icon(tab_model, entity, icon_name); } } + self.sync_terminal_focus(); return self.update_title(None); } Message::TabActivateJump(pos) => { @@ -3420,6 +3576,7 @@ impl Application for App { } } + self.sync_terminal_focus(); return self.update_title(None); } Message::TabContextAction(entity, action) => { @@ -3586,10 +3743,43 @@ impl Application for App { } self.pane_model.update_terminal_focus(); } + Message::TabMoveLeft => { + if let Some(tab_model) = self.pane_model.active_mut() { + let pos: usize = tab_model + .position(tab_model.active()) + .expect("at least one tab is always open") + .into(); + if pos > 0 { + let dragged = tab_model.iter().nth(pos).expect("tab unavailable"); + let target = tab_model.iter().nth(pos - 1).expect("previous tab missing"); + tab_model.reorder(dragged, target, InsertPosition::Before); + } + } + } + Message::TabMoveRight => { + if let Some(tab_model) = self.pane_model.active_mut() { + let len = tab_model.iter().count(); + let pos: usize = tab_model + .position(tab_model.active()) + .expect("at least one tab is always open") + .into(); + if pos < len - 1 && len > 1 { + let dragged = tab_model.iter().nth(pos).expect("tab unavailable"); + let target = tab_model.iter().nth(pos + 1).expect("next tab missing"); + tab_model.reorder(dragged, target, InsertPosition::After); + } + } + } Message::TermEvent(pane, entity, event) => { match event { TermEvent::Bell => { - //TODO: audible or visible bell options? + // Comme le défaut de Ghostty (`bell-features = attention,title`), + // sans la demande d'attention xdg-activation : cosmic-comp donne + // un jeton valide à toute application non sandboxée, la fenêtre + // volerait le focus. Il reste l'indicateur d'onglet. + if !self.tab_in_view(pane, entity) { + self.set_tab_attention(pane, entity, AttentionKind::Bell); + } } TermEvent::ClipboardLoad(kind, callback) => { match kind { @@ -3623,7 +3813,7 @@ impl Application for App { let terminal = terminal.lock().unwrap(); let rgb = terminal.effective_color(index); let text = f(rgb); - terminal.input_no_scroll(text.into_bytes()); + terminal.respond(text.into_bytes()); } } TermEvent::CursorBlinkingChange => { @@ -3637,7 +3827,7 @@ impl Application for App { && let Some(terminal) = tab_model.data::>(entity) { let terminal = terminal.lock().unwrap(); - terminal.input_no_scroll(text.into_bytes()); + terminal.respond(text.into_bytes()); } } TermEvent::ResetTitle => { @@ -3662,7 +3852,7 @@ impl Application for App { { let terminal = terminal.lock().unwrap(); let text = f(terminal.size().into()); - terminal.input_no_scroll(text.into_bytes()); + terminal.respond(text.into_bytes()); } } TermEvent::Title(title) => { @@ -3749,6 +3939,76 @@ impl Application for App { // Spawn first tab return self.update(Message::TabNew); } + Message::TermOsc(pane, entity, event) => match event { + OscEvent::Notify { title, body } => { + // Rien à signaler pour l'onglet qu'on a sous les yeux + if self.tab_in_view(pane, entity) { + return Task::none(); + } + self.set_tab_attention(pane, entity, AttentionKind::Notification); + + let Some(tab_model) = self.pane_model.panes.get(pane) else { + return Task::none(); + }; + let allowed = tab_model + .data::>(entity) + .is_some_and(|terminal| { + terminal + .lock() + .unwrap() + .notify_allowed(title.as_deref(), &body) + }); + if !allowed { + return Task::none(); + } + + // Titre de la séquence, suivi de l'onglet d'où elle vient : + // plusieurs sessions Claude Code se distinguent ainsi. + let tab_title = tab_model.text(entity).unwrap_or_default().to_string(); + let summary = match title { + Some(title) if !tab_title.is_empty() && title != tab_title => { + format!("{title} · {tab_title}") + } + Some(title) => title, + None if !tab_title.is_empty() => tab_title, + None => fl!("cosmic-terminal"), + }; + if let Some(notify_tx) = &self.notify_tx_opt { + let _ = notify_tx.send(desktop_notify::Request::Show { + tab: (pane, entity), + summary, + body, + }); + } + } + }, + Message::DesktopNotify(event) => match event { + desktop_notify::Event::Ready(notify_tx) => { + self.notify_tx_opt = Some(notify_tx); + } + desktop_notify::Event::Activated { + tab: (pane, entity), + token, + } => { + // Clic sur la notification : retour à l'onglet d'origine, + // puis la fenêtre au premier plan avec le jeton + // xdg-activation fourni par le serveur de notifications. + let mut tasks = Vec::new(); + if let Some(tab_model) = self.pane_model.panes.get_mut(pane) + && tab_model.contains_item(entity) + { + tab_model.activate(entity); + self.pane_model.set_focus(pane); + tasks.push(self.update(Message::TabActivate(entity))); + } + if let Some(token) = token { + tasks.push(Task::done(cosmic::Action::Cosmic( + cosmic::app::Action::Activate(token), + ))); + } + return Task::batch(tasks); + } + }, Message::ToggleContextPage(context_page) => { if self.context_page == context_page { self.core.window.show_context = !self.core.window.show_context; @@ -3839,12 +4099,14 @@ impl Application for App { } }, Message::WindowFocused => { + self.window_focused = true; if !self.core.window.show_context { self.pane_model.update_terminal_focus(); } return self.update_focus(); } Message::WindowUnfocused => { + self.window_focused = false; self.pane_model.unfocus_all_terminals(); } Message::ZoomIn => { @@ -3993,6 +4255,7 @@ impl Application for App { &self.config, &self.key_binds, self.active_terminal_monitors(), + self.active_terminal_read_only(), &self.ssh_hosts, )] } @@ -4096,6 +4359,20 @@ impl Application for App { terminal_box = terminal_box.on_mouse_enter(move || Message::MouseEnter(pane)); } + if self.config.copy_on_select_clipboard { + terminal_box = + terminal_box.on_selection_done(move || Message::SelectionDone(pane, entity)); + } + + // [yoda] Voile sur les panneaux non focalisés (à la Ghostty), seulement + // quand il y en a plusieurs : on voit d'un coup d'œil où l'on tape. + if let Some(dim) = self.config.unfocused_pane_dim_ratio() + && self.pane_model.panes.panes.len() > 1 + && pane != self.pane_model.focused() + { + terminal_box = terminal_box.dim(dim); + } + // The terminal records what was under the right press; the widget opens the // menu on release, so build it from that state let context_link = terminal @@ -4116,6 +4393,7 @@ impl Application for App { entity, link, self.terminal_monitors(pane, entity), + self.terminal_read_only(pane, entity), ) }), ) @@ -4264,16 +4542,18 @@ impl Application for App { output.send(Message::TermEventTx(event_tx)).await.unwrap(); while let Some((pane, entity, event)) = event_rx.recv().await { - output - .send(Message::TermEvent(pane, entity, event)) - .await - .unwrap(); + let message = match event { + PtyEvent::Term(event) => Message::TermEvent(pane, entity, event), + PtyEvent::Osc(event) => Message::TermOsc(pane, entity, event), + }; + output.send(message).await.unwrap(); } panic!("terminal event channel closed"); }, ) }), + desktop_notify::subscription().map(Message::DesktopNotify), cosmic_config::config_subscription( TypeId::of::(), Self::APP_ID.into(), diff --git a/src/menu.rs b/src/menu.rs index 4ffb397..3050b91 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -45,6 +45,7 @@ pub fn context_menu( entity: segmented_button::Entity, link: Option, monitors: (bool, bool, bool), + read_only: bool, ) -> Vec> { let item = |label: String, action: Action| MenuItem::Button(label, None, TabAction(entity, action)); @@ -78,6 +79,7 @@ pub fn context_menu( monitors.2, Action::Monitor(MonitorKind::ProcessFinished), ), + check(fl!("read-only"), read_only, Action::ToggleReadOnly), MenuItem::Divider, item(fl!("split-horizontal"), Action::PaneSplitHorizontal), item(fl!("split-vertical"), Action::PaneSplitVertical), @@ -163,6 +165,7 @@ pub fn menu_bar<'a>( config: &Config, key_binds: &HashMap, monitors: (bool, bool, bool), + read_only: bool, ssh_hosts: &[String], ) -> Element<'a, Message> { let mut profile_items = Vec::with_capacity(config.profiles.len()); @@ -234,6 +237,9 @@ pub fn menu_bar<'a>( MenuItem::Button(fl!("next-tab"), None, Action::TabNext), MenuItem::Button(fl!("previous-tab"), None, Action::TabPrev), MenuItem::Divider, + MenuItem::Button(fl!("move-tab-left"), None, Action::TabMoveLeft), + MenuItem::Button(fl!("move-tab-right"), None, Action::TabMoveRight), + MenuItem::Divider, MenuItem::Button( fl!("split-horizontal"), None, @@ -264,6 +270,12 @@ pub fn menu_bar<'a>( monitors.2, Action::Monitor(MonitorKind::ProcessFinished), ), + MenuItem::CheckBox( + fl!("read-only"), + None, + read_only, + Action::ToggleReadOnly, + ), MenuItem::Divider, MenuItem::Button( fl!("menu-color-schemes"), diff --git a/src/osc_tap.rs b/src/osc_tap.rs new file mode 100644 index 0000000..641fab1 --- /dev/null +++ b/src/osc_tap.rs @@ -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, 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, + 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 { + 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 { + 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::() + .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, + } + + impl Read for TapReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + 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) -> io::Result { + 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, + interest: Event, + mode: PollMode, + ) -> io::Result<()> { + unsafe { self.pty.register(poll, interest, mode) } + } + + fn reregister( + &mut self, + poll: &Arc, + interest: Event, + mode: PollMode, + ) -> io::Result<()> { + self.pty.reregister(poll, interest, mode) + } + + fn deregister(&mut self, poll: &Arc) -> 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 { + 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 { + 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")]); + } +} diff --git a/src/shortcuts.rs b/src/shortcuts.rs index a04a103..7b2db24 100644 --- a/src/shortcuts.rs +++ b/src/shortcuts.rs @@ -89,7 +89,10 @@ pub enum KeyBindAction { TabNext, TabPrev, TabRename, + TabMoveLeft, + TabMoveRight, ToggleFullscreen, + ToggleReadOnly, WindowClose, WindowNew, ZoomIn, @@ -131,7 +134,10 @@ impl KeyBindAction { Self::TabNext => Some(Action::TabNext), Self::TabPrev => Some(Action::TabPrev), Self::TabRename => Some(Action::TabRename), + Self::TabMoveLeft => Some(Action::TabMoveLeft), + Self::TabMoveRight => Some(Action::TabMoveRight), Self::ToggleFullscreen => Some(Action::ToggleFullscreen), + Self::ToggleReadOnly => Some(Action::ToggleReadOnly), Self::WindowClose => Some(Action::WindowClose), Self::WindowNew => Some(Action::WindowNew), Self::ZoomIn => Some(Action::ZoomIn), @@ -294,7 +300,10 @@ pub fn action_label(action: KeyBindAction) -> String { KeyBindAction::TabNext => fl!("next-tab"), KeyBindAction::TabPrev => fl!("previous-tab"), KeyBindAction::TabRename => fl!("rename-tab"), + KeyBindAction::TabMoveLeft => fl!("move-tab-left"), + KeyBindAction::TabMoveRight => fl!("move-tab-right"), KeyBindAction::ToggleFullscreen => fl!("toggle-fullscreen"), + KeyBindAction::ToggleReadOnly => fl!("read-only"), KeyBindAction::WindowClose => fl!("close-window"), KeyBindAction::WindowNew => fl!("new-window"), KeyBindAction::ZoomIn => fl!("zoom-in"), @@ -329,6 +338,8 @@ pub fn shortcut_groups() -> Vec { KeyBindAction::TabRename, KeyBindAction::TabNext, KeyBindAction::TabPrev, + KeyBindAction::TabMoveLeft, + KeyBindAction::TabMoveRight, KeyBindAction::TabActivate0, KeyBindAction::TabActivate1, KeyBindAction::TabActivate2, @@ -369,7 +380,11 @@ pub fn shortcut_groups() -> Vec { KeyBindAction::ZoomReset, ], }); - let mut other_actions = vec![KeyBindAction::ClearScrollback, KeyBindAction::SaveOutput]; + let mut other_actions = vec![ + KeyBindAction::ClearScrollback, + KeyBindAction::SaveOutput, + KeyBindAction::ToggleReadOnly, + ]; #[cfg(feature = "password_manager")] other_actions.push(KeyBindAction::PasswordManager); groups.push(ShortcutGroup { @@ -479,6 +494,10 @@ fn fallback_shortcuts() -> Shortcuts { bind!([Ctrl], "Tab", TabNext); bind!([Ctrl, Shift], "Tab", TabPrev); + // Ctrl+Shift+PageUp/Down move tabs left and right + bind!([Ctrl, Shift], "PageUp", TabMoveLeft); + bind!([Ctrl, Shift], "PageDown", TabMoveRight); + // Ctrl+Shift+# activates tabs by index bind!([Ctrl, Shift], "1", TabActivate0); bind!([Ctrl, Shift], "2", TabActivate1); diff --git a/src/terminal.rs b/src/terminal.rs index 683ac9a..213e5fd 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -27,13 +27,15 @@ use indexmap::IndexSet; use std::{ borrow::Cow, collections::HashMap, - fs, io, mem, + fs, + hash::{DefaultHasher, Hash, Hasher}, + io, mem, path::PathBuf, sync::{ Arc, Mutex, Weak, atomic::{AtomicU32, Ordering}, }, - time::Instant, + time::{Duration, Instant}, }; use tokio::sync::mpsc; @@ -43,6 +45,7 @@ use crate::{ config::{ColorSchemeKind, Config as AppConfig, ProfileId}, menu::MenuState, mouse_reporter::MouseReporter, + osc_tap::OscEvent, }; /// Minimum contrast between a fixed cursor color and the cell's background. @@ -65,6 +68,28 @@ pub enum MonitorKind { /// Seconds without output before a silence alert fires (Konsole default). pub const MONITOR_SILENCE_SECONDS: u64 = 10; +/// Appel d'attention d'un onglet en arrière-plan (yoda). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum AttentionKind { + /// Cloche (BEL). + Bell, + /// Notification de bureau (OSC 9 / 777). + Notification, +} + +/// Anti-rafale des notifications d'un onglet, aux valeurs de Ghostty : au plus +/// une par seconde, et une notification identique au plus toutes les 5 s. +const NOTIFY_MIN_INTERVAL: Duration = Duration::from_secs(1); +const NOTIFY_SAME_INTERVAL: Duration = Duration::from_secs(5); + +/// Événement remonté d'un terminal : celui d'alacritty_terminal, ou une +/// séquence OSC interceptée à la sortie du PTY (voir [`crate::osc_tap`]). +#[derive(Debug)] +pub enum PtyEvent { + Term(Event), + Osc(OscEvent), +} + /// https://github.com/alacritty/alacritty/blob/4a7728bf7fac06a35f27f6c4f31e0d9214e5152b/alacritty/src/config/ui_config.rs#L36-L39 fn url_regex_search() -> RegexSearch { let url_regex = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)\ @@ -109,13 +134,13 @@ impl From for WindowSize { pub struct EventProxy( pane_grid::Pane, segmented_button::Entity, - mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, Event)>, + mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>, ); impl EventListener for EventProxy { fn send_event(&self, event: Event) { //TODO: handle error - let _ = self.2.send((self.0, self.1, event)); + let _ = self.2.send((self.0, self.1, PtyEvent::Term(event))); } } @@ -196,23 +221,47 @@ impl TerminalPaneGrid { self.focus } - pub fn update_terminal_focus(&self) { - for (pane, tab_model) in self.panes.panes.iter() { - let entity = tab_model.active(); - if let Some(terminal) = tab_model.data::>(entity) { - let mut terminal = terminal.lock().unwrap(); - terminal.set_focused(self.focus == *pane); - terminal.update(); + pub fn update_terminal_focus(&mut self) { + let focus = self.focus; + for (pane, tab_model) in self.panes.panes.iter_mut() { + let active = tab_model.active(); + let entities: Vec = tab_model.iter().collect(); + for entity in entities { + let mut icon_update = None; + if let Some(terminal) = tab_model.data::>(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) { for tab_model in self.panes.panes.values() { - let entity = tab_model.active(); - if let Some(terminal) = tab_model.data::>(entity) { - let mut terminal = terminal.lock().unwrap(); - terminal.set_focused(false); - terminal.update(); + let active = tab_model.active(); + for entity in tab_model.iter() { + if let Some(terminal) = tab_model.data::>(entity) { + let mut terminal = terminal.lock().unwrap(); + terminal.set_focused(false); + if entity == active { + terminal.update(); + } + } } } } @@ -331,6 +380,11 @@ pub struct Terminal { pub monitor_alert: Option, pub monitor_last_output: Instant, pub monitor_job_running: bool, + pub attention: Option, + notify_last: Option<(Instant, u64)>, + /// [yoda] Lecture seule : aucune saisie de l'utilisateur n'atteint le PTY + /// (touches, collage, souris) ; les réponses du terminal passent toujours. + pub read_only: bool, pub needs_update: bool, pub profile_id_opt: Option, pub tab_title_override: Option, @@ -361,7 +415,7 @@ impl Terminal { pub fn new( pane: pane_grid::Pane, entity: segmented_button::Entity, - event_tx: mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, Event)>, + event_tx: mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>, config: Config, options: Options, app_config: &AppConfig, @@ -413,6 +467,8 @@ impl Terminal { cell_width, cell_height, }; + #[cfg(unix)] + let osc_tx = event_tx.clone(); let event_proxy = EventProxy(pane, entity, event_tx); let term = Arc::new(FairMutex::new(Term::new( config, @@ -427,6 +483,15 @@ impl Terminal { #[cfg(windows)] let shell_pid = pty.child_watcher().pid().map(|pid| pid.get()); + // OSC 9 / 777 : interceptés avant vte, qui les ignore + #[cfg(unix)] + let pty = crate::osc_tap::OscTapPty::new( + pty, + Box::new(move |event| { + let _ = osc_tx.send((pane, entity, PtyEvent::Osc(event))); + }), + )?; + let pty_event_loop = EventLoop::new(term.clone(), event_proxy, pty, options.drain_on_exit, false)?; let notifier = Notifier(pty_event_loop.channel()); @@ -450,6 +515,9 @@ impl Terminal { monitor_alert: None, monitor_last_output: Instant::now(), monitor_job_running: false, + attention: None, + notify_last: None, + read_only: false, mouse_reporter: Default::default(), needs_update: true, notifier, @@ -513,6 +581,25 @@ impl Terminal { self.zoom_adj = value; } + /// Anti-rafale des notifications de bureau de cet onglet ; `true` si + /// celle-ci peut partir. + pub fn notify_allowed(&mut self, title: Option<&str>, body: &str) -> bool { + let mut hasher = DefaultHasher::new(); + (title, body).hash(&mut hasher); + let hash = hasher.finish(); + let now = Instant::now(); + if let Some((last, last_hash)) = self.notify_last { + let elapsed = now.duration_since(last); + if elapsed < NOTIFY_MIN_INTERVAL + || (hash == last_hash && elapsed < NOTIFY_SAME_INTERVAL) + { + return false; + } + } + self.notify_last = Some((now, hash)); + true + } + fn set_focused(&mut self, is_focused: bool) { let focus_changed = self.is_focused != is_focused; self.is_focused = is_focused; @@ -524,7 +611,7 @@ impl Terminal { const FOCUS_OUT: &[u8] = b"\x1b[O"; let input = if is_focused { FOCUS_IN } else { FOCUS_OUT }; - self.input_no_scroll(input); + self.respond(input); } } } @@ -537,7 +624,18 @@ impl Terminal { self.with_buffer_mut(|buffer| buffer.set_redraw(redraw)); } + /// Saisie de l'utilisateur vers le PTY ; ignorée en lecture seule. pub fn input_no_scroll>>(&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>>(&self, input: I) { self.notifier.notify(input); } @@ -555,6 +653,10 @@ impl Terminal { } pub fn input_scroll>>(&self, input: I) { + // En lecture seule, une frappe ne ramène pas non plus la vue en bas + if self.read_only { + return; + } self.input_no_scroll(input); self.scroll(TerminalScroll::Bottom); } @@ -1216,8 +1318,11 @@ impl Terminal { y, ); - for code in codes { - self.notifier.notify(code); + // Saisie de l'utilisateur : rien en lecture seule + if !self.read_only { + for code in codes { + self.notifier.notify(code); + } } } else { self.scroll_as_arrows(delta); @@ -1430,6 +1535,61 @@ impl Drop for Terminal { 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::::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. diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 1df9ae3..c43d440 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -47,7 +47,7 @@ use std::{ }; use crate::{ - Action, Terminal, TerminalScroll, + Action, Terminal, TerminalScroll, kitty_keyboard, menu::MenuState, terminal::{BlockFill, Metadata}, }; @@ -120,8 +120,10 @@ pub struct TerminalBox<'a, Message> { on_context_menu: Option) -> Message + 'a>>, on_mouse_enter: Option Message + 'a>>, opacity: Option, + dim: Option, mouse_inside_boundary: Option, on_middle_click: Option Message + 'a>>, + on_selection_done: Option Message + 'a>>, on_open_hyperlink: Option Message + 'a>>, on_window_focused: Option Message + 'a>>, on_window_unfocused: Option Message + 'a>>, @@ -147,8 +149,10 @@ where on_context_menu: None, on_mouse_enter: None, opacity: None, + dim: None, mouse_inside_boundary: None, on_middle_click: None, + on_selection_done: None, key_binds, on_open_hyperlink: None, on_window_focused: None, @@ -206,6 +210,14 @@ where self } + /// [yoda] Fin d'un geste de sélection fait dans ce panneau (glisser, double + /// ou triple clic, Shift+clic) : le moment de copier la sélection. Un + /// simple clic ailleurs dans la fenêtre ne le déclenche pas. + pub fn on_selection_done(mut self, on_selection_done: impl Fn() -> Message + 'a) -> Self { + self.on_selection_done = Some(Box::new(on_selection_done)); + self + } + pub fn on_middle_click(mut self, on_middle_click: impl Fn() -> Message + 'a) -> Self { self.on_middle_click = Some(Box::new(on_middle_click)); self @@ -216,6 +228,13 @@ where self } + /// [yoda] Voile de la couleur du fond posé sur tout le panneau (texte + /// compris), d'opacité `amount` : panneau non focalisé. + pub fn dim(mut self, amount: f32) -> Self { + self.dim = Some(amount); + self + } + pub fn sharp_corners(mut self, sharp_corners: bool) -> Self { self.sharp_corners = sharp_corners; self @@ -915,6 +934,32 @@ where } } + // [yoda] Panneau non focalisé : voile de la couleur du fond, comme + // `unfocused-split-opacity` de Ghostty. Calque à part, sinon le texte + // (primitive du même calque) serait tracé par-dessus. + if let Some(dim) = self.dim { + let bg = terminal.metadata_set[terminal.default_attrs().metadata].bg; + renderer.with_layer(layout.bounds(), |renderer| { + renderer.fill_quad( + Quad { + bounds: layout.bounds(), + border: Border { + radius: border_radius, + ..Default::default() + }, + snap: true, + ..Default::default() + }, + Color::from_rgba( + f32::from(bg.r()) / 255.0, + f32::from(bg.g()) / 255.0, + f32::from(bg.b()) / 255.0, + dim, + ), + ); + }); + } + let duration = instant.elapsed(); log::trace!("redraw {}, {}: {:?}", view_w, view_h, duration); } @@ -939,7 +984,11 @@ where let buffer_size = terminal.with_buffer(|buffer| buffer.size()); let is_app_cursor = terminal.term.lock().mode().contains(TermMode::APP_CURSOR); - let is_mouse_mode = terminal.term.lock().mode().intersects(TermMode::MOUSE_MODE); + // [yoda] En lecture seule, la souris revient au terminal : sélection et + // défilement locaux, même dans une application qui la capture. + let is_mouse_mode = terminal.term.lock().mode().intersects(TermMode::MOUSE_MODE) + && !terminal.read_only; + let term_mode = *terminal.term.lock().mode(); match event { Event::Window(event) => match event { cosmic::iced::window::Event::Focused => { @@ -979,6 +1028,101 @@ where } _ => {} }, + // [yoda] Protocole clavier kitty, quand l'application au premier plan + // l'a demandé (voir `kitty_keyboard`) ; sinon encodage legacy plus bas. + Event::Keyboard(KeyEvent::KeyPressed { + key, + modified_key, + physical_key, + location, + modifiers, + text, + repeat, + }) if state.is_focused && kitty_keyboard::active(term_mode) => { + for key_bind in self.key_binds.keys() { + if key_bind.matches(*modifiers, key, Some(physical_key)) { + shell.capture_event(); + return; + } + } + + // Comportements locaux conservés, comme en legacy : Shift+PageUp… + // fait défiler l'historique, Échap annule une sélection. + let local_scroll = match modified_key { + Key::Named(Named::PageUp) if modifiers.shift() => Some(TerminalScroll::PageUp), + Key::Named(Named::PageDown) if modifiers.shift() => { + Some(TerminalScroll::PageDown) + } + Key::Named(Named::Home) if modifiers.shift() => Some(TerminalScroll::Top), + Key::Named(Named::End) if modifiers.shift() => Some(TerminalScroll::Bottom), + _ => None, + }; + if let Some(scroll) = local_scroll { + terminal.scroll(scroll); + shell.capture_event(); + return; + } + if *modified_key == Key::Named(Named::Escape) { + let had_selection = terminal.term.lock().selection.take().is_some(); + if had_selection { + terminal.update(); + shell.capture_event(); + return; + } + } + + let input = kitty_keyboard::KeyInput { + key, + modified_key, + location: *location, + modifiers: *modifiers, + text: text.as_deref(), + action: if *repeat { + kitty_keyboard::KeyAction::Repeat + } else { + kitty_keyboard::KeyAction::Press + }, + }; + let bytes = kitty_keyboard::encode(&input, term_mode); + if !bytes.is_empty() { + // Une touche de modification seule (« toutes les touches ») + // ne ramène pas la vue en bas. + let modifier_only = matches!( + modified_key, + Key::Named(Named::Shift | Named::Control | Named::Alt | Named::Super) + ); + if modifier_only { + terminal.input_no_scroll(bytes); + } else { + terminal.input_scroll(bytes); + } + shell.capture_event(); + } + } + Event::Keyboard(KeyEvent::KeyReleased { + key, + modified_key, + location, + modifiers, + .. + }) if state.is_focused + && kitty_keyboard::active(term_mode) + && term_mode.contains(TermMode::REPORT_EVENT_TYPES) => + { + let input = kitty_keyboard::KeyInput { + key, + modified_key, + location: *location, + modifiers: *modifiers, + text: None, + action: kitty_keyboard::KeyAction::Release, + }; + let bytes = kitty_keyboard::encode(&input, term_mode); + if !bytes.is_empty() { + terminal.input_no_scroll(bytes); + shell.capture_event(); + } + } Event::Keyboard(KeyEvent::KeyPressed { key: Key::Named(named), physical_key, @@ -1094,7 +1238,15 @@ where shell.capture_event(); } Named::Enter => { - terminal.input_scroll(format!("{}{}", alt_prefix, "\x0D").into_bytes()); + // [yoda] Shift+Entrée envoie LF (`\n`, comme Ctrl+J) : saut de + // ligne dans les éditeurs qui ne parlent pas le protocole kitty + // (rustyline de Modjo/Goose) ; pour un shell, c'est Entrée. + let code = if modifiers.shift() && !modifiers.control() { + "\x0A" + } else { + "\x0D" + }; + terminal.input_scroll(format!("{alt_prefix}{code}").into_bytes()); shell.capture_event(); } Named::Escape => { @@ -1439,6 +1591,9 @@ where } } terminal.needs_update = true; + if let Some(on_selection_done) = &self.on_selection_done { + shell.publish(on_selection_done()); + } } if let Some(p) = cursor_position.position_in(layout.bounds()) { let x = p.x - self.padding.left;