diff options
| author | Ayose <ayosec@gmail.com> | 2021-04-18 22:45:19 +0100 |
|---|---|---|
| committer | Ayose <ayosec@gmail.com> | 2021-04-18 22:45:19 +0100 |
| commit | e5e9c8293535ea6eaaa3d017cbfb322c7ac95c14 (patch) | |
| tree | b805d27d397246e3684224e7aac0b833d763e7b6 | |
| parent | fd0218f2545762c0d13dc17a55a1b6a334e31c19 (diff) | |
| parent | 28abb1f9c78ab316126bdf94e2ca12f034f1d8fd (diff) | |
| download | r-alacritty-e5e9c8293535ea6eaaa3d017cbfb322c7ac95c14.tar.gz r-alacritty-e5e9c8293535ea6eaaa3d017cbfb322c7ac95c14.tar.bz2 r-alacritty-e5e9c8293535ea6eaaa3d017cbfb322c7ac95c14.zip | |
Merge remote-tracking branch 'vendor/master' into graphics
| -rw-r--r-- | CHANGELOG.md | 5 | ||||
| -rw-r--r-- | Cargo.lock | 203 | ||||
| -rw-r--r-- | alacritty.yml | 99 | ||||
| -rw-r--r-- | alacritty/Cargo.toml | 1 | ||||
| -rw-r--r-- | alacritty/src/config/bindings.rs | 7 | ||||
| -rw-r--r-- | alacritty/src/config/mouse.rs | 38 | ||||
| -rw-r--r-- | alacritty/src/config/ui_config.rs | 101 | ||||
| -rw-r--r-- | alacritty/src/display/content.rs | 24 | ||||
| -rw-r--r-- | alacritty/src/display/hint.rs | 188 | ||||
| -rw-r--r-- | alacritty/src/display/mod.rs | 122 | ||||
| -rw-r--r-- | alacritty/src/event.rs | 169 | ||||
| -rw-r--r-- | alacritty/src/input.rs | 288 | ||||
| -rw-r--r-- | alacritty/src/main.rs | 1 | ||||
| -rw-r--r-- | alacritty_terminal/Cargo.toml | 2 | ||||
| -rw-r--r-- | alacritty_terminal/src/ansi.rs | 93 | ||||
| -rw-r--r-- | alacritty_terminal/src/event.rs | 4 | ||||
| -rw-r--r-- | alacritty_terminal/src/event_loop.rs | 4 | ||||
| -rw-r--r-- | alacritty_terminal/src/grid/resize.rs | 4 | ||||
| -rw-r--r-- | alacritty_terminal/src/term/mod.rs | 50 | ||||
| -rw-r--r-- | alacritty_terminal/src/term/search.rs | 4 | ||||
| -rw-r--r-- | alacritty_terminal/tests/ref.rs | 4 | ||||
| -rw-r--r-- | docs/features.md | 12 |
22 files changed, 791 insertions, 632 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 74391ed8..2df7c779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Synchronized terminal updates using `DCS = 1 s ST`/`DCS = 2 s ST` - Regex terminal hints ([see features.md](./docs/features.md#hints)) - Support for Sixel protocol +- macOS keybinding (cmd+alt+H) hiding all windows other than Alacritty + +### Changed + +- The vi mode cursor is now created in the top-left if the terminal cursor is invisible ### Fixed @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "ab_glyph_rasterizer" version = "0.1.4" @@ -41,7 +43,6 @@ dependencies = [ "serde_yaml", "time", "unicode-width", - "urlocator", "wayland-client", "winapi 0.3.9", "x11-dl", @@ -73,8 +74,8 @@ dependencies = [ "mio", "mio-anonymous-pipes", "mio-extras", - "miow 0.3.6", - "nix 0.19.1", + "miow 0.3.7", + "nix 0.20.0", "parking_lot", "regex-automata", "serde", @@ -174,9 +175,9 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "byteorder" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "calloop" @@ -421,14 +422,13 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae8f328835f8f5a6ceb6a7842a7f2d0c03692adb5c889347235d59194731fe3" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" dependencies = [ "autocfg", "cfg-if 1.0.0", "lazy_static", - "loom", ] [[package]] @@ -530,7 +530,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b11f15d1e3268f140f68d390637d5e76d849782d971ae7063e0da69fe9709a76" dependencies = [ - "libloading", + "libloading 0.6.7", +] + +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading 0.7.0", ] [[package]] @@ -541,9 +550,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "dtoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" [[package]] name = "dwrote" @@ -588,7 +597,7 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.5", + "redox_syscall 0.2.6", "winapi 0.3.9", ] @@ -698,19 +707,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] -name = "generator" -version = "0.6.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fed24fd1e18827652b4d55652899a1e9da8e54d91624dc3437a5bc3a9f9a9c" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "winapi 0.3.9", -] - -[[package]] name = "getrandom" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -748,7 +744,7 @@ dependencies = [ "glutin_glx_sys", "glutin_wgl_sys", "lazy_static", - "libloading", + "libloading 0.6.7", "log", "objc", "osmesa-sys", @@ -905,9 +901,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.86" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" [[package]] name = "libloading" @@ -920,6 +916,16 @@ dependencies = [ ] [[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if 1.0.0", + "winapi 0.3.9", +] + +[[package]] name = "linked-hash-map" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -927,9 +933,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "lock_api" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176" dependencies = [ "scopeguard", ] @@ -945,17 +951,6 @@ dependencies = [ ] [[package]] -name = "loom" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d44c73b4636e497b4917eb21c33539efa3816741a2d3ff26c6316f1b529481a4" -dependencies = [ - "cfg-if 1.0.0", - "generator", - "scoped-tls", -] - -[[package]] name = "malloc_buf" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1029,7 +1024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c274c3c52dcd1d78c5d7ed841eca1e9ea2db8353f3b8ec25789cc62c471aaf" dependencies = [ "mio", - "miow 0.3.6", + "miow 0.3.7", "spsc-buffer", "winapi 0.3.9", ] @@ -1060,11 +1055,10 @@ dependencies = [ [[package]] name = "miow" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "socket2", "winapi 0.3.9", ] @@ -1138,9 +1132,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" dependencies = [ "bitflags", "cc", @@ -1160,9 +1154,9 @@ dependencies = [ [[package]] name = "notify" -version = "4.0.15" +version = "4.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd" +checksum = "2599080e87c9bd051ddb11b10074f4da7b1223298df65d4c2ec5bcf309af1533" dependencies = [ "bitflags", "filetime", @@ -1229,9 +1223,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.5.2" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] name = "osmesa-sys" @@ -1271,7 +1265,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.5", + "redox_syscall 0.2.6", "smallvec", "winapi 0.3.9", ] @@ -1310,9 +1304,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" dependencies = [ "unicode-xid", ] @@ -1343,9 +1337,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" dependencies = [ "bitflags", ] @@ -1373,9 +1367,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.22" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" [[package]] name = "rust-argon2" @@ -1400,12 +1394,6 @@ dependencies = [ ] [[package]] -name = "rustversion" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" - -[[package]] name = "ryu" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1434,18 +1422,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.123" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.123" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" +checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" dependencies = [ "proc-macro2", "quote", @@ -1454,9 +1442,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.62" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ "itoa", "ryu", @@ -1540,14 +1528,14 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" [[package]] name = "smithay-client-toolkit" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "316e13a3eb853ce7bf72ad3530dc186cb2005c57c521ef5f4ada5ee4eed74de6" +checksum = "4750c76fd5d3ac95fa3ed80fe667d6a3d8590a960e5b575b98eea93339a80b80" dependencies = [ "andrew", "bitflags", "calloop", - "dlib", + "dlib 0.4.2", "lazy_static", "log", "memmap2", @@ -1568,17 +1556,6 @@ dependencies = [ ] [[package]] -name = "socket2" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "winapi 0.3.9", -] - -[[package]] name = "spsc-buffer" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1598,9 +1575,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "syn" -version = "1.0.60" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" dependencies = [ "proc-macro2", "quote", @@ -1675,12 +1652,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] -name = "urlocator" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e39a4f106dafb0a748b951494667a44e62b55fd7942b4fc12706d63cc535a0" - -[[package]] name = "utf8parse" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1694,9 +1665,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" [[package]] name = "vswhom" @@ -1720,9 +1691,9 @@ dependencies = [ [[package]] name = "vte" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df25fed2855d2fbcbdf1016c69a6ac070fa1aabc8b5d7aedaab8703dce0d2d6" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" dependencies = [ "utf8parse", "vte_generate_state_changes", @@ -1740,9 +1711,9 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", "winapi 0.3.9", @@ -1763,14 +1734,14 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wayland-client" -version = "0.28.3" +version = "0.28.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdbdbe01d03b2267809f3ed99495b37395387fde789e0f2ebb78e8b43f75b6d7" +checksum = "06ca44d86554b85cf449f1557edc6cc7da935cc748c8e4bf1c507cbd43bae02c" dependencies = [ "bitflags", "downcast-rs", "libc", - "nix 0.18.0", + "nix 0.20.0", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -1779,11 +1750,11 @@ dependencies = [ [[package]] name = "wayland-commons" -version = "0.28.3" +version = "0.28.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480450f76717edd64ad04a4426280d737fc3d10a236b982df7b1aee19f0e2d56" +checksum = "8bd75ae380325dbcff2707f0cd9869827ea1d2d6d534cff076858d3f0460fd5a" dependencies = [ - "nix 0.18.0", + "nix 0.20.0", "once_cell", "smallvec", "wayland-sys", @@ -1791,20 +1762,20 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.28.3" +version = "0.28.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6eb122c160223a7660feeaf949d0100281d1279acaaed3720eb3c9894496e5f" +checksum = "b37e5455ec72f5de555ec39b5c3704036ac07c2ecd50d0bffe02d5fe2d4e65ab" dependencies = [ - "nix 0.18.0", + "nix 0.20.0", "wayland-client", "xcursor", ] [[package]] name = "wayland-egl" -version = "0.28.3" +version = "0.28.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c653507447113c967a1aeee413699acb42d96d6302ec967c6d51930eae8aa7f5" +checksum = "9461a67930ec16da7a4fd8b50e9ffa23f4417240b43ec84008bd1b2c94421c94" dependencies = [ "wayland-client", "wayland-sys", @@ -1812,9 +1783,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.28.3" +version = "0.28.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "319a82b4d3054dd25acc32d9aee0f84fa95b63bc983fffe4703b6b8d47e01a30" +checksum = "95df3317872bcf9eec096c864b69aa4769a1d5d6291a5b513f8ba0af0efbd52c" dependencies = [ "bitflags", "wayland-client", @@ -1824,9 +1795,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.28.3" +version = "0.28.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7010ba5767b3fcd350decc59055390b4ebe6bd1b9279a9feb1f1888987f1133d" +checksum = "389d680d7bd67512dc9c37f39560224327038deb0f0e8d33f870900441b68720" dependencies = [ "proc-macro2", "quote", @@ -1835,11 +1806,11 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.28.3" +version = "0.28.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6793834e0c35d11fd96a97297abe03d37be627e1847da52e17d7e0e3b51cc099" +checksum = "2907bd297eef464a95ba9349ea771611771aa285b932526c633dc94d5400a8e2" dependencies = [ - "dlib", + "dlib 0.5.0", "lazy_static", "pkg-config", ] diff --git a/alacritty.yml b/alacritty.yml index 3df946a9..8c3a1ab0 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -445,29 +445,6 @@ # If this is `true`, the cursor is temporarily hidden when typing. #hide_when_typing: false - #url: - # URL launcher - # - # This program is executed when clicking on a text which is recognized as a - # URL. The URL is always added to the command as the last parameter. - # - # When set to `launcher: None`, URL launching will be disabled completely. - # - # Default: - # - (macOS) open - # - (Linux/BSD) xdg-open - # - (Windows) cmd /c start "" - #launcher: - # program: xdg-open - # args: [] - - # URL modifiers - # - # These are the modifiers that need to be held down for opening URLs when - # clicking on them. The available modifiers are documented in the key - # binding section. - #modifiers: None - # Regex hints # # Terminal hints can be used to find text in the visible part of the terminal @@ -478,10 +455,18 @@ # List with all available hints # - # Each hint takes a `regex`, `binding` and either a `command` or an `action`. + # Each hint must have a `regex` and either an `action` or a `command` field. + # The fields `mouse`, `binding` and `post_processing` are optional. + # + # The fields `command`, `binding.key`, `binding.mods` and `mouse.mods` accept + # the same values as they do in the `key_bindings` section. + # + # The `mouse.enabled` field controls if the hint should be underlined while + # the mouse with all `mouse.mods` keys held or the vi mode cursor is above it. # - # The fields `command`, `binding.key` and `binding.mods` accept the same - # values as they do in the `key_bindings` section. + # If the `post_processing` field is set to `true`, heuristics will be used to + # shorten the match if there are characters likely not to be part of the hint + # (e.g. a trailing `.`). This is most useful for URIs. # # Values for `action`: # - Copy @@ -490,16 +475,19 @@ # Paste the hint's text to the terminal or search. # - Select # Select the hint's text. - # - # Example - # - # enabled: - # - regex: "alacritty/alacritty#\\d*" - # command: firefox - # binding: - # key: G - # mods: Control|Shift - #enabled: [] + # - MoveViModeCursor + # Move the vi mode cursor to the beginning of the hint. + #enabled: + # - regex: "(mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ + # [^\u0000-\u001F\u007F-\u009F<>\" {-}\\^⟨⟩`]+" + # command: xdg-open + # post_processing: true + # mouse: + # enabled: true + # mods: None + # binding: + # key: U + # mods: Control|Shift # Mouse bindings # @@ -821,25 +809,26 @@ #- { key: Return, mods: Alt, action: ToggleFullscreen } # (macOS only) - #- { key: K, mods: Command, mode: ~Vi|~Search, chars: "\x0c" } - #- { key: K, mods: Command, mode: ~Vi|~Search, action: ClearHistory } - #- { key: Key0, mods: Command, action: ResetFontSize } - #- { key: Equals, mods: Command, action: IncreaseFontSize } - #- { key: Plus, mods: Command, action: IncreaseFontSize } - #- { key: NumpadAdd, mods: Command, action: IncreaseFontSize } - #- { key: Minus, mods: Command, action: DecreaseFontSize } - #- { key: NumpadSubtract, mods: Command, action: DecreaseFontSize } - #- { key: V, mods: Command, action: Paste } - #- { key: C, mods: Command, action: Copy } - #- { key: C, mods: Command, mode: Vi|~Search, action: ClearSelection } - #- { key: H, mods: Command, action: Hide } - #- { key: M, mods: Command, action: Minimize } - #- { key: Q, mods: Command, action: Quit } - #- { key: W, mods: Command, action: Quit } - #- { key: N, mods: Command, action: SpawnNewInstance } - #- { key: F, mods: Command|Control, action: ToggleFullscreen } - #- { key: F, mods: Command, mode: ~Search, action: SearchForward } - #- { key: B, mods: Command, mode: ~Search, action: SearchBackward } + #- { key: K, mods: Command, mode: ~Vi|~Search, chars: "\x0c" } + #- { key: K, mods: Command, mode: ~Vi|~Search, action: ClearHistory } + #- { key: Key0, mods: Command, action: ResetFontSize } + #- { key: Equals, mods: Command, action: IncreaseFontSize } + #- { key: Plus, mods: Command, action: IncreaseFontSize } + #- { key: NumpadAdd, mods: Command, action: IncreaseFontSize } + #- { key: Minus, mods: Command, action: DecreaseFontSize } + #- { key: NumpadSubtract, mods: Command, action: DecreaseFontSize } + #- { key: V, mods: Command, action: Paste } + #- { key: C, mods: Command, action: Copy } + #- { key: C, mods: Command, mode: Vi|~Search, action: ClearSelection } + #- { key: H, mods: Command, action: Hide } + #- { key: H, mods: Command|Alt, action: HideOtherApplications } + #- { key: M, mods: Command, action: Minimize } + #- { key: Q, mods: Command, action: Quit } + #- { key: W, mods: Command, action: Quit } + #- { key: N, mods: Command, action: SpawnNewInstance } + #- { key: F, mods: Command|Control, action: ToggleFullscreen } + #- { key: F, mods: Command, mode: ~Search, action: SearchForward } + #- { key: B, mods: Command, mode: ~Search, action: SearchBackward } #debug: # Display the time it takes to redraw each frame. diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index 5f0c06ae..31e63d87 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -29,7 +29,6 @@ glutin = { version = "0.26.0", default-features = false, features = ["serde"] } notify = "4" parking_lot = "0.11.0" crossfont = { version = "0.2.0", features = ["force_system_fontconfig"] } -urlocator = "0.1.3" copypasta = { version = "0.7.0", default-features = false } libc = "0.2" unicode-width = "0.1" diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index 732875db..4e7c2fcb 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -161,6 +161,10 @@ pub enum Action { /// Hide the Alacritty window. Hide, + /// Hide all windows other than Alacritty on macOS. + #[cfg(target_os = "macos")] + HideOtherApplications, + /// Minimize the Alacritty window. Minimize, @@ -685,6 +689,7 @@ pub fn platform_key_bindings() -> Vec<KeyBinding> { C, ModifiersState::LOGO; Action::Copy; C, ModifiersState::LOGO, +BindingMode::VI, ~BindingMode::SEARCH; Action::ClearSelection; H, ModifiersState::LOGO; Action::Hide; + H, ModifiersState::LOGO | ModifiersState::ALT; Action::HideOtherApplications; M, ModifiersState::LOGO; Action::Minimize; Q, ModifiersState::LOGO; Action::Quit; W, ModifiersState::LOGO; Action::Quit; @@ -1157,7 +1162,7 @@ impl<'a> de::Deserialize<'a> for ModsWrapper { type Value = ModsWrapper; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("a subset of Shift|Control|Super|Command|Alt|Option") + f.write_str("None or a subset of Shift|Control|Super|Command|Alt|Option") } fn visit_str<E>(self, value: &str) -> Result<ModsWrapper, E> diff --git a/alacritty/src/config/mouse.rs b/alacritty/src/config/mouse.rs index 2aa7557c..aed1ab04 100644 --- a/alacritty/src/config/mouse.rs +++ b/alacritty/src/config/mouse.rs @@ -1,50 +1,12 @@ use std::time::Duration; -use glutin::event::ModifiersState; - use alacritty_config_derive::ConfigDeserialize; -use alacritty_terminal::config::Program; - -use crate::config::bindings::ModsWrapper; #[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq, Eq)] pub struct Mouse { pub double_click: ClickHandler, pub triple_click: ClickHandler, pub hide_when_typing: bool, - pub url: Url, -} - -#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] -pub struct Url { - /// Program for opening links. - pub launcher: Option<Program>, - - /// Modifier used to open links. - modifiers: ModsWrapper, -} - -impl Url { - pub fn mods(&self) -> ModifiersState { - self.modifiers.into_inner() - } -} - -impl Default for Url { - fn default() -> Url { - Url { - #[cfg(not(any(target_os = "macos", windows)))] - launcher: Some(Program::Just(String::from("xdg-open"))), - #[cfg(target_os = "macos")] - launcher: Some(Program::Just(String::from("open"))), - #[cfg(windows)] - launcher: Some(Program::WithArgs { - program: String::from("cmd"), - args: vec!["/c".to_string(), "start".to_string(), "".to_string()], - }), - modifiers: Default::default(), - } - } } #[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index 3cd2ad88..135d12c8 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -21,6 +21,11 @@ use crate::config::font::Font; use crate::config::mouse::Mouse; use crate::config::window::WindowConfig; +/// Regex used for the default URL hint. +#[rustfmt::skip] +const URL_REGEX: &str = "(mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ + [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\" {-}\\^⟨⟩`]+"; + #[derive(ConfigDeserialize, Debug, PartialEq)] pub struct UiConfig { /// Font configuration. @@ -90,13 +95,18 @@ impl Default for UiConfig { impl UiConfig { /// Generate key bindings for all keyboard hints. pub fn generate_hint_bindings(&mut self) { - for hint in self.hints.enabled.drain(..) { + for hint in &self.hints.enabled { + let binding = match hint.binding { + Some(binding) => binding, + None => continue, + }; + let binding = KeyBinding { - trigger: hint.binding.key, - mods: hint.binding.mods.0, + trigger: binding.key, + mods: binding.mods.0, mode: BindingMode::empty(), notmode: BindingMode::empty(), - action: Action::Hint(hint), + action: Action::Hint(hint.clone()), }; self.key_bindings.0.push(binding); @@ -197,13 +207,42 @@ pub struct Delta<T: Default> { } /// Regex terminal hints. -#[derive(ConfigDeserialize, Default, Debug, PartialEq, Eq)] +#[derive(ConfigDeserialize, Debug, PartialEq, Eq)] pub struct Hints { /// Characters for the hint labels. alphabet: HintsAlphabet, /// All configured terminal hints. - enabled: Vec<Hint>, + pub enabled: Vec<Hint>, +} + +impl Default for Hints { + fn default() -> Self { + // Add URL hint by default when no other hint is present. + let pattern = LazyRegexVariant::Pattern(String::from(URL_REGEX)); + let regex = LazyRegex(Rc::new(RefCell::new(pattern))); + + #[cfg(not(any(target_os = "macos", windows)))] + let action = HintAction::Command(Program::Just(String::from("xdg-open"))); + #[cfg(target_os = "macos")] + let action = HintAction::Command(Program::Just(String::from("open"))); + #[cfg(windows)] + let action = HintAction::Command(Program::WithArgs { + program: String::from("cmd"), + args: vec!["/c".to_string(), "start".to_string(), "".to_string()], + }); + + Self { + enabled: vec![Hint { + regex, + action, + post_processing: true, + mouse: Some(HintMouse { enabled: true, mods: Default::default() }), + binding: Default::default(), + }], + alphabet: Default::default(), + } + } } impl Hints { @@ -254,6 +293,8 @@ pub enum HintInternalAction { Paste, /// Select the text matching the hint. Select, + /// Move the vi mode cursor to the beginning of the hint. + MoveViModeCursor, } /// Actions for hint bindings. @@ -271,33 +312,51 @@ pub enum HintAction { /// Hint configuration. #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct Hint { + /// Regex for finding matches. + pub regex: LazyRegex, + /// Action executed when this hint is triggered. #[serde(flatten)] pub action: HintAction, - /// Regex for finding matches. - pub regex: LazyRegex, + /// Hint text post processing. + #[serde(default)] + pub post_processing: bool, + + /// Hint mouse highlighting. + pub mouse: Option<HintMouse>, /// Binding required to search for this hint. - binding: HintBinding, + binding: Option<HintBinding>, } /// Binding for triggering a keyboard hint. #[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)] pub struct HintBinding { pub key: Key, + #[serde(default)] + pub mods: ModsWrapper, +} + +/// Hint mouse highlighting. +#[derive(ConfigDeserialize, Default, Copy, Clone, Debug, PartialEq, Eq)] +pub struct HintMouse { + /// Hint mouse highlighting availability. + pub enabled: bool, + + /// Required mouse modifiers for hint highlighting. pub mods: ModsWrapper, } /// Lazy regex with interior mutability. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct LazyRegex(Rc<RefCell<LazyRegexVariant>>); impl LazyRegex { /// Execute a function with the compiled regex DFAs as parameter. - pub fn with_compiled<T, F>(&self, f: F) -> T + pub fn with_compiled<T, F>(&self, mut f: F) -> T where - F: Fn(&RegexSearch) -> T, + F: FnMut(&RegexSearch) -> T, { f(self.0.borrow_mut().compiled()) } @@ -313,14 +372,6 @@ impl<'de> Deserialize<'de> for LazyRegex { } } -/// Implement placeholder to allow derive upstream, since we never need it for this struct itself. -impl PartialEq for LazyRegex { - fn eq(&self, _other: &Self) -> bool { - false - } -} -impl Eq for LazyRegex {} - /// Regex which is compiled on demand, to avoid expensive computations at startup. #[derive(Clone, Debug)] pub enum LazyRegexVariant { @@ -357,3 +408,13 @@ impl LazyRegexVariant { } } } + +impl PartialEq for LazyRegexVariant { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Pattern(regex), Self::Pattern(other_regex)) => regex == other_regex, + _ => false, + } + } +} +impl Eq for LazyRegexVariant {} diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs index ae4bdd26..7b412c1d 100644 --- a/alacritty/src/display/content.rs +++ b/alacritty/src/display/content.rs @@ -19,15 +19,12 @@ use alacritty_terminal::term::{ use crate::config::ui_config::UiConfig; use crate::display::color::{List, DIM_FACTOR}; use crate::display::hint::HintState; -use crate::display::Display; +use crate::display::{self, Display, MAX_SEARCH_LINES}; use crate::event::SearchState; /// Minimum contrast between a fixed cursor color and the cell's background. pub const MIN_CURSOR_CONTRAST: f64 = 1.5; -/// Maximum number of linewraps followed outside of the viewport during search highlighting. -const MAX_SEARCH_LINES: usize = 100; - /// Renderable terminal content. /// /// This provides the terminal cursor and an iterator over all non-empty cells. @@ -139,8 +136,8 @@ impl<'a> RenderableContent<'a> { // Convert cursor point to viewport position. let cursor_point = self.terminal_cursor.point; - let line = (cursor_point.line + self.terminal_content.display_offset as i32).0 as usize; - let point = Point::new(line, cursor_point.column); + let display_offset = self.terminal_content.display_offset; + let point = display::point_to_viewport(display_offset, cursor_point).unwrap(); Some(RenderableCursor { shape: self.terminal_cursor.shape, @@ -221,10 +218,12 @@ impl RenderableCell { .selection .map_or(false, |selection| selection.contains_cell(&cell, content.terminal_cursor)); + let display_offset = content.terminal_content.display_offset; + let viewport_start = Point::new(Line(-(display_offset as i32)), Column(0)); + let colors = &content.config.ui_config.colors; let mut character = cell.c; - let colors = &content.config.ui_config.colors; - if let Some((c, is_first)) = content.hint.advance(cell.point) { + if let Some((c, is_first)) = content.hint.advance(viewport_start, cell.point) { let (config_fg, config_bg) = if is_first { (colors.hints.start.foreground, colors.hints.start.background) } else { @@ -260,8 +259,7 @@ impl RenderableCell { // Convert cell point to viewport position. let cell_point = cell.point; - let line = (cell_point.line + content.terminal_content.display_offset as i32).0 as usize; - let point = Point::new(line, cell_point.column); + let point = display::point_to_viewport(display_offset, cell_point).unwrap(); RenderableCell { zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()), @@ -412,7 +410,7 @@ impl<'a> Hint<'a> { /// this position will be returned. /// /// The tuple's [`bool`] will be `true` when the character is the first for this hint. - fn advance(&mut self, point: Point) -> Option<(char, bool)> { + fn advance(&mut self, viewport_start: Point, point: Point) -> Option<(char, bool)> { // Check if we're within a match at all. if !self.regex.advance(point) { return None; @@ -423,7 +421,7 @@ impl<'a> Hint<'a> { .regex .matches .get(self.regex.index) - .map(|regex_match| regex_match.start()) + .map(|regex_match| max(*regex_match.start(), viewport_start)) .filter(|start| start.line == point.line)?; // Position within the hint label. @@ -444,7 +442,7 @@ impl<'a> From<&'a HintState> for Hint<'a> { /// Wrapper for finding visible regex matches. #[derive(Default, Clone)] -pub struct RegexMatches(Vec<RangeInclusive<Point>>); +pub struct RegexMatches(pub Vec<RangeInclusive<Point>>); impl RegexMatches { /// Find all visible matches. diff --git a/alacritty/src/display/hint.rs b/alacritty/src/display/hint.rs index 2a5e9c65..6ab68f10 100644 --- a/alacritty/src/display/hint.rs +++ b/alacritty/src/display/hint.rs @@ -1,8 +1,16 @@ -use alacritty_terminal::term::search::Match; -use alacritty_terminal::term::Term; +use std::cmp::{max, min}; + +use glutin::event::ModifiersState; + +use alacritty_terminal::grid::BidirectionalIterator; +use alacritty_terminal::index::{Boundary, Point}; +use alacritty_terminal::term::search::{Match, RegexSearch}; +use alacritty_terminal::term::{Term, TermMode}; use crate::config::ui_config::{Hint, HintAction}; +use crate::config::Config; use crate::display::content::RegexMatches; +use crate::display::MAX_SEARCH_LINES; /// Percentage of characters in the hints alphabet used for the last character. const HINT_SPLIT_PERCENTAGE: f32 = 0.5; @@ -63,7 +71,20 @@ impl HintState { }; // Find visible matches. - self.matches = hint.regex.with_compiled(|regex| RegexMatches::new(term, regex)); + self.matches.0 = hint.regex.with_compiled(|regex| { + let mut matches = RegexMatches::new(term, regex); + + // Apply post-processing and search for sub-matches if necessary. + if hint.post_processing { + matches + .drain(..) + .map(|rm| HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>()) + .flatten() + .collect() + } else { + matches.0 + } + }); // Cancel highlight with no visible matches. if self.matches.is_empty() { @@ -144,6 +165,7 @@ impl HintState { } /// Hint match which was selected by the user. +#[derive(Clone)] pub struct HintMatch { /// Action for handling the text. pub action: HintAction, @@ -217,6 +239,166 @@ impl HintLabels { } } +/// Check if there is a hint highlighted at the specified point. +pub fn highlighted_at<T>( + term: &Term<T>, + config: &Config, + point: Point, + mouse_mods: ModifiersState, +) -> Option<HintMatch> { + let mouse_mode = term.mode().intersects(TermMode::MOUSE_MODE); + + config.ui_config.hints.enabled.iter().find_map(|hint| { + // Check if all required modifiers are pressed. + let highlight = hint.mouse.map_or(false, |mouse| { + mouse.enabled + && mouse_mods.contains(mouse.mods.0) + && (!mouse_mode || mouse_mods.contains(ModifiersState::SHIFT)) + }); + if !highlight { + return None; + } + + hint.regex.with_compiled(|regex| { + // Setup search boundaries. + let mut start = term.line_search_left(point); + start.line = max(start.line, point.line - MAX_SEARCH_LINES); + let mut end = term.line_search_right(point); + end.line = min(end.line, point.line + MAX_SEARCH_LINES); + + // Function to verify if the specified point is inside the match. + let at_point = |rm: &Match| *rm.start() <= point && *rm.end() >= point; + + // Check if there's any match at the specified point. + let regex_match = term.regex_search_right(regex, start, end).filter(at_point)?; + + // Apply post-processing and search for sub-matches if necessary. + let regex_match = if hint.post_processing { + HintPostProcessor::new(term, regex, regex_match).find(at_point) + } else { + Some(regex_match) + }; + + regex_match.map(|bounds| HintMatch { action: hint.action.clone(), bounds }) + }) + }) +} + +/// Iterator over all post-processed matches inside an existing hint match. +struct HintPostProcessor<'a, T> { + /// Regex search DFAs. + regex: &'a RegexSearch, + + /// Terminal reference. + term: &'a Term<T>, + + /// Next hint match in the iterator. + next_match: Option<Match>, + + /// Start point for the next search. + start: Point, + + /// End point for the hint match iterator. + end: Point, +} + +impl<'a, T> HintPostProcessor<'a, T> { + /// Create a new iterator for an unprocessed match. + fn new(term: &'a Term<T>, regex: &'a RegexSearch, regex_match: Match) -> Self { + let end = *regex_match.end(); + let mut post_processor = Self { next_match: None, start: end, end, term, regex }; + + // Post-process the first hint match. + let next_match = post_processor.hint_post_processing(®ex_match); + post_processor.start = next_match.end().add(term, Boundary::Grid, 1); + post_processor.next_match = Some(next_match); + + post_processor + } + + /// Apply some hint post processing heuristics. + /// + /// This will check the end of the hint and make it shorter if certain characters are determined + /// to be unlikely to be intentionally part of the hint. + /// + /// This is most useful for identifying URLs appropriately. + fn hint_post_processing(&self, regex_match: &Match) -> Match { + let mut iter = self.term.grid().iter_from(*regex_match.start()); + + let mut c = iter.cell().c; + + // Truncate uneven number of brackets. + let end = *regex_match.end(); + let mut open_parents = 0; + let mut open_brackets = 0; + loop { + match c { + '(' => open_parents += 1, + '[' => open_brackets += 1, + ')' => { + if open_parents == 0 { + iter.prev(); + break; + } else { + open_parents -= 1; + } + }, + ']' => { + if open_brackets == 0 { + iter.prev(); + break; + } else { + open_brackets -= 1; + } + }, + _ => (), + } + + if iter.point() == end { + break; + } + + match iter.next() { + Some(indexed) => c = indexed.cell.c, + None => break, + } + } + + // Truncate trailing characters which are likely to be delimiters. + let start = *regex_match.start(); + while iter.point() != start { + if !matches!(c, '.' | ',' | ':' | ';' | '?' | '!' | '(' | '[' | '\'') { + break; + } + + match iter.prev() { + Some(indexed) => c = indexed.cell.c, + None => break, + } + } + + start..=iter.point() + } +} + +impl<'a, T> Iterator for HintPostProcessor<'a, T> { + type Item = Match; + + fn next(&mut self) -> Option<Self::Item> { + let next_match = self.next_match.take()?; + + if self.start <= self.end { + if let Some(rm) = self.term.regex_search_right(self.regex, self.start, self.end) { + let regex_match = self.hint_post_processing(&rm); + self.start = regex_match.end().add(self.term, Boundary::Grid, 1); + self.next_match = Some(regex_match); + } + } + + Some(next_match) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs index 2c74920d..f6b55e93 100644 --- a/alacritty/src/display/mod.rs +++ b/alacritty/src/display/mod.rs @@ -2,6 +2,7 @@ //! GPU drawing. use std::cmp::min; +use std::convert::TryFrom; use std::f64; use std::fmt::{self, Formatter}; #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] @@ -27,6 +28,7 @@ use alacritty_terminal::event::{EventListener, OnResize}; use alacritty_terminal::grid::Dimensions as _; use alacritty_terminal::index::{Column, Direction, Line, Point}; use alacritty_terminal::selection::Selection; +use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::{SizeInfo, Term, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES}; use crate::config::font::Font; @@ -38,14 +40,13 @@ use crate::display::bell::VisualBell; use crate::display::color::List; use crate::display::content::RenderableContent; use crate::display::cursor::IntoRects; -use crate::display::hint::HintState; +use crate::display::hint::{HintMatch, HintState}; use crate::display::meter::Meter; use crate::display::window::Window; use crate::event::{Mouse, SearchState}; use crate::message_bar::{MessageBuffer, MessageType}; use crate::renderer::rects::{RenderLines, RenderRect}; use crate::renderer::{self, GlyphCache, QuadRenderer}; -use crate::url::{Url, Urls}; pub mod content; pub mod cursor; @@ -58,7 +59,13 @@ mod meter; #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] mod wayland_theme; +/// Maximum number of linewraps followed outside of the viewport during search highlighting. +pub const MAX_SEARCH_LINES: usize = 100; + +/// Label for the forward terminal search bar. const FORWARD_SEARCH_LABEL: &str = "Search: "; + +/// Label for the backward terminal search bar. const BACKWARD_SEARCH_LABEL: &str = "Backward Search: "; #[derive(Debug)] @@ -164,10 +171,12 @@ impl DisplayUpdate { pub struct Display { pub size_info: SizeInfo, pub window: Window, - pub urls: Urls, - /// Currently highlighted URL. - pub highlighted_url: Option<Url>, + /// Hint highlighted by the mouse. + pub highlighted_hint: Option<HintMatch>, + + /// Hint highlighted by the vi mode cursor. + pub vi_highlighted_hint: Option<HintMatch>, #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] pub wayland_event_queue: Option<EventQueue>, @@ -331,8 +340,8 @@ impl Display { hint_state, meter: Meter::new(), size_info, - urls: Urls::new(), - highlighted_url: None, + highlighted_hint: None, + vi_highlighted_hint: None, #[cfg(not(any(target_os = "macos", windows)))] is_x11, #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] @@ -473,8 +482,6 @@ impl Display { mut terminal: MutexGuard<'_, Term<T>>, message_buffer: &MessageBuffer, config: &Config, - mouse: &Mouse, - mods: ModifiersState, search_state: &SearchState, ) { // Collect renderable content before the terminal is dropped. @@ -492,10 +499,6 @@ impl Display { let metrics = self.glyph_cache.font_metrics(); let size_info = self.size_info; - let selection = !terminal.selection.as_ref().map(Selection::is_empty).unwrap_or(true); - let mouse_mode = terminal.mode().intersects(TermMode::MOUSE_MODE) - && !terminal.mode().contains(TermMode::VI); - let vi_mode = terminal.mode().contains(TermMode::VI); let vi_mode_cursor = if vi_mode { Some(terminal.vi_mode_cursor) } else { None }; @@ -513,7 +516,6 @@ impl Display { } let mut lines = RenderLines::new(); - let mut urls = Urls::new(); let mut graphics_list = renderer::graphics::RenderList::default(); // Draw grid. @@ -521,11 +523,18 @@ impl Display { let _sampler = self.meter.sampler(); let glyph_cache = &mut self.glyph_cache; + let highlighted_hint = &self.highlighted_hint; + let vi_highlighted_hint = &self.vi_highlighted_hint; self.renderer.with_api(&config.ui_config, &size_info, |mut api| { // Iterate over all non-empty cells in the grid. - for cell in grid_cells { - // Update URL underlines. - urls.update(&size_info, &cell); + for mut cell in grid_cells { + // Underline hints hovered by mouse or vi mode cursor. + let point = viewport_to_point(display_offset, cell.point); + if highlighted_hint.as_ref().map_or(false, |h| h.bounds.contains(&point)) + || vi_highlighted_hint.as_ref().map_or(false, |h| h.bounds.contains(&point)) + { + cell.flags.insert(Flags::UNDERLINE); + } // Update underline/strikeout. lines.update(&cell); @@ -543,33 +552,9 @@ impl Display { let mut rects = lines.rects(&metrics, &size_info); - // Update visible URLs. - self.urls = urls; - if let Some(url) = self.urls.highlighted(config, mouse, mods, mouse_mode, selection) { - rects.append(&mut url.rects(&metrics, &size_info)); - - self.window.set_mouse_cursor(CursorIcon::Hand); - - self.highlighted_url = Some(url); - } else if self.highlighted_url.is_some() { - self.highlighted_url = None; - - if mouse_mode { - self.window.set_mouse_cursor(CursorIcon::Default); - } else { - self.window.set_mouse_cursor(CursorIcon::Text); - } - } - if let Some(vi_mode_cursor) = vi_mode_cursor { - // Highlight URLs at the vi mode cursor position. - let vi_point = vi_mode_cursor.point; - let line = (vi_point.line + display_offset).0 as usize; - if let Some(url) = self.urls.find_at(Point::new(line, vi_point.column)) { - rects.append(&mut url.rects(&metrics, &size_info)); - } - // Indicate vi mode by showing the cursor's position in the top right corner. + let vi_point = vi_mode_cursor.point; let line = (-vi_point.line.0 + size_info.bottommost_line().0) as usize; self.draw_line_indicator(config, &size_info, total_lines, Some(vi_point), line); } else if search_state.regex().is_some() { @@ -683,6 +668,47 @@ impl Display { self.colors = List::from(&config.ui_config.colors); } + /// Update the mouse/vi mode cursor hint highlighting. + pub fn update_highlighted_hints<T>( + &mut self, + term: &Term<T>, + config: &Config, + mouse: &Mouse, + modifiers: ModifiersState, + ) { + // Update vi mode cursor hint. + if term.mode().contains(TermMode::VI) { + let mods = ModifiersState::all(); + let point = term.vi_mode_cursor.point; + self.vi_highlighted_hint = hint::highlighted_at(&term, config, point, mods); + } else { + self.vi_highlighted_hint = None; + } + + // Abort if mouse highlighting conditions are not met. + if !mouse.inside_text_area || !term.selection.as_ref().map_or(true, Selection::is_empty) { + self.highlighted_hint = None; + return; + } + + // Find highlighted hint at mouse position. + let point = mouse.point(&self.size_info, term.grid().display_offset()); + let highlighted_hint = hint::highlighted_at(&term, config, point, modifiers); + + // Update cursor shape. + if highlighted_hint.is_some() { + self.window.set_mouse_cursor(CursorIcon::Hand); + } else if self.highlighted_hint.is_some() { + if term.mode().intersects(TermMode::MOUSE_MODE) && !term.mode().contains(TermMode::VI) { + self.window.set_mouse_cursor(CursorIcon::Default); + } else { + self.window.set_mouse_cursor(CursorIcon::Text); + } + } + + self.highlighted_hint = highlighted_hint; + } + /// Format search regex to account for the cursor and fullwidth characters. fn format_search(size_info: &SizeInfo, search_regex: &str, search_label: &str) -> String { // Add spacers for wide chars. @@ -794,6 +820,18 @@ impl Display { } } +/// Convert a terminal point to a viewport relative point. +pub fn point_to_viewport(display_offset: usize, point: Point) -> Option<Point<usize>> { + let viewport_line = point.line.0 + display_offset as i32; + usize::try_from(viewport_line).ok().map(|line| Point::new(line, point.column)) +} + +/// Convert a viewport relative point to a terminal point. +pub fn viewport_to_point(display_offset: usize, point: Point<usize>) -> Point { + let line = Line(point.line as i32) - display_offset; + Point::new(line, point.column) +} + /// Calculate the cell dimensions based on font metrics. /// /// This will return a tuple of the cell width and height. diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 341f398a..a895514f 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -44,15 +44,14 @@ use crate::clipboard::Clipboard; use crate::config::ui_config::{HintAction, HintInternalAction}; use crate::config::{self, Config}; use crate::daemon::start_daemon; -use crate::display::hint::{HintMatch, HintState}; +use crate::display::hint::HintMatch; use crate::display::window::Window; -use crate::display::{Display, DisplayUpdate}; +use crate::display::{self, Display, DisplayUpdate}; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; #[cfg(target_os = "macos")] use crate::macos; use crate::message_bar::{Message, MessageBuffer}; use crate::scheduler::{Scheduler, TimerId}; -use crate::url::{Url, Urls}; /// Duration after the last user input until an unlimited search is performed. pub const TYPING_SEARCH_DELAY: Duration = Duration::from_millis(500); @@ -207,17 +206,17 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon // Update selection. if self.terminal.mode().contains(TermMode::VI) - && self.terminal.selection.as_ref().map(|s| s.is_empty()) != Some(true) + && self.terminal.selection.as_ref().map_or(true, |s| !s.is_empty()) { self.update_selection(self.terminal.vi_mode_cursor.point, Side::Right); - } else if self.mouse().left_button_state == ElementState::Pressed - || self.mouse().right_button_state == ElementState::Pressed + } else if self.mouse.left_button_state == ElementState::Pressed + || self.mouse.right_button_state == ElementState::Pressed { - let point = self.mouse().point; - let line = Line(point.line as i32) - self.terminal.grid().display_offset(); - let point = Point::new(line, point.column); - self.update_selection(point, self.mouse().cell_side); + let display_offset = self.terminal.grid().display_offset(); + let point = self.mouse.point(&self.size_info(), display_offset); + self.update_selection(point, self.mouse.cell_side); } + self.copy_selection(ClipboardType::Selection); *self.dirty = true; } @@ -264,8 +263,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon self.terminal.selection = Some(selection); *self.dirty = true; - - self.copy_selection(ClipboardType::Selection); } fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side) { @@ -322,13 +319,13 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } #[inline] - fn window(&self) -> &Window { - &self.display.window + fn window(&mut self) -> &mut Window { + &mut self.display.window } #[inline] - fn window_mut(&mut self) -> &mut Window { - &mut self.display.window + fn display(&mut self) -> &mut Display { + &mut self.display } #[inline] @@ -385,30 +382,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon start_daemon(&alacritty, &args); } - /// Spawn URL launcher when clicking on URLs. - fn launch_url(&self, url: Url) { - if self.mouse.block_url_launcher { - return; - } - - if let Some(ref launcher) = self.config.ui_config.mouse.url.launcher { - let display_offset = self.terminal.grid().display_offset(); - let start = url.start(); - let start = Point::new(Line(start.line as i32 - display_offset as i32), start.column); - let end = url.end(); - let end = Point::new(Line(end.line as i32 - display_offset as i32), end.column); - - let mut args = launcher.args().to_vec(); - args.push(self.terminal.bounds_to_string(start, end)); - - start_daemon(launcher.program(), &args); - } - } - - fn highlighted_url(&self) -> Option<&Url> { - self.display.highlighted_url.as_ref() - } - fn change_font_size(&mut self, delta: f32) { *self.font_size = max(*self.font_size + delta, Size::new(FONT_SIZE_STEP)); let font = self.config.ui_config.font.clone().with_size(*self.font_size); @@ -487,6 +460,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon let end = *focused_match.end(); self.start_selection(SelectionType::Simple, start, Side::Left); self.update_selection(end, Side::Right); + self.copy_selection(ClipboardType::Selection); } self.search_state.dfas = None; @@ -645,42 +619,53 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } } - fn hint_state(&mut self) -> &mut HintState { - &mut self.display.hint_state - } - /// Process a new character for keyboard hints. fn hint_input(&mut self, c: char) { - let action = self.display.hint_state.keyboard_input(self.terminal, c); + if let Some(hint) = self.display.hint_state.keyboard_input(self.terminal, c) { + self.mouse.block_hint_launcher = false; + self.trigger_hint(&hint); + } *self.dirty = true; + } - let HintMatch { action, bounds } = match action { - Some(action) => action, - None => return, - }; + /// Trigger a hint action. + fn trigger_hint(&mut self, hint: &HintMatch) { + if self.mouse.block_hint_launcher { + return; + } - match action { + match &hint.action { // Launch an external program. HintAction::Command(command) => { - let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); + let text = self.terminal.bounds_to_string(*hint.bounds.start(), *hint.bounds.end()); let mut args = command.args().to_vec(); args.push(text); start_daemon(command.program(), &args); }, // Copy the text to the clipboard. HintAction::Action(HintInternalAction::Copy) => { - let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); + let text = self.terminal.bounds_to_string(*hint.bounds.start(), *hint.bounds.end()); self.clipboard.store(ClipboardType::Clipboard, text); }, // Write the text to the PTY/search. HintAction::Action(HintInternalAction::Paste) => { - let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); + let text = self.terminal.bounds_to_string(*hint.bounds.start(), *hint.bounds.end()); self.paste(&text); }, // Select the text. HintAction::Action(HintInternalAction::Select) => { - self.start_selection(SelectionType::Simple, *bounds.start(), Side::Left); - self.update_selection(*bounds.end(), Side::Right); + self.start_selection(SelectionType::Simple, *hint.bounds.start(), Side::Left); + self.update_selection(*hint.bounds.end(), Side::Right); + self.copy_selection(ClipboardType::Selection); + }, + // Move the vi mode cursor. + HintAction::Action(HintInternalAction::MoveViModeCursor) => { + // Enter vi mode if we're not in it already. + if !self.terminal.mode().contains(TermMode::VI) { + self.terminal.toggle_vi_mode(); + } + + self.terminal.vi_goto_point(*hint.bounds.start()); }, } } @@ -731,10 +716,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon self.event_loop } - fn urls(&self) -> &Urls { - &self.display.urls - } - fn clipboard_mut(&mut self) -> &mut Clipboard { self.clipboard } @@ -908,9 +889,9 @@ pub struct Mouse { pub scroll_px: f64, pub cell_side: Side, pub lines_scrolled: f32, - pub block_url_launcher: bool, + pub block_hint_launcher: bool, + pub hint_highlight_dirty: bool, pub inside_text_area: bool, - pub point: Point<usize>, pub x: usize, pub y: usize, } @@ -925,17 +906,34 @@ impl Default for Mouse { right_button_state: ElementState::Released, click_state: ClickState::None, cell_side: Side::Left, - block_url_launcher: Default::default(), + hint_highlight_dirty: Default::default(), + block_hint_launcher: Default::default(), inside_text_area: Default::default(), lines_scrolled: Default::default(), scroll_px: Default::default(), - point: Default::default(), x: Default::default(), y: Default::default(), } } } +impl Mouse { + /// Convert mouse pixel coordinates to viewport point. + /// + /// If the coordinates are outside of the terminal grid, like positions inside the padding, the + /// coordinates will be clamped to the closest grid coordinates. + #[inline] + pub fn point(&self, size: &SizeInfo, display_offset: usize) -> Point { + let col = self.x.saturating_sub(size.padding_x() as usize) / (size.cell_width() as usize); + let col = min(Column(col), size.last_column()); + + let line = self.y.saturating_sub(size.padding_y() as usize) / (size.cell_height() as usize); + let line = min(line, size.bottommost_line().0 as usize); + + display::viewport_to_point(display_offset, Point::new(line, col)) + } +} + /// The event processor. /// /// Stores some state from received events and dispatches actions when they are @@ -1115,6 +1113,17 @@ impl<N: Notify + OnResize> Processor<N> { return; } + if self.dirty || self.mouse.hint_highlight_dirty { + self.display.update_highlighted_hints( + &terminal, + &self.config, + &self.mouse, + self.modifiers, + ); + self.mouse.hint_highlight_dirty = false; + self.dirty = true; + } + if self.dirty { self.dirty = false; @@ -1127,14 +1136,7 @@ impl<N: Notify + OnResize> Processor<N> { } // Redraw screen. - self.display.draw( - terminal, - &self.message_buffer, - &self.config, - &self.mouse, - self.modifiers, - &self.search_state, - ); + self.display.draw(terminal, &self.message_buffer, &self.config, &self.search_state); } }); @@ -1165,7 +1167,7 @@ impl<N: Notify + OnResize> Processor<N> { // Resize to event's dimensions, since no resize event is emitted on Wayland. display_update_pending.set_dimensions(PhysicalSize::new(width, height)); - processor.ctx.window_mut().dpr = scale_factor; + processor.ctx.window().dpr = scale_factor; *processor.ctx.dirty = true; }, Event::Message(message) => { @@ -1184,7 +1186,7 @@ impl<N: Notify + OnResize> Processor<N> { TerminalEvent::Title(title) => { let ui_config = &processor.ctx.config.ui_config; if ui_config.window.dynamic_title { - processor.ctx.window_mut().set_title(&title); + processor.ctx.window().set_title(&title); } }, TerminalEvent::ResetTitle => { @@ -1198,7 +1200,7 @@ impl<N: Notify + OnResize> Processor<N> { // Set window urgency. if processor.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) { let focused = processor.ctx.terminal.is_focused; - processor.ctx.window_mut().set_urgent(!focused); + processor.ctx.window().set_urgent(!focused); } // Ring visual bell. @@ -1220,6 +1222,7 @@ impl<N: Notify + OnResize> Processor<N> { let text = format(processor.ctx.display.colors[index]); processor.ctx.write_to_pty(text.into_bytes()); }, + TerminalEvent::PtyWrite(text) => processor.ctx.write_to_pty(text.into_bytes()), TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(), TerminalEvent::Exit => (), TerminalEvent::CursorBlinkingChange(_) => { @@ -1251,16 +1254,16 @@ impl<N: Notify + OnResize> Processor<N> { }, WindowEvent::ReceivedCharacter(c) => processor.received_char(c), WindowEvent::MouseInput { state, button, .. } => { - processor.ctx.window_mut().set_mouse_visible(true); + processor.ctx.window().set_mouse_visible(true); processor.mouse_input(state, button); *processor.ctx.dirty = true; }, WindowEvent::CursorMoved { position, .. } => { - processor.ctx.window_mut().set_mouse_visible(true); + processor.ctx.window().set_mouse_visible(true); processor.mouse_moved(position); }, WindowEvent::MouseWheel { delta, phase, .. } => { - processor.ctx.window_mut().set_mouse_visible(true); + processor.ctx.window().set_mouse_visible(true); processor.mouse_wheel_input(delta, phase); }, WindowEvent::Focused(is_focused) => { @@ -1269,9 +1272,9 @@ impl<N: Notify + OnResize> Processor<N> { *processor.ctx.dirty = true; if is_focused { - processor.ctx.window_mut().set_urgent(false); + processor.ctx.window().set_urgent(false); } else { - processor.ctx.window_mut().set_mouse_visible(true); + processor.ctx.window().set_mouse_visible(true); } processor.ctx.update_cursor_blinking(); @@ -1285,7 +1288,7 @@ impl<N: Notify + OnResize> Processor<N> { WindowEvent::CursorLeft { .. } => { processor.ctx.mouse.inside_text_area = false; - if processor.ctx.highlighted_url().is_some() { + if processor.ctx.display().highlighted_hint.is_some() { *processor.ctx.dirty = true; } }, @@ -1382,12 +1385,12 @@ impl<N: Notify + OnResize> Processor<N> { if !config.ui_config.window.dynamic_title || processor.ctx.config.ui_config.window.title != config.ui_config.window.title { - processor.ctx.window_mut().set_title(&config.ui_config.window.title); + processor.ctx.window().set_title(&config.ui_config.window.title); } #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] if processor.ctx.event_loop.is_wayland() { - processor.ctx.window_mut().set_wayland_theme(&config.ui_config.colors); + processor.ctx.window().set_wayland_theme(&config.ui_config.colors); } // Set subpixel anti-aliasing. @@ -1396,7 +1399,7 @@ impl<N: Notify + OnResize> Processor<N> { // Disable shadows for transparent windows on macOS. #[cfg(target_os = "macos")] - processor.ctx.window_mut().set_has_shadow(config.ui_config.background_opacity() >= 1.0); + processor.ctx.window().set_has_shadow(config.ui_config.background_opacity() >= 1.0); // Update hint keys. processor.ctx.display.hint_state.update_alphabet(config.ui_config.hints.alphabet()); diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index a66511cf..3559b85e 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -22,7 +22,7 @@ use glutin::window::CursorIcon; use alacritty_terminal::ansi::{ClearMode, Handler}; use alacritty_terminal::event::EventListener; use alacritty_terminal::grid::{Dimensions, Scroll}; -use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side}; +use alacritty_terminal::index::{Boundary, Column, Direction, Point, Side}; use alacritty_terminal::selection::SelectionType; use alacritty_terminal::term::search::Match; use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode}; @@ -31,12 +31,12 @@ use alacritty_terminal::vi_mode::ViMotion; use crate::clipboard::Clipboard; use crate::config::{Action, BindingMode, Config, Key, SearchAction, ViAction}; use crate::daemon::start_daemon; -use crate::display::hint::HintState; +use crate::display::hint::HintMatch; use crate::display::window::Window; +use crate::display::Display; use crate::event::{ClickState, Event, Mouse, TYPING_SEARCH_DELAY}; use crate::message_bar::{self, Message}; use crate::scheduler::{Scheduler, TimerId}; -use crate::url::{Url, Urls}; /// Font size change interval. pub const FONT_SIZE_STEP: f32 = 0.5; @@ -75,8 +75,8 @@ pub trait ActionContext<T: EventListener> { fn suppress_chars(&mut self) -> &mut bool; fn modifiers(&mut self) -> &mut ModifiersState; fn scroll(&mut self, _scroll: Scroll) {} - fn window(&self) -> &Window; - fn window_mut(&mut self) -> &mut Window; + fn window(&mut self) -> &mut Window; + fn display(&mut self) -> &mut Display; fn terminal(&self) -> &Term<T>; fn terminal_mut(&mut self) -> &mut Term<T>; fn spawn_new_instance(&mut self) {} @@ -86,9 +86,6 @@ pub trait ActionContext<T: EventListener> { fn message(&self) -> Option<&Message>; fn config(&self) -> &Config; fn event_loop(&self) -> &EventLoopWindowTarget<Event>; - fn urls(&self) -> &Urls; - fn launch_url(&self, _url: Url) {} - fn highlighted_url(&self) -> Option<&Url>; fn mouse_mode(&self) -> bool; fn clipboard_mut(&mut self) -> &mut Clipboard; fn scheduler_mut(&mut self) -> &mut Scheduler; @@ -105,8 +102,8 @@ pub trait ActionContext<T: EventListener> { fn search_active(&self) -> bool; fn on_typing_start(&mut self) {} fn toggle_vi_mode(&mut self) {} - fn hint_state(&mut self) -> &mut HintState; fn hint_input(&mut self, _character: char) {} + fn trigger_hint(&mut self, _hint: &HintMatch) {} fn paste(&mut self, _text: &str) {} } @@ -142,7 +139,7 @@ impl<T: EventListener> Execute<T> for Action { }, Action::Command(program) => start_daemon(program.program(), program.args()), Action::Hint(hint) => { - ctx.hint_state().start(hint.clone()); + ctx.display().hint_state.start(hint.clone()); ctx.mark_dirty(); }, Action::ToggleViMode => ctx.toggle_vi_mode(), @@ -164,12 +161,12 @@ impl<T: EventListener> Execute<T> for Action { Self::toggle_selection(ctx, SelectionType::Semantic); }, Action::ViAction(ViAction::Open) => { - ctx.mouse_mut().block_url_launcher = false; - let vi_point = ctx.terminal().vi_mode_cursor.point; - let line = (vi_point.line + ctx.terminal().grid().display_offset()).0 as usize; - if let Some(url) = ctx.urls().find_at(Point::new(line, vi_point.column)) { - ctx.launch_url(url); + let hint = ctx.display().vi_highlighted_hint.take(); + if let Some(hint) = &hint { + ctx.mouse_mut().block_hint_launcher = false; + ctx.trigger_hint(hint); } + ctx.display().vi_highlighted_hint = hint; }, Action::ViAction(ViAction::SearchNext) => { let terminal = ctx.terminal(); @@ -250,11 +247,13 @@ impl<T: EventListener> Execute<T> for Action { let text = ctx.clipboard_mut().load(ClipboardType::Selection); ctx.paste(&text); }, - Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(), + Action::ToggleFullscreen => ctx.window().toggle_fullscreen(), #[cfg(target_os = "macos")] - Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(), + Action::ToggleSimpleFullscreen => ctx.window().toggle_simple_fullscreen(), #[cfg(target_os = "macos")] Action::Hide => ctx.event_loop().hide_application(), + #[cfg(target_os = "macos")] + Action::HideOtherApplications => ctx.event_loop().hide_other_applications(), #[cfg(not(target_os = "macos"))] Action::Hide => ctx.window().set_visible(false), Action::Minimize => ctx.window().set_minimized(true), @@ -325,25 +324,6 @@ impl<T: EventListener> Execute<T> for Action { } } -#[derive(Debug, Clone, PartialEq)] -pub enum MouseState { - Url(Url), - MessageBar, - MessageBarButton, - Mouse, - Text, -} - -impl From<MouseState> for CursorIcon { - fn from(mouse_state: MouseState) -> CursorIcon { - match mouse_state { - MouseState::Url(_) | MouseState::MessageBarButton => CursorIcon::Hand, - MouseState::Text => CursorIcon::Text, - _ => CursorIcon::Default, - } - } -} - impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { pub fn new(ctx: A) -> Self { Self { ctx, _phantom: Default::default() } @@ -361,22 +341,19 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { self.update_selection_scrolling(y); } + let display_offset = self.ctx.terminal().grid().display_offset(); + let old_point = self.ctx.mouse().point(&size_info, display_offset); + let x = min(max(x, 0), size_info.width() as i32 - 1) as usize; let y = min(max(y, 0), size_info.height() as i32 - 1) as usize; - self.ctx.mouse_mut().x = x; self.ctx.mouse_mut().y = y; let inside_text_area = size_info.contains_point(x, y); - let point = self.coords_to_point(x, y); let cell_side = self.cell_side(x); - let cell_changed = point != self.ctx.mouse().point; - - // Update mouse state and check for URL change. - let mouse_state = self.mouse_state(); - self.update_url_state(&mouse_state); - self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); + let point = self.ctx.mouse().point(&size_info, display_offset); + let cell_changed = old_point != point; // If the mouse hasn't changed cells, do nothing. if !cell_changed @@ -388,18 +365,21 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { self.ctx.mouse_mut().inside_text_area = inside_text_area; self.ctx.mouse_mut().cell_side = cell_side; - self.ctx.mouse_mut().point = point; + + // Update mouse state and check for URL change. + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); + + // Prompt hint highlight update. + self.ctx.mouse_mut().hint_highlight_dirty = true; // Don't launch URLs if mouse has moved. - self.ctx.mouse_mut().block_url_launcher = true; + self.ctx.mouse_mut().block_hint_launcher = true; if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) { - let line = Line(point.line as i32) - self.ctx.terminal().grid().display_offset(); - let point = Point::new(line, point.column); self.ctx.update_selection(point, cell_side); } else if cell_changed - && point.line < self.ctx.terminal().screen_lines() && self.ctx.terminal().mode().intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) { if lmb_pressed { @@ -414,23 +394,6 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } } - /// Convert window space pixels to terminal grid coordinates. - /// - /// If the coordinates are outside of the terminal grid, like positions inside the padding, the - /// coordinates will be clamped to the closest grid coordinates. - #[inline] - fn coords_to_point(&self, x: usize, y: usize) -> Point<usize> { - let size = self.ctx.size_info(); - - let column = x.saturating_sub(size.padding_x() as usize) / (size.cell_width() as usize); - let column = min(Column(column), size.last_column()); - - let line = y.saturating_sub(size.padding_y() as usize) / (size.cell_height() as usize); - let line = min(line, size.bottommost_line().0 as usize); - - Point::new(line, column) - } - /// Check which side of a cell an X coordinate lies on. fn cell_side(&self, x: usize) -> Side { let size_info = self.ctx.size_info(); @@ -453,13 +416,45 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } } - fn normal_mouse_report(&mut self, button: u8) { - let Point { line, column } = self.ctx.mouse().point; + fn mouse_report(&mut self, button: u8, state: ElementState) { + let display_offset = self.ctx.terminal().grid().display_offset(); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); + + // Assure the mouse point is not in the scrollback. + if point.line < 0 { + return; + } + + // Calculate modifiers value. + let mut mods = 0; + let modifiers = self.ctx.modifiers(); + if modifiers.shift() { + mods += 4; + } + if modifiers.alt() { + mods += 8; + } + if modifiers.ctrl() { + mods += 16; + } + + // Report mouse events. + if self.ctx.terminal().mode().contains(TermMode::SGR_MOUSE) { + self.sgr_mouse_report(point, button + mods, state); + } else if let ElementState::Released = state { + self.normal_mouse_report(point, 3 + mods); + } else { + self.normal_mouse_report(point, button + mods); + } + } + + fn normal_mouse_report(&mut self, point: Point, button: u8) { + let Point { line, column } = point; let utf8 = self.ctx.terminal().mode().contains(TermMode::UTF8_MOUSE); let max_point = if utf8 { 2015 } else { 223 }; - if line >= max_point || column >= Column(max_point) { + if line >= max_point || column >= max_point { return; } @@ -479,49 +474,24 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } if utf8 && line >= 95 { - msg.append(&mut mouse_pos_encode(line)); + msg.append(&mut mouse_pos_encode(line.0 as usize)); } else { - msg.push(32 + 1 + line as u8); + msg.push(32 + 1 + line.0 as u8); } self.ctx.write_to_pty(msg); } - fn sgr_mouse_report(&mut self, button: u8, state: ElementState) { - let Point { line, column } = self.ctx.mouse().point; + fn sgr_mouse_report(&mut self, point: Point, button: u8, state: ElementState) { let c = match state { ElementState::Pressed => 'M', ElementState::Released => 'm', }; - let msg = format!("\x1b[<{};{};{}{}", button, column + 1, line + 1, c); + let msg = format!("\x1b[<{};{};{}{}", button, point.column + 1, point.line + 1, c); self.ctx.write_to_pty(msg.into_bytes()); } - fn mouse_report(&mut self, button: u8, state: ElementState) { - // Calculate modifiers value. - let mut mods = 0; - let modifiers = self.ctx.modifiers(); - if modifiers.shift() { - mods += 4; - } - if modifiers.alt() { - mods += 8; - } - if modifiers.ctrl() { - mods += 16; - } - - // Report mouse events. - if self.ctx.terminal().mode().contains(TermMode::SGR_MOUSE) { - self.sgr_mouse_report(button + mods, state); - } else if let ElementState::Released = state { - self.normal_mouse_report(3 + mods); - } else { - self.normal_mouse_report(button + mods); - } - } - fn on_mouse_press(&mut self, button: MouseButton) { // Handle mouse mode. if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() { @@ -560,10 +530,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { }; // Load mouse point, treating message bar and padding as the closest cell. - let point = self.ctx.mouse().point; let display_offset = self.ctx.terminal().grid().display_offset(); - let absolute_line = Line(point.line as i32) - display_offset; - let point = Point::new(absolute_line, point.column); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); match button { MouseButton::Left => self.on_left_click(point), @@ -617,7 +585,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { match self.ctx.mouse().click_state { ClickState::Click => { // Don't launch URLs if this click cleared the selection. - self.ctx.mouse_mut().block_url_launcher = !self.ctx.selection_is_empty(); + self.ctx.mouse_mut().block_hint_launcher = !self.ctx.selection_is_empty(); self.ctx.clear_selection(); @@ -629,11 +597,11 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } }, ClickState::DoubleClick => { - self.ctx.mouse_mut().block_url_launcher = true; + self.ctx.mouse_mut().block_hint_launcher = true; self.ctx.start_selection(SelectionType::Semantic, point, side); }, ClickState::TripleClick => { - self.ctx.mouse_mut().block_url_launcher = true; + self.ctx.mouse_mut().block_hint_launcher = true; self.ctx.start_selection(SelectionType::Lines, point, side); }, ClickState::None => (), @@ -656,11 +624,19 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { }; self.mouse_report(code, ElementState::Released); return; - } else if let (MouseButton::Left, MouseState::Url(url)) = (button, self.mouse_state()) { - self.ctx.launch_url(url); } + // Trigger hints highlighted by the mouse. + let hint = self.ctx.display().highlighted_hint.take(); + if let Some(hint) = hint.as_ref().filter(|_| button == MouseButton::Left) { + self.ctx.trigger_hint(hint); + } + self.ctx.display().highlighted_hint = hint; + self.ctx.scheduler_mut().unschedule(TimerId::SelectionScrolling); + + // Copy selection on release, to prevent flooding the display server. + self.ctx.copy_selection(ClipboardType::Selection); } pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) { @@ -746,7 +722,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } // Skip normal mouse events if the message bar has been clicked. - if self.message_bar_mouse_state() == Some(MouseState::MessageBarButton) + if self.message_bar_cursor_state() == Some(CursorIcon::Hand) && state == ElementState::Pressed { let size = self.ctx.size_info(); @@ -771,7 +747,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { }, }; - self.ctx.window_mut().set_mouse_cursor(new_icon); + self.ctx.window().set_mouse_cursor(new_icon); } else { match state { ElementState::Pressed => { @@ -786,7 +762,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { /// Process key input. pub fn key_input(&mut self, input: KeyboardInput) { // All key bindings are disabled while a hint is being selected. - if self.ctx.hint_state().active() { + if self.ctx.display().hint_state.active() { *self.ctx.suppress_chars() = false; return; } @@ -811,17 +787,19 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { pub fn modifiers_input(&mut self, modifiers: ModifiersState) { *self.ctx.modifiers() = modifiers; + // Prompt hint highlight update. + self.ctx.mouse_mut().hint_highlight_dirty = true; + // Update mouse state and check for URL change. - let mouse_state = self.mouse_state(); - self.update_url_state(&mouse_state); - self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); } /// Reset mouse cursor based on modifier and terminal state. #[inline] pub fn reset_mouse_cursor(&mut self) { - let mouse_state = self.mouse_state(); - self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); } /// Process a received character. @@ -829,7 +807,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { let suppress_chars = *self.ctx.suppress_chars(); // Handle hint selection over anything else. - if self.ctx.hint_state().active() && !suppress_chars { + if self.ctx.display().hint_state.active() && !suppress_chars { self.ctx.hint_input(c); return; } @@ -923,8 +901,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } } - /// Check mouse state in relation to the message bar. - fn message_bar_mouse_state(&self) -> Option<MouseState> { + /// Check mouse icon state in relation to the message bar. + fn message_bar_cursor_state(&self) -> Option<CursorIcon> { // Since search is above the message bar, the button is offset by search's height. let search_height = if self.ctx.search_active() { 1 } else { 0 }; @@ -934,58 +912,36 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { + size.cell_height() as usize * (size.screen_lines() + search_height); let mouse = self.ctx.mouse(); + let display_offset = self.ctx.terminal().grid().display_offset(); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); + if self.ctx.message().is_none() || (mouse.y <= terminal_end) { None } else if mouse.y <= terminal_end + size.cell_height() as usize - && mouse.point.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.columns() + && point.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.columns() { - Some(MouseState::MessageBarButton) + Some(CursorIcon::Hand) } else { - Some(MouseState::MessageBar) - } - } - - /// Trigger redraw when URL highlight changed. - #[inline] - fn update_url_state(&mut self, mouse_state: &MouseState) { - let highlighted_url = self.ctx.highlighted_url(); - if let MouseState::Url(url) = mouse_state { - if Some(url) != highlighted_url { - self.ctx.mark_dirty(); - } - } else if highlighted_url.is_some() { - self.ctx.mark_dirty(); + Some(CursorIcon::Default) } } - /// Location of the mouse cursor. - fn mouse_state(&mut self) -> MouseState { - // Check message bar before URL to ignore URLs in the message bar. - if let Some(mouse_state) = self.message_bar_mouse_state() { - return mouse_state; - } + /// Icon state of the cursor. + fn cursor_state(&mut self) -> CursorIcon { + let display_offset = self.ctx.terminal().grid().display_offset(); + let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset); - let mouse_mode = self.ctx.mouse_mode(); + // Function to check if mouse is on top of a hint. + let hint_highlighted = |hint: &HintMatch| hint.bounds.contains(&point); - // Check for URL at mouse cursor. - let mods = *self.ctx.modifiers(); - let highlighted_url = self.ctx.urls().highlighted( - self.ctx.config(), - self.ctx.mouse(), - mods, - mouse_mode, - !self.ctx.selection_is_empty(), - ); - - if let Some(url) = highlighted_url { - return MouseState::Url(url); - } - - // Check mouse mode if location is not special. - if !self.ctx.modifiers().shift() && mouse_mode { - MouseState::Mouse + if let Some(mouse_state) = self.message_bar_cursor_state() { + mouse_state + } else if self.ctx.display().highlighted_hint.as_ref().map_or(false, hint_highlighted) { + CursorIcon::Hand + } else if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() { + CursorIcon::Default } else { - MouseState::Text + CursorIcon::Text } } @@ -1127,11 +1083,11 @@ mod tests { &mut self.modifiers } - fn window(&self) -> &Window { + fn window(&mut self) -> &mut Window { unimplemented!(); } - fn window_mut(&mut self) -> &mut Window { + fn display(&mut self) -> &mut Display { unimplemented!(); } @@ -1155,21 +1111,9 @@ mod tests { unimplemented!(); } - fn urls(&self) -> &Urls { - unimplemented!(); - } - - fn highlighted_url(&self) -> Option<&Url> { - unimplemented!(); - } - fn scheduler_mut(&mut self) -> &mut Scheduler { unimplemented!(); } - - fn hint_state(&mut self) -> &mut HintState { - unimplemented!(); - } } macro_rules! test_clickstate { diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs index 0914aee4..c68f3cfb 100644 --- a/alacritty/src/main.rs +++ b/alacritty/src/main.rs @@ -45,7 +45,6 @@ mod message_bar; mod panic; mod renderer; mod scheduler; -mod url; mod gl { #![allow(clippy::all)] diff --git a/alacritty_terminal/Cargo.toml b/alacritty_terminal/Cargo.toml index c29362e7..c6947009 100644 --- a/alacritty_terminal/Cargo.toml +++ b/alacritty_terminal/Cargo.toml @@ -28,7 +28,7 @@ regex-automata = "0.1.9" dirs = "3.0.1" [target.'cfg(unix)'.dependencies] -nix = "0.19.0" +nix = "0.20.0" signal-hook = { version = "0.1", features = ["mio-support"] } [target.'cfg(windows)'.dependencies] diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs index 27b9231e..14617de1 100644 --- a/alacritty_terminal/src/ansi.rs +++ b/alacritty_terminal/src/ansi.rs @@ -2,7 +2,7 @@ use std::convert::TryFrom; use std::time::{Duration, Instant}; -use std::{io, iter, str}; +use std::{iter, str}; use log::{debug, trace}; use serde::{Deserialize, Serialize}; @@ -157,29 +157,27 @@ impl Processor { /// Process a new byte from the PTY. #[inline] - pub fn advance<H, W>(&mut self, handler: &mut H, byte: u8, writer: &mut W) + pub fn advance<H>(&mut self, handler: &mut H, byte: u8) where H: Handler, - W: io::Write, { if self.state.sync_state.timeout.is_none() { - let mut performer = Performer::new(&mut self.state, handler, writer); + let mut performer = Performer::new(&mut self.state, handler); self.parser.advance(&mut performer, byte); } else { - self.advance_sync(handler, byte, writer); + self.advance_sync(handler, byte); } } /// End a synchronized update. - pub fn stop_sync<H, W>(&mut self, handler: &mut H, writer: &mut W) + pub fn stop_sync<H>(&mut self, handler: &mut H) where H: Handler, - W: io::Write, { // Process all synchronized bytes. for i in 0..self.state.sync_state.buffer.len() { let byte = self.state.sync_state.buffer[i]; - let mut performer = Performer::new(&mut self.state, handler, writer); + let mut performer = Performer::new(&mut self.state, handler); self.parser.advance(&mut performer, byte); } @@ -202,16 +200,15 @@ impl Processor { /// Process a new byte during a synchronized update. #[cold] - fn advance_sync<H, W>(&mut self, handler: &mut H, byte: u8, writer: &mut W) + fn advance_sync<H>(&mut self, handler: &mut H, byte: u8) where H: Handler, - W: io::Write, { self.state.sync_state.buffer.push(byte); // Handle sync DCS escape sequences. match self.state.sync_state.pending_dcs { - Some(_) => self.advance_sync_dcs_end(handler, byte, writer), + Some(_) => self.advance_sync_dcs_end(handler, byte), None => self.advance_sync_dcs_start(), } } @@ -232,10 +229,9 @@ impl Processor { } /// Parse the DCS termination sequence for synchronized updates. - fn advance_sync_dcs_end<H, W>(&mut self, handler: &mut H, byte: u8, writer: &mut W) + fn advance_sync_dcs_end<H>(&mut self, handler: &mut H, byte: u8) where H: Handler, - W: io::Write, { match byte { // Ignore DCS passthrough characters. @@ -247,7 +243,7 @@ impl Processor { Some(Dcs::SyncStart) => { self.state.sync_state.timeout = Some(Instant::now() + SYNC_UPDATE_TIMEOUT); }, - Some(Dcs::SyncEnd) => self.stop_sync(handler, writer), + Some(Dcs::SyncEnd) => self.stop_sync(handler), Some(Dcs::SixelData(_)) => (), None => (), }, @@ -259,21 +255,16 @@ impl Processor { /// /// Processor creates a Performer when running advance and passes the Performer /// to `vte::Parser`. -struct Performer<'a, H: Handler, W: io::Write> { +struct Performer<'a, H: Handler> { state: &'a mut ProcessorState, handler: &'a mut H, - writer: &'a mut W, } -impl<'a, H: Handler + 'a, W: io::Write> Performer<'a, H, W> { +impl<'a, H: Handler + 'a> Performer<'a, H> { /// Create a performer. #[inline] - pub fn new<'b>( - state: &'b mut ProcessorState, - handler: &'b mut H, - writer: &'b mut W, - ) -> Performer<'b, H, W> { - Performer { state, handler, writer } + pub fn new<'b>(state: &'b mut ProcessorState, handler: &'b mut H) -> Performer<'b, H> { + Performer { state, handler } } } @@ -313,10 +304,10 @@ pub trait Handler { fn move_down(&mut self, _: usize) {} /// Identify the terminal (should write back to the pty stream). - fn identify_terminal<W: io::Write>(&mut self, _: &mut W, _intermediate: Option<char>) {} + fn identify_terminal(&mut self, _intermediate: Option<char>) {} /// Report device status. - fn device_status<W: io::Write>(&mut self, _: &mut W, _: usize) {} + fn device_status(&mut self, _: usize) {} /// Move cursor forward `cols`. fn move_forward(&mut self, _: Column) {} @@ -466,13 +457,13 @@ pub trait Handler { fn pop_title(&mut self) {} /// Report text area size in pixels. - fn text_area_size_pixels<W: io::Write>(&mut self, _: &mut W) {} + fn text_area_size_pixels(&mut self) {} /// Report text area size in characters. - fn text_area_size_chars<W: io::Write>(&mut self, _: &mut W) {} + fn text_area_size_chars(&mut self) {} /// Report a graphics attribute. - fn graphics_attribute<W: io::Write>(&mut self, _: &mut W, _: u16, _: u16) {} + fn graphics_attribute(&mut self, _: u16, _: u16) {} /// Create a parser for Sixel data. fn start_sixel_graphic(&mut self, _params: &Params) -> Option<Box<sixel::Parser>> { @@ -905,10 +896,9 @@ impl StandardCharset { } } -impl<'a, H, W> vte::Perform for Performer<'a, H, W> +impl<'a, H> vte::Perform for Performer<'a, H> where H: Handler + 'a, - W: io::Write + 'a, { #[inline] fn print(&mut self, c: char) { @@ -1154,7 +1144,6 @@ where let mut params_iter = params.iter(); let handler = &mut self.handler; - let writer = &mut self.writer; let mut next_param_or = |default: u16| { params_iter.next().map(|param| param[0]).filter(|¶m| param != 0).unwrap_or(default) @@ -1175,7 +1164,7 @@ where }, ('C', []) | ('a', []) => handler.move_forward(Column(next_param_or(1) as usize)), ('c', intermediates) if next_param_or(0) == 0 => { - handler.identify_terminal(writer, intermediates.get(0).map(|&i| i as char)) + handler.identify_terminal(intermediates.get(0).map(|&i| i as char)) }, ('D', []) => handler.move_backward(Column(next_param_or(1) as usize)), ('d', []) => handler.goto_line(Line(next_param_or(1) as i32 - 1)), @@ -1257,7 +1246,7 @@ where } } }, - ('n', []) => handler.device_status(writer, next_param_or(0) as usize), + ('n', []) => handler.device_status(next_param_or(0) as usize), ('P', []) => handler.delete_chars(next_param_or(1) as usize), ('q', [b' ']) => { // DECSCUSR (CSI Ps SP q) -- Set Cursor Style. @@ -1285,12 +1274,12 @@ where handler.set_scrolling_region(top, bottom); }, ('S', []) => handler.scroll_up(next_param_or(1) as usize), - ('S', [b'?']) => handler.graphics_attribute(writer, next_param_or(0), next_param_or(0)), + ('S', [b'?']) => handler.graphics_attribute(next_param_or(0), next_param_or(0)), ('s', []) => handler.save_cursor_position(), ('T', []) => handler.scroll_down(next_param_or(1) as usize), ('t', []) => match next_param_or(1) as usize { - 14 => handler.text_area_size_pixels(writer), - 18 => handler.text_area_size_chars(writer), + 14 => handler.text_area_size_pixels(), + 18 => handler.text_area_size_chars(), 22 => handler.push_title(), 23 => handler.pop_title(), _ => unhandled!(), @@ -1338,7 +1327,7 @@ where }, (b'H', []) => self.handler.set_horizontal_tabstop(), (b'M', []) => self.handler.reverse_index(), - (b'Z', []) => self.handler.identify_terminal(self.writer, None), + (b'Z', []) => self.handler.identify_terminal(None), (b'c', []) => self.handler.reset_state(), (b'0', intermediates) => { configure_charset!(StandardCharset::SpecialCharacterAndLineDrawing, intermediates) @@ -1534,11 +1523,9 @@ pub mod C0 { // Byte sequences used in these tests are recording of pty stdout. #[cfg(test)] mod tests { - use super::{ - parse_number, xparse_color, Attr, CharsetIndex, Color, Handler, Processor, StandardCharset, - }; + use super::*; + use crate::term::color::Rgb; - use std::io; struct MockHandler { index: CharsetIndex, @@ -1561,7 +1548,7 @@ mod tests { self.index = index; } - fn identify_terminal<W: io::Write>(&mut self, _: &mut W, _intermediate: Option<char>) { + fn identify_terminal(&mut self, _intermediate: Option<char>) { self.identity_reported = true; } @@ -1589,7 +1576,7 @@ mod tests { let mut handler = MockHandler::default(); for byte in BYTES { - parser.advance(&mut handler, *byte, &mut io::sink()); + parser.advance(&mut handler, *byte); } assert_eq!(handler.attr, Some(Attr::Bold)); @@ -1603,7 +1590,7 @@ mod tests { let mut handler = MockHandler::default(); for byte in bytes { - parser.advance(&mut handler, *byte, &mut io::sink()); + parser.advance(&mut handler, *byte); } assert!(!handler.identity_reported); @@ -1612,7 +1599,7 @@ mod tests { let bytes: &[u8] = &[0x1b, b'[', b'c']; for byte in bytes { - parser.advance(&mut handler, *byte, &mut io::sink()); + parser.advance(&mut handler, *byte); } assert!(handler.identity_reported); @@ -1621,7 +1608,7 @@ mod tests { let bytes: &[u8] = &[0x1b, b'[', b'0', b'c']; for byte in bytes { - parser.advance(&mut handler, *byte, &mut io::sink()); + parser.advance(&mut handler, *byte); } assert!(handler.identity_reported); @@ -1635,7 +1622,7 @@ mod tests { let mut handler = MockHandler::default(); for byte in bytes { - parser.advance(&mut handler, *byte, &mut io::sink()); + parser.advance(&mut handler, *byte); } assert!(handler.identity_reported); @@ -1647,7 +1634,7 @@ mod tests { let mut handler = MockHandler::default(); for byte in bytes { - parser.advance(&mut handler, *byte, &mut io::sink()); + parser.advance(&mut handler, *byte); } assert!(!handler.identity_reported); @@ -1665,7 +1652,7 @@ mod tests { let mut handler = MockHandler::default(); for byte in BYTES { - parser.advance(&mut handler, *byte, &mut io::sink()); + parser.advance(&mut handler, *byte); } let spec = Rgb { r: 128, g: 66, b: 255 }; @@ -1696,7 +1683,7 @@ mod tests { let mut parser = Processor::new(); for byte in BYTES { - parser.advance(&mut handler, *byte, &mut io::sink()); + parser.advance(&mut handler, *byte); } } @@ -1707,7 +1694,7 @@ mod tests { let mut handler = MockHandler::default(); for byte in BYTES { - parser.advance(&mut handler, *byte, &mut io::sink()); + parser.advance(&mut handler, *byte); } assert_eq!(handler.index, CharsetIndex::G0); @@ -1721,14 +1708,14 @@ mod tests { let mut handler = MockHandler::default(); for byte in &BYTES[..3] { - parser.advance(&mut handler, *byte, &mut io::sink()); + parser.advance(&mut handler, *byte); } assert_eq!(handler.index, CharsetIndex::G1); assert_eq!(handler.charset, StandardCharset::SpecialCharacterAndLineDrawing); let mut handler = MockHandler::default(); - parser.advance(&mut handler, BYTES[3], &mut io::sink()); + parser.advance(&mut handler, BYTES[3]); assert_eq!(handler.index, CharsetIndex::G1); } diff --git a/alacritty_terminal/src/event.rs b/alacritty_terminal/src/event.rs index 70d16127..fac7a56a 100644 --- a/alacritty_terminal/src/event.rs +++ b/alacritty_terminal/src/event.rs @@ -35,6 +35,9 @@ pub enum Event { /// expected escape sequence format. ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>), + /// Write some text to the PTY. + PtyWrite(String), + /// Cursor blinking state has changed. CursorBlinkingChange(bool), @@ -57,6 +60,7 @@ impl Debug for Event { Event::ClipboardStore(ty, text) => write!(f, "ClipboardStore({:?}, {})", ty, text), Event::ClipboardLoad(ty, _) => write!(f, "ClipboardLoad({:?})", ty), Event::ColorRequest(index, _) => write!(f, "ColorRequest({})", index), + Event::PtyWrite(text) => write!(f, "PtyWrite({})", text), Event::Wakeup => write!(f, "Wakeup"), Event::Bell => write!(f, "Bell"), Event::Exit => write!(f, "Exit"), diff --git a/alacritty_terminal/src/event_loop.rs b/alacritty_terminal/src/event_loop.rs index c3224dfe..098ee896 100644 --- a/alacritty_terminal/src/event_loop.rs +++ b/alacritty_terminal/src/event_loop.rs @@ -240,7 +240,7 @@ where // Run the parser. for byte in &buf[..got] { - state.parser.advance(&mut **terminal, *byte, &mut self.pty.writer()); + state.parser.advance(&mut **terminal, *byte); } // Exit if we've processed enough bytes. @@ -334,7 +334,7 @@ where // Handle synchronized update timeout. if events.is_empty() { - state.parser.stop_sync(&mut *self.terminal.lock(), &mut self.pty.writer()); + state.parser.stop_sync(&mut *self.terminal.lock()); self.event_proxy.send_event(Event::Wakeup); continue; } diff --git a/alacritty_terminal/src/grid/resize.rs b/alacritty_terminal/src/grid/resize.rs index 10bc51f9..882c0c90 100644 --- a/alacritty_terminal/src/grid/resize.rs +++ b/alacritty_terminal/src/grid/resize.rs @@ -187,7 +187,7 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> { cursor_line_delta += line_delta.0 as usize; } else if row.is_clear() { - if i <= self.display_offset { + if i < self.display_offset { // Since we removed a line, rotate down the viewport. self.display_offset = self.display_offset.saturating_sub(1); } @@ -355,7 +355,7 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> { } row = Row::from_vec(wrapped, occ); - if i <= self.display_offset { + if i < self.display_offset { // Since we added a new line, rotate up the viewport. self.display_offset += 1; } diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index ef9071e7..5ca15b21 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -1,9 +1,10 @@ //! Exports the `Term` type which is a high-level API for the Grid. use std::cmp::{max, min}; +use std::fmt::Write; use std::ops::{Index, IndexMut, Range}; use std::sync::Arc; -use std::{io, mem, ptr, str}; +use std::{mem, ptr, str}; use bitflags::bitflags; use log::{debug, trace}; @@ -630,8 +631,15 @@ impl<T> Term<T> { self.mode ^= TermMode::VI; if self.mode.contains(TermMode::VI) { - // Reset vi mode cursor position to match primary cursor. - self.vi_mode_cursor = ViModeCursor::new(self.grid.cursor.point); + let display_offset = self.grid.display_offset() as i32; + if self.grid.cursor.point.line > self.bottommost_line() - display_offset { + // Move cursor to top-left if terminal cursor is not visible. + let point = Point::new(Line(-display_offset), Column(0)); + self.vi_mode_cursor = ViModeCursor::new(point); + } else { + // Reset vi mode cursor position to match primary cursor. + self.vi_mode_cursor = ViModeCursor::new(self.grid.cursor.point); + } } // Update UI about cursor blinking state changes. @@ -979,32 +987,35 @@ impl<T: EventListener> Handler for Term<T> { } #[inline] - fn identify_terminal<W: io::Write>(&mut self, writer: &mut W, intermediate: Option<char>) { + fn identify_terminal(&mut self, intermediate: Option<char>) { match intermediate { None => { trace!("Reporting primary device attributes"); - let _ = writer.write_all(b"\x1b[?6c"); + let text = String::from("\x1b[?6c"); + self.event_proxy.send_event(Event::PtyWrite(text)); }, Some('>') => { trace!("Reporting secondary device attributes"); let version = version_number(env!("CARGO_PKG_VERSION")); - let _ = writer.write_all(format!("\x1b[>0;{};1c", version).as_bytes()); + let text = format!("\x1b[>0;{};1c", version); + self.event_proxy.send_event(Event::PtyWrite(text)); }, _ => debug!("Unsupported device attributes intermediate"), } } #[inline] - fn device_status<W: io::Write>(&mut self, writer: &mut W, arg: usize) { + fn device_status(&mut self, arg: usize) { trace!("Reporting device status: {}", arg); match arg { 5 => { - let _ = writer.write_all(b"\x1b[0n"); + let text = String::from("\x1b[0n"); + self.event_proxy.send_event(Event::PtyWrite(text)); }, 6 => { let pos = self.grid.cursor.point; - let response = format!("\x1b[{};{}R", pos.line + 1, pos.column + 1); - let _ = writer.write_all(response.as_bytes()); + let text = format!("\x1b[{};{}R", pos.line + 1, pos.column + 1); + self.event_proxy.send_event(Event::PtyWrite(text)); }, _ => debug!("unknown device status query: {}", arg), }; @@ -1707,19 +1718,21 @@ impl<T: EventListener> Handler for Term<T> { } #[inline] - fn text_area_size_pixels<W: io::Write>(&mut self, writer: &mut W) { + fn text_area_size_pixels(&mut self) { let width = self.cell_width * self.columns(); let height = self.cell_height * self.screen_lines(); - let _ = write!(writer, "\x1b[4;{};{}t", height, width); + let text = format!("\x1b[4;{};{}t", height, width); + self.event_proxy.send_event(Event::PtyWrite(text)); } #[inline] - fn text_area_size_chars<W: io::Write>(&mut self, writer: &mut W) { - let _ = write!(writer, "\x1b[8;{};{}t", self.screen_lines(), self.columns()); + fn text_area_size_chars(&mut self) { + let text = format!("\x1b[8;{};{}t", self.screen_lines(), self.columns()); + self.event_proxy.send_event(Event::PtyWrite(text)); } #[inline] - fn graphics_attribute<W: io::Write>(&mut self, writer: &mut W, pi: u16, pa: u16) { + fn graphics_attribute(&mut self, pi: u16, pa: u16) { // From Xterm documentation: // // Pi = 1 -> item is number of color registers. @@ -1740,13 +1753,14 @@ impl<T: EventListener> Handler for Term<T> { (2, &[][..]) // Report error in Pa }; - let _ = write!(writer, "\x1b[?{};{}", pi, ps); + let mut text = format!("\x1b[?{};{}", pi, ps); for item in pv { - let _ = write!(writer, ";{}", item); + let _ = write!(&mut text, ";{}", item); } - let _ = write!(writer, "S"); + text.push('S'); + self.event_proxy.send_event(Event::PtyWrite(text)); } fn start_sixel_graphic(&mut self, params: &Params) -> Option<Box<sixel::Parser>> { diff --git a/alacritty_terminal/src/term/search.rs b/alacritty_terminal/src/term/search.rs index 638df670..93345e4f 100644 --- a/alacritty_terminal/src/term/search.rs +++ b/alacritty_terminal/src/term/search.rs @@ -82,7 +82,7 @@ impl<T> Term<T> { // Limit maximum number of lines searched. end = match max_lines { Some(max_lines) => { - let line = (start.line + max_lines).grid_clamp(self, Boundary::Grid); + let line = (start.line + max_lines).grid_clamp(self, Boundary::None); Point::new(line, self.last_column()) }, _ => end.sub(self, Boundary::None, 1), @@ -121,7 +121,7 @@ impl<T> Term<T> { // Limit maximum number of lines searched. end = match max_lines { Some(max_lines) => { - let line = (start.line - max_lines).grid_clamp(self, Boundary::Grid); + let line = (start.line - max_lines).grid_clamp(self, Boundary::None); Point::new(line, Column(0)) }, _ => end.add(self, Boundary::None, 1), diff --git a/alacritty_terminal/tests/ref.rs b/alacritty_terminal/tests/ref.rs index e229b6d2..a9968736 100644 --- a/alacritty_terminal/tests/ref.rs +++ b/alacritty_terminal/tests/ref.rs @@ -2,7 +2,7 @@ use serde::Deserialize; use serde_json as json; use std::fs::{self, File}; -use std::io::{self, Read}; +use std::io::Read; use std::path::Path; use alacritty_terminal::ansi; @@ -108,7 +108,7 @@ fn ref_test(dir: &Path) { let mut parser = ansi::Processor::new(); for byte in recording { - parser.advance(&mut terminal, byte, &mut io::sink()); + parser.advance(&mut terminal, byte); } // Truncate invisible lines from the grid. diff --git a/docs/features.md b/docs/features.md index fe93eb0e..094210fd 100644 --- a/docs/features.md +++ b/docs/features.md @@ -27,13 +27,6 @@ line (<kbd>Shift</kbd> <kbd>v</kbd>) and block selection (<kbd>Ctrl</kbd> <kbd>v</kbd>). You can also toggle between them while the selection is still active. -### Opening URLs - -While in vi mode you can open URLs using the <kbd>Enter</kbd> key. If some text -is recognized as a URL, it will be underlined once you move the vi cursor above -it. The program used to open these URLs can be changed in the [configuration -file]. - ## Search Search allows you to find anything in Alacritty's scrollback buffer. You can @@ -61,6 +54,11 @@ start vi mode. They consist of a regex that detects these text elements and then either feeds them to an external application or triggers one of Alacritty's built-in actions. +Hints can also be triggered using the mouse or vi mode cursor. If a hint is +enabled for mouse interaction and recognized as such, it will be underlined when +the mouse or vi mode cursor is on top of it. Using the left mouse button or +<kbd>Enter</kbd> key in vi mode will then trigger the hint. + Hints can be configured in the `hints` and `colors.hints` sections in the Alacritty configuration file. |