├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature_request.md └── workflows │ └── rust_build.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── PKGBUILD ├── README.md ├── build_shell_completions_and_man_pages.rs └── src ├── connection.rs ├── pager ├── cli.rs ├── context.rs ├── main.rs └── neovim.rs └── picker ├── cli.rs ├── context.rs └── main.rs /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Please add logs 4 | title: '' 5 | labels: '' 6 | assignees: I60R 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/rust_build.yml: -------------------------------------------------------------------------------- 1 | name: Rust Build 2 | 3 | on: 4 | 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | workflow_dispatch: 10 | 11 | env: 12 | 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | 17 | build: 18 | 19 | strategy: 20 | matrix: 21 | os: [ubuntu, macos, windows] 22 | 23 | runs-on: ${{ matrix.os }}-latest 24 | 25 | steps: 26 | - name: Checkout source code 27 | uses: actions/checkout@v3 28 | 29 | #- name: Install clippy from rust toolchain 30 | # uses: actions-rs/toolchain@v1 31 | # with: 32 | # toolchain: stable 33 | # default: true 34 | # profile: minimal # minimal component installation (ie, no documentation) 35 | # components: clippy 36 | # 37 | #- name: Run clippy 38 | # uses: actions-rs/cargo@v1 39 | # with: 40 | # command: clippy 41 | # args: --locked --all-targets --all-features 42 | 43 | - if: matrix.os == 'windows' 44 | name: Build on windows 45 | run: | 46 | cargo build --verbose --release 47 | mkdir binaries 48 | move target\release\page.exe binaries 49 | move target\release\nv.exe binaries 50 | 51 | - if: matrix.os != 'windows' 52 | name: Build on ${{ matrix.os }} 53 | run: | 54 | cargo build --verbose --release 55 | mkdir binaries 56 | mv target/release/page binaries 57 | mv target/release/nv binaries 58 | 59 | #- name: Run tests 60 | # run: cargo test --verbose 61 | 62 | - name: Upload binaries 63 | uses: actions/upload-artifact@v3 64 | with: 65 | name: binaries-${{ matrix.os }} 66 | path: binaries 67 | if-no-files-found: error 68 | retention-days: 7 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/b3ae3810f8b0f97f24cd61c2d3dd1b5089b91801/Global/JetBrains.gitignore 2 | 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | 27 | # Mongo Explorer plugin: 28 | .idea/**/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | 53 | 54 | ### https://raw.github.com/github/gitignore/b3ae3810f8b0f97f24cd61c2d3dd1b5089b91801/Global/Vim.gitignore 55 | 56 | # Swap 57 | [._]*.s[a-v][a-z] 58 | [._]*.sw[a-p] 59 | [._]s[a-v][a-z] 60 | [._]sw[a-p] 61 | 62 | # Session 63 | Session.vim 64 | 65 | # Temporary 66 | .netrwhist 67 | *~ 68 | # Auto-generated tag files 69 | tags 70 | 71 | 72 | ### https://raw.github.com/github/gitignore/b3ae3810f8b0f97f24cd61c2d3dd1b5089b91801/Rust.gitignore 73 | 74 | # Generated by Cargo 75 | # will have compiled files and executables 76 | /target/ 77 | 78 | # These are backup files generated by rustfmt 79 | **/*.rs.bk 80 | 81 | 82 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "async-trait" 7 | version = "0.1.60" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" 10 | dependencies = [ 11 | "proc-macro2", 12 | "quote", 13 | "syn", 14 | ] 15 | 16 | [[package]] 17 | name = "atty" 18 | version = "0.2.14" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 21 | dependencies = [ 22 | "hermit-abi 0.1.19", 23 | "libc", 24 | "winapi", 25 | ] 26 | 27 | [[package]] 28 | name = "autocfg" 29 | version = "1.1.0" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 32 | 33 | [[package]] 34 | name = "bitflags" 35 | version = "1.3.2" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 38 | 39 | [[package]] 40 | name = "byteorder" 41 | version = "1.4.3" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 44 | 45 | [[package]] 46 | name = "bytes" 47 | version = "0.4.12" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" 50 | dependencies = [ 51 | "byteorder", 52 | "iovec", 53 | ] 54 | 55 | [[package]] 56 | name = "bytes" 57 | version = "1.3.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" 60 | 61 | [[package]] 62 | name = "cc" 63 | version = "1.0.78" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" 66 | 67 | [[package]] 68 | name = "cfg-if" 69 | version = "1.0.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 72 | 73 | [[package]] 74 | name = "clap" 75 | version = "4.0.32" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" 78 | dependencies = [ 79 | "bitflags", 80 | "clap_derive", 81 | "clap_lex", 82 | "is-terminal", 83 | "once_cell", 84 | "strsim", 85 | "termcolor", 86 | "terminal_size", 87 | ] 88 | 89 | [[package]] 90 | name = "clap_complete" 91 | version = "4.0.7" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "10861370d2ba66b0f5989f83ebf35db6421713fd92351790e7fdd6c36774c56b" 94 | dependencies = [ 95 | "clap", 96 | ] 97 | 98 | [[package]] 99 | name = "clap_derive" 100 | version = "4.0.21" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" 103 | dependencies = [ 104 | "heck", 105 | "proc-macro-error", 106 | "proc-macro2", 107 | "quote", 108 | "syn", 109 | ] 110 | 111 | [[package]] 112 | name = "clap_lex" 113 | version = "0.3.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" 116 | dependencies = [ 117 | "os_str_bytes", 118 | ] 119 | 120 | [[package]] 121 | name = "clap_mangen" 122 | version = "0.2.6" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "904eb24d05ad587557e0f484ddce5c737c30cf81372badb16d13e41c4b8340b1" 125 | dependencies = [ 126 | "clap", 127 | "roff", 128 | ] 129 | 130 | [[package]] 131 | name = "errno" 132 | version = "0.2.8" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 135 | dependencies = [ 136 | "errno-dragonfly", 137 | "libc", 138 | "winapi", 139 | ] 140 | 141 | [[package]] 142 | name = "errno-dragonfly" 143 | version = "0.1.2" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 146 | dependencies = [ 147 | "cc", 148 | "libc", 149 | ] 150 | 151 | [[package]] 152 | name = "fern" 153 | version = "0.6.1" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "3bdd7b0849075e79ee9a1836df22c717d1eba30451796fdc631b04565dd11e2a" 156 | dependencies = [ 157 | "log", 158 | ] 159 | 160 | [[package]] 161 | name = "futures" 162 | version = "0.1.31" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" 165 | 166 | [[package]] 167 | name = "futures" 168 | version = "0.3.25" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" 171 | dependencies = [ 172 | "futures-channel", 173 | "futures-core", 174 | "futures-executor", 175 | "futures-io", 176 | "futures-sink", 177 | "futures-task", 178 | "futures-util", 179 | ] 180 | 181 | [[package]] 182 | name = "futures-channel" 183 | version = "0.3.25" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" 186 | dependencies = [ 187 | "futures-core", 188 | "futures-sink", 189 | ] 190 | 191 | [[package]] 192 | name = "futures-core" 193 | version = "0.3.25" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" 196 | 197 | [[package]] 198 | name = "futures-executor" 199 | version = "0.3.25" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" 202 | dependencies = [ 203 | "futures-core", 204 | "futures-task", 205 | "futures-util", 206 | ] 207 | 208 | [[package]] 209 | name = "futures-io" 210 | version = "0.3.25" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" 213 | 214 | [[package]] 215 | name = "futures-macro" 216 | version = "0.3.25" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" 219 | dependencies = [ 220 | "proc-macro2", 221 | "quote", 222 | "syn", 223 | ] 224 | 225 | [[package]] 226 | name = "futures-sink" 227 | version = "0.3.25" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" 230 | 231 | [[package]] 232 | name = "futures-task" 233 | version = "0.3.25" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" 236 | 237 | [[package]] 238 | name = "futures-util" 239 | version = "0.3.25" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" 242 | dependencies = [ 243 | "futures 0.1.31", 244 | "futures-channel", 245 | "futures-core", 246 | "futures-io", 247 | "futures-macro", 248 | "futures-sink", 249 | "futures-task", 250 | "memchr", 251 | "pin-project-lite", 252 | "pin-utils", 253 | "slab", 254 | "tokio-io", 255 | ] 256 | 257 | [[package]] 258 | name = "getrandom" 259 | version = "0.1.16" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 262 | dependencies = [ 263 | "cfg-if", 264 | "libc", 265 | "wasi 0.9.0+wasi-snapshot-preview1", 266 | ] 267 | 268 | [[package]] 269 | name = "heck" 270 | version = "0.4.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 273 | 274 | [[package]] 275 | name = "hermit-abi" 276 | version = "0.1.19" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 279 | dependencies = [ 280 | "libc", 281 | ] 282 | 283 | [[package]] 284 | name = "hermit-abi" 285 | version = "0.2.6" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 288 | dependencies = [ 289 | "libc", 290 | ] 291 | 292 | [[package]] 293 | name = "indoc" 294 | version = "1.0.8" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "da2d6f23ffea9d7e76c53eee25dfb67bcd8fde7f1198b0855350698c9f07c780" 297 | 298 | [[package]] 299 | name = "io-lifetimes" 300 | version = "1.0.3" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" 303 | dependencies = [ 304 | "libc", 305 | "windows-sys", 306 | ] 307 | 308 | [[package]] 309 | name = "iovec" 310 | version = "0.1.4" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" 313 | dependencies = [ 314 | "libc", 315 | ] 316 | 317 | [[package]] 318 | name = "is-terminal" 319 | version = "0.4.2" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" 322 | dependencies = [ 323 | "hermit-abi 0.2.6", 324 | "io-lifetimes", 325 | "rustix", 326 | "windows-sys", 327 | ] 328 | 329 | [[package]] 330 | name = "libc" 331 | version = "0.2.139" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 334 | 335 | [[package]] 336 | name = "linux-raw-sys" 337 | version = "0.1.4" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 340 | 341 | [[package]] 342 | name = "lock_api" 343 | version = "0.4.9" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 346 | dependencies = [ 347 | "autocfg", 348 | "scopeguard", 349 | ] 350 | 351 | [[package]] 352 | name = "log" 353 | version = "0.4.17" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 356 | dependencies = [ 357 | "cfg-if", 358 | ] 359 | 360 | [[package]] 361 | name = "memchr" 362 | version = "2.5.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 365 | 366 | [[package]] 367 | name = "mio" 368 | version = "0.8.5" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" 371 | dependencies = [ 372 | "libc", 373 | "log", 374 | "wasi 0.11.0+wasi-snapshot-preview1", 375 | "windows-sys", 376 | ] 377 | 378 | [[package]] 379 | name = "num-traits" 380 | version = "0.2.15" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 383 | dependencies = [ 384 | "autocfg", 385 | ] 386 | 387 | [[package]] 388 | name = "num_cpus" 389 | version = "1.15.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 392 | dependencies = [ 393 | "hermit-abi 0.2.6", 394 | "libc", 395 | ] 396 | 397 | [[package]] 398 | name = "nvim-rs" 399 | version = "0.5.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "1e98dcbd3b0ece3cf2b76ebc1e33e6511777ea7322884f4b7150cbc253afa37e" 402 | dependencies = [ 403 | "async-trait", 404 | "futures 0.3.25", 405 | "log", 406 | "parity-tokio-ipc", 407 | "rmp", 408 | "rmpv", 409 | "tokio", 410 | "tokio-util", 411 | ] 412 | 413 | [[package]] 414 | name = "once_cell" 415 | version = "1.17.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" 418 | 419 | [[package]] 420 | name = "os_str_bytes" 421 | version = "6.4.1" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 424 | 425 | [[package]] 426 | name = "page" 427 | version = "4.6.3" 428 | dependencies = [ 429 | "async-trait", 430 | "atty", 431 | "clap", 432 | "clap_complete", 433 | "clap_mangen", 434 | "fern", 435 | "futures 0.3.25", 436 | "indoc", 437 | "log", 438 | "nvim-rs", 439 | "once_cell", 440 | "parity-tokio-ipc", 441 | "shell-words", 442 | "term_size", 443 | "tokio", 444 | "tokio-util", 445 | "walkdir", 446 | ] 447 | 448 | [[package]] 449 | name = "parity-tokio-ipc" 450 | version = "0.9.0" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "9981e32fb75e004cc148f5fb70342f393830e0a4aa62e3cc93b50976218d42b6" 453 | dependencies = [ 454 | "futures 0.3.25", 455 | "libc", 456 | "log", 457 | "rand", 458 | "tokio", 459 | "winapi", 460 | ] 461 | 462 | [[package]] 463 | name = "parking_lot" 464 | version = "0.12.1" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 467 | dependencies = [ 468 | "lock_api", 469 | "parking_lot_core", 470 | ] 471 | 472 | [[package]] 473 | name = "parking_lot_core" 474 | version = "0.9.5" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" 477 | dependencies = [ 478 | "cfg-if", 479 | "libc", 480 | "redox_syscall", 481 | "smallvec", 482 | "windows-sys", 483 | ] 484 | 485 | [[package]] 486 | name = "paste" 487 | version = "1.0.11" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" 490 | 491 | [[package]] 492 | name = "pin-project-lite" 493 | version = "0.2.9" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 496 | 497 | [[package]] 498 | name = "pin-utils" 499 | version = "0.1.0" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 502 | 503 | [[package]] 504 | name = "ppv-lite86" 505 | version = "0.2.17" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 508 | 509 | [[package]] 510 | name = "proc-macro-error" 511 | version = "1.0.4" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 514 | dependencies = [ 515 | "proc-macro-error-attr", 516 | "proc-macro2", 517 | "quote", 518 | "syn", 519 | "version_check", 520 | ] 521 | 522 | [[package]] 523 | name = "proc-macro-error-attr" 524 | version = "1.0.4" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 527 | dependencies = [ 528 | "proc-macro2", 529 | "quote", 530 | "version_check", 531 | ] 532 | 533 | [[package]] 534 | name = "proc-macro2" 535 | version = "1.0.49" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" 538 | dependencies = [ 539 | "unicode-ident", 540 | ] 541 | 542 | [[package]] 543 | name = "quote" 544 | version = "1.0.23" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 547 | dependencies = [ 548 | "proc-macro2", 549 | ] 550 | 551 | [[package]] 552 | name = "rand" 553 | version = "0.7.3" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 556 | dependencies = [ 557 | "getrandom", 558 | "libc", 559 | "rand_chacha", 560 | "rand_core", 561 | "rand_hc", 562 | ] 563 | 564 | [[package]] 565 | name = "rand_chacha" 566 | version = "0.2.2" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 569 | dependencies = [ 570 | "ppv-lite86", 571 | "rand_core", 572 | ] 573 | 574 | [[package]] 575 | name = "rand_core" 576 | version = "0.5.1" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 579 | dependencies = [ 580 | "getrandom", 581 | ] 582 | 583 | [[package]] 584 | name = "rand_hc" 585 | version = "0.2.0" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 588 | dependencies = [ 589 | "rand_core", 590 | ] 591 | 592 | [[package]] 593 | name = "redox_syscall" 594 | version = "0.2.16" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 597 | dependencies = [ 598 | "bitflags", 599 | ] 600 | 601 | [[package]] 602 | name = "rmp" 603 | version = "0.8.11" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" 606 | dependencies = [ 607 | "byteorder", 608 | "num-traits", 609 | "paste", 610 | ] 611 | 612 | [[package]] 613 | name = "rmpv" 614 | version = "1.0.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "de8813b3a2f95c5138fe5925bfb8784175d88d6bff059ba8ce090aa891319754" 617 | dependencies = [ 618 | "num-traits", 619 | "rmp", 620 | ] 621 | 622 | [[package]] 623 | name = "roff" 624 | version = "0.2.1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" 627 | 628 | [[package]] 629 | name = "rustix" 630 | version = "0.36.6" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" 633 | dependencies = [ 634 | "bitflags", 635 | "errno", 636 | "io-lifetimes", 637 | "libc", 638 | "linux-raw-sys", 639 | "windows-sys", 640 | ] 641 | 642 | [[package]] 643 | name = "same-file" 644 | version = "1.0.6" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 647 | dependencies = [ 648 | "winapi-util", 649 | ] 650 | 651 | [[package]] 652 | name = "scopeguard" 653 | version = "1.1.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 656 | 657 | [[package]] 658 | name = "shell-words" 659 | version = "1.1.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 662 | 663 | [[package]] 664 | name = "signal-hook-registry" 665 | version = "1.4.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 668 | dependencies = [ 669 | "libc", 670 | ] 671 | 672 | [[package]] 673 | name = "slab" 674 | version = "0.4.7" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 677 | dependencies = [ 678 | "autocfg", 679 | ] 680 | 681 | [[package]] 682 | name = "smallvec" 683 | version = "1.10.0" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 686 | 687 | [[package]] 688 | name = "socket2" 689 | version = "0.4.7" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 692 | dependencies = [ 693 | "libc", 694 | "winapi", 695 | ] 696 | 697 | [[package]] 698 | name = "strsim" 699 | version = "0.10.0" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 702 | 703 | [[package]] 704 | name = "syn" 705 | version = "1.0.107" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 708 | dependencies = [ 709 | "proc-macro2", 710 | "quote", 711 | "unicode-ident", 712 | ] 713 | 714 | [[package]] 715 | name = "term_size" 716 | version = "0.3.2" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" 719 | dependencies = [ 720 | "libc", 721 | "winapi", 722 | ] 723 | 724 | [[package]] 725 | name = "termcolor" 726 | version = "1.1.3" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 729 | dependencies = [ 730 | "winapi-util", 731 | ] 732 | 733 | [[package]] 734 | name = "terminal_size" 735 | version = "0.2.3" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "cb20089a8ba2b69debd491f8d2d023761cbf196e999218c591fa1e7e15a21907" 738 | dependencies = [ 739 | "rustix", 740 | "windows-sys", 741 | ] 742 | 743 | [[package]] 744 | name = "tokio" 745 | version = "1.23.0" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" 748 | dependencies = [ 749 | "autocfg", 750 | "bytes 1.3.0", 751 | "libc", 752 | "memchr", 753 | "mio", 754 | "num_cpus", 755 | "parking_lot", 756 | "pin-project-lite", 757 | "signal-hook-registry", 758 | "socket2", 759 | "tokio-macros", 760 | "windows-sys", 761 | ] 762 | 763 | [[package]] 764 | name = "tokio-io" 765 | version = "0.1.13" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" 768 | dependencies = [ 769 | "bytes 0.4.12", 770 | "futures 0.1.31", 771 | "log", 772 | ] 773 | 774 | [[package]] 775 | name = "tokio-macros" 776 | version = "1.8.2" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" 779 | dependencies = [ 780 | "proc-macro2", 781 | "quote", 782 | "syn", 783 | ] 784 | 785 | [[package]] 786 | name = "tokio-util" 787 | version = "0.7.4" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" 790 | dependencies = [ 791 | "bytes 1.3.0", 792 | "futures-core", 793 | "futures-io", 794 | "futures-sink", 795 | "pin-project-lite", 796 | "tokio", 797 | ] 798 | 799 | [[package]] 800 | name = "unicode-ident" 801 | version = "1.0.6" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 804 | 805 | [[package]] 806 | name = "version_check" 807 | version = "0.9.4" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 810 | 811 | [[package]] 812 | name = "walkdir" 813 | version = "2.3.2" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 816 | dependencies = [ 817 | "same-file", 818 | "winapi", 819 | "winapi-util", 820 | ] 821 | 822 | [[package]] 823 | name = "wasi" 824 | version = "0.9.0+wasi-snapshot-preview1" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 827 | 828 | [[package]] 829 | name = "wasi" 830 | version = "0.11.0+wasi-snapshot-preview1" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 833 | 834 | [[package]] 835 | name = "winapi" 836 | version = "0.3.9" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 839 | dependencies = [ 840 | "winapi-i686-pc-windows-gnu", 841 | "winapi-x86_64-pc-windows-gnu", 842 | ] 843 | 844 | [[package]] 845 | name = "winapi-i686-pc-windows-gnu" 846 | version = "0.4.0" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 849 | 850 | [[package]] 851 | name = "winapi-util" 852 | version = "0.1.5" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 855 | dependencies = [ 856 | "winapi", 857 | ] 858 | 859 | [[package]] 860 | name = "winapi-x86_64-pc-windows-gnu" 861 | version = "0.4.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 864 | 865 | [[package]] 866 | name = "windows-sys" 867 | version = "0.42.0" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 870 | dependencies = [ 871 | "windows_aarch64_gnullvm", 872 | "windows_aarch64_msvc", 873 | "windows_i686_gnu", 874 | "windows_i686_msvc", 875 | "windows_x86_64_gnu", 876 | "windows_x86_64_gnullvm", 877 | "windows_x86_64_msvc", 878 | ] 879 | 880 | [[package]] 881 | name = "windows_aarch64_gnullvm" 882 | version = "0.42.0" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 885 | 886 | [[package]] 887 | name = "windows_aarch64_msvc" 888 | version = "0.42.0" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 891 | 892 | [[package]] 893 | name = "windows_i686_gnu" 894 | version = "0.42.0" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 897 | 898 | [[package]] 899 | name = "windows_i686_msvc" 900 | version = "0.42.0" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 903 | 904 | [[package]] 905 | name = "windows_x86_64_gnu" 906 | version = "0.42.0" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 909 | 910 | [[package]] 911 | name = "windows_x86_64_gnullvm" 912 | version = "0.42.0" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 915 | 916 | [[package]] 917 | name = "windows_x86_64_msvc" 918 | version = "0.42.0" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 921 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "page" 3 | version = "4.6.3" 4 | authors = ["160R <160R@protonmail.com>"] 5 | description = "Pager powered by neovim and inspired by neovim-remote" 6 | repository = "https://github.com/I60R/page" 7 | license = "MIT" 8 | readme = "README.md" 9 | edition = "2021" 10 | rust-version = "1.65.0" 11 | build = "build_shell_completions_and_man_pages.rs" 12 | 13 | 14 | [dependencies] 15 | term_size = { version = "0.3.2", optional = true } 16 | walkdir = { version = "2.3.2", optional = true } 17 | 18 | once_cell = "1.17.0" 19 | futures = "0.3.25" 20 | async-trait = "0.1.60" 21 | tokio = { version = "1.23.0", features = ["full"] } 22 | tokio-util = { version = "0.7.4", features = ["compat"] } 23 | parity-tokio-ipc = "0.9.0" 24 | nvim-rs = { version = "0.5.0", features = ["use_tokio"] } 25 | atty = "0.2.14" 26 | shell-words = "1.1.0" 27 | log = "0.4.17" 28 | fern = "0.6.1" 29 | indoc = "1.0.8" 30 | clap = { version = "4.0.32", features = ["wrap_help", "derive", "env"] } 31 | 32 | 33 | [build-dependencies] 34 | once_cell = "1.17.0" 35 | clap = { version = "4.0.32", features = ["derive", "env"] } 36 | clap_complete = "4.0.7" 37 | clap_mangen = "0.2.6" 38 | 39 | 40 | [profile.release] 41 | lto = true 42 | 43 | 44 | [features] 45 | default = ["pager", "picker"] 46 | 47 | pager = ["dep:term_size"] 48 | picker = ["dep:walkdir"] 49 | 50 | 51 | [lib] 52 | name = "connection" 53 | path = "src/connection.rs" 54 | 55 | 56 | [[bin]] 57 | name = "page" 58 | path = "src/pager/main.rs" 59 | required-features = ["pager"] 60 | 61 | [[bin]] 62 | name = "nv" 63 | path = "src/picker/main.rs" 64 | required-features = ["picker"] 65 | 66 | 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 160R 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | 23 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: 160R@protonmail.com 2 | _pkgname=page 3 | pkgname=${_pkgname}-git 4 | pkgrel=2 5 | pkgver=v4.6.3 6 | pkgdesc='Pager powered by neovim and inspired by neovim-remote' 7 | arch=('i686' 'x86_64') 8 | url="https://github.com/I60R/page" 9 | license=('MIT') 10 | depends=('neovim' 'gcc-libs' 'file' 'bat') 11 | makedepends=('rust' 'cargo' 'git') 12 | provides=('page') 13 | conflicts=('page') 14 | source=("git+https://github.com/I60R/page.git#branch=main") 15 | md5sums=('SKIP') 16 | 17 | 18 | pkgver() { 19 | checkout_project_root 20 | git describe --tags --abbrev=0 21 | } 22 | 23 | package() { 24 | checkout_project_root 25 | 26 | cargo build --release 27 | 28 | # Install binaries 29 | install -D -m755 "target/release/page" "$pkgdir/usr/bin/page" 30 | install -D -m755 "target/release/nv" "$pkgdir/usr/bin/nv" 31 | 32 | # Find last build directory where completions was generated 33 | out_dir=$(find "target" -name "assets" -type d -printf "%T+\t%p\n" | sort | awk 'NR==1{print $2}') 34 | 35 | # Install shell completions 36 | install -D -m644 "$out_dir/_page" "$pkgdir/usr/share/zsh/site-functions/_page" 37 | install -D -m644 "$out_dir/page.bash" "$pkgdir/usr/share/bash-completion/completions/page.bash" 38 | install -D -m644 "$out_dir/page.fish" "$pkgdir/usr/share/fish/completions/page.fish" 39 | 40 | install -D -m644 "$out_dir/_nv" "$pkgdir/usr/share/zsh/site-functions/_nv" 41 | install -D -m644 "$out_dir/nv.bash" "$pkgdir/usr/share/bash-completion/completions/nv.bash" 42 | install -D -m644 "$out_dir/nv.fish" "$pkgdir/usr/share/fish/completions/nv.fish" 43 | 44 | # Install man pages 45 | install -D -m644 "$out_dir/page.1" "$pkgdir/usr/share/man/man1/page.1" 46 | install -D -m644 "$out_dir/nv.1" "$pkgdir/usr/share/man/man1/nv.1" 47 | 48 | # Install MIT license 49 | install -D -m644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" 50 | } 51 | 52 | # Ensures that current directory is root of repository 53 | checkout_project_root() { 54 | cd "$srcdir" 55 | cd "$_pkgname" > /dev/null 2>&1 || cd .. 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Page 2 | 3 | [![Rust Build](https://github.com/I60R/page/actions/workflows/rust_build.yml/badge.svg)](https://github.com/I60R/page/actions/workflows/rust_build.yml) 4 | [![Lines Of Code](https://tokei.rs/b1/github/I60R/page)](https://github.com/I60R/page) 5 | 6 | Allows you to redirect text into [neovim](https://github.com/neovim/neovim). 7 | You can set it as `$PAGER` to view logs, diffs, various command outputs. 8 | 9 | ANSI escape sequences will be interpreted by :term buffer, which makes `page` noticeably faster than [vimpager](https://github.com/rkitover/vimpager) and [nvimpager](https://github.com/lucc/nvimpager). 10 | And text will be displayed instantly as it arrives - no need to wait until EOF. 11 | 12 | Also, text from neovim :term buffer will be redirected directly into a new buffer in the same neovim instance - no nested neovim will be spawned. 13 | That's by utilizing `$NVIM` variable like [neovim-remote](https://github.com/mhinz/neovim-remote) does. 14 | 15 | **Bonus**: another binary named `nv` is included, which reimplements `neovim-remote` but with interface similar to `page`. There's no intention to have all `nvim --remote` features — it should be only a simple file picker that prevents spawning nested neovim instance. Also, in contrast with `neovim-remote` there are some safeguards e.g. it won't open non-text files unless explicit flag is provided for that so `nv *` opens only text files in current directory. I recommend to read `--help` output and experiment with options a bit. 16 | 17 | Ultimately, `page` and `nv` reuses all of neovim's text editing+navigating+searching facilities and will either facilitate all of plugins+mappings+options set in your neovim config. 18 | 19 | ## Usage 20 | 21 | * *under regular terminal* 22 | 23 | ![usage under regular terminal](https://imgur.com/lxDCPpn.gif) 24 | 25 | * *under neovim's terminal* 26 | 27 | ![usage under neovim's terminal](https://i.imgur.com/rcLEM6X.gif) 28 | 29 | --- 30 | 31 | ## CLI 32 | 33 |
expand page --help 34 | 35 | ```xml 36 | Usage: page [OPTIONS] [FILE]... 37 | 38 | Arguments: 39 | [FILE]... Open provided file in separate buffer [without other flags revokes implied by default -o or -p 40 | option] 41 | 42 | Options: 43 | -o Create and use output buffer (to redirect text from page's stdin) [implied by 44 | default unless -x and/or provided without other flags] 45 | -O [] Prefetch from page's stdin: if all input fits then print it to 46 | stdout and exit without neovim usage (to emulate `less --quit-if-one-screen`) 47 | [empty: term height - 3 (space for prompt); negative: term height - 48 | ; 0: disabled and default; ignored with -o, -p, -x and when page 49 | isn't piped] 50 | -p Print path of pty device associated with output buffer (to redirect text from 51 | commands respecting output buffer size and preserving colors) [implied if page 52 | isn't piped unless -x and/or provided without other flags] 53 | -P Set $PWD as working directory at output buffer (to navigate paths with `gf`) 54 | -q [] Read no more than from page's stdin: next lines should be 55 | fetched by invoking :Page command or 'r'/'R' keypress on neovim side 56 | [empty: term height - 2 (space for tab and buffer lines); negative: term 57 | height - ; 0: disabled and default; is optional and 58 | defaults to ; doesn't take effect on buffers] 59 | -f Cursor follows content of output buffer as it appears instead of keeping top 60 | position (like `tail -f`) 61 | -F Cursor follows content of output and buffers as it appears instead of 62 | keeping top position 63 | -t Set filetype on output buffer (to enable syntax highlighting) [pager: default; 64 | not works with text echoed by -O] 65 | -b Return back to current buffer 66 | -B Return back to current buffer and enter into INSERT/TERMINAL mode 67 | -n Set title for output buffer (to display it in statusline) [env: 68 | PAGE_BUFFER_NAME=] 69 | -w Do not remap i, I, a, A, u, d, x, q (and r, R with -q) keys [wouldn't unmap on 70 | connected instance output buffer] 71 | -z [] Pagerize output when it exceeds lines (to view `journalctl`) 72 | [default: disabled; empty: 100_000] 73 | ~ ~ ~ 74 | 75 | ~ ~ ~ 76 | -a
TCP/IP socked address or path to named pipe listened by running host neovim 77 | process [env: NVIM=/run/user/1000/nvim.9389.0] 78 | -A Arguments that will be passed to child neovim process spawned when
79 | is missing [env: NVIM_PAGE_ARGS=] 80 | -c Config that will be used by child neovim process spawned when
is 81 | missing [file:$XDG_CONFIG_HOME/page/init.vim] 82 | -C Enable PageConnect PageDisconnect autocommands 83 | -e Run command on output buffer after it was created 84 | --e Run lua expr on output buffer after it was created 85 | -E Run command on output buffer after it was created or connected as instance 86 | --E Run lua expr on output buffer after it was created or connected as instance 87 | ~ ~ ~ 88 | -i Create output buffer with tag or use existed with replacing its 89 | content by text from page's stdin 90 | -I Create output buffer with tag or use existed with appending 91 | to its content text from page's stdin 92 | -x Close output buffer with tag if it exists [without other 93 | flags revokes implied by defalt -o or -p option] 94 | ~ ~ ~ 95 | -W Flush redirection protection that prevents from producing junk and possible 96 | overwriting of existed files by invoking commands like `ls > $(NVIM= page -E 97 | q)` where the RHS of > operator evaluates not into /path/to/pty as expected 98 | but into a bunch of whitespace-separated strings/escape sequences from neovim 99 | UI; bad things happens when some shells interpret this as many valid targets 100 | for text redirection. The protection is only printing of a path to the existed 101 | dummy directory always first before printing of a neovim UI might occur; this 102 | makes the first target for text redirection from page's output invalid and 103 | disrupts the whole redirection early before other harmful writes might occur. 104 | [env:PAGE_REDIRECTION_PROTECT; (0 to disable)] 105 | ~ ~ ~ 106 | -l... Split left with ratio: window_width * 3 / ( + 1) 107 | -r... Split right with ratio: window_width * 3 / ( + 1) 108 | -u... Split above with ratio: window_height * 3 / ( + 1) 109 | -d... Split below with ratio: window_height * 3 / ( + 1) 110 | -L Split left and resize to columns 111 | -R Split right and resize to columns 112 | -U Split above and resize to rows 113 | -D Split below and resize to rows 114 | ^ 115 | -+ With any of -r -l -u -d -R -L -U -D open floating window instead of split [to 116 | not overwrite data in the current terminal] 117 | ~ ~ ~ 118 | -h, --help Print help information 119 | ``` 120 | 121 |
122 | 123 |
expand nv --help 124 | 125 | ```xml 126 | Usage: nv [OPTIONS] [FILE]... 127 | 128 | Arguments: 129 | [FILE]... Open provided files as editable [if none provided nv opens last modified file in currend 130 | directory] 131 | 132 | Options: 133 | -o Open non-text files including directories, binaries, images etc 134 | -O [] Ignoring [FILE] open all text files in the current directory and recursively 135 | open all text files in its subdirectories [0: disabled and default; empty: 136 | defaults to 1 and implied if no provided; : 137 | also opens in subdirectories at this level of depth] 138 | -v Open in `page` instead (just postfix shortcut) 139 | ~ ~ ~ 140 | -f Open each [FILE] at last line 141 | -p Open and search for a specified 142 | -P Open and search backwars for a specified 143 | -b Return back to current buffer 144 | -B Return back to current buffer and enter into INSERT/TERMINAL mode 145 | -k Keep `nv` process until buffer is closed (for editing git commit message) 146 | -K Keep `nv` process until first write occur, then close buffer and neovim if 147 | it was spawned by `nv` 148 | ~ ~ ~ 149 | -a
TCP/IP socket address or path to named pipe listened by running host neovim 150 | process [env: NVIM=/run/user/1000/nvim.604327.0] 151 | -A Arguments that will be passed to child neovim process spawned when
152 | is missing [env: NVIM_PAGE_PICKER_ARGS=] 153 | -c Config that will be used by child neovim process spawned when
is 154 | missing [file: $XDG_CONFIG_HOME/page/init.vim] 155 | -t Override filetype on each [FILE] buffer (to enable custom syntax highlighting 156 | [text: default] 157 | ~ ~ ~ 158 | -e Run command on each [FILE] buffer after it was created 159 | --e Run lua expr on each [FILE] buffer after it was created 160 | -x Just run command with ignoring all other options 161 | --x Just run lua expr with ignoring all other options 162 | ~ ~ ~ 163 | -l... Split left with ratio: window_width * 3 / ( + 1) 164 | -r... Split right with ratio: window_width * 3 / ( + 1) 165 | -u... Split above with ratio: window_height * 3 / ( + 1) 166 | -d... Split below with ratio: window_height * 3 / ( + 1) 167 | -L Split left and resize to columns 168 | -R Split right and resize to columns 169 | -U Split above and resize to rows 170 | -D Split below and resize to rows 171 | ^ 172 | -+ With any of -r -l -u -d -R -L -U -D open floating window instead of split 173 | [to not overwrite data in the current terminal] 174 | ~ ~ ~ 175 | -h, --help Print help information 176 | ``` 177 | 178 |
179 | 180 | **Note**: `page` and `nv` may be unergonomic to type so I suggest users to create alias like `p` and `v` 181 | 182 | ## `nvim/init.lua` customizations 183 | 184 | ```lua 185 | -- Opacity of popup window spawned with -+ option 186 | vim.g.page_popup_winblend = 25 187 | ``` 188 | 189 | ## `nvim/init.lua` customizations (pager only) 190 | 191 | Statusline appearance: 192 | 193 | ```lua 194 | -- String that will append to buffer name 195 | vim.g.page_icon_pipe = '|' -- When piped 196 | vim.g.page_icon_redirect = '>' -- When exposes pty device 197 | vim.g.page_icon_instance = '$' -- When `-i, -I` flags provided 198 | ``` 199 | 200 | Autocommand hooks: 201 | 202 | ```lua 203 | -- Will run once when output buffer is created 204 | vim.api.create_autocmd('User', { 205 | pattern = 'PageOpen', 206 | callback = lua_function, 207 | }) 208 | 209 | -- Will run once when file buffer is created 210 | vim.api.create_autocmd('User', { 211 | pattern = 'PageOpenFile', 212 | callback = lua_function, 213 | }) 214 | ``` 215 | 216 | Only with `-C` option provided: 217 | 218 | ```lua 219 | -- will run always when output buffer is created 220 | -- and also when `page` connects to instance `-i, -I` buffers: 221 | vim.api.create_autocmd('User', { 222 | pattern = 'PageConnect', 223 | callback = lua_function, 224 | }) 225 | 226 | -- Will run when page process exits 227 | vim.api.create_autocmd('User', { 228 | pattern = 'PageDisconnect', 229 | callback = lua_function, 230 | }) 231 | ``` 232 | 233 | ## Shell hacks 234 | 235 | To use as `$PAGER` without [scrollback overflow](https://github.com/I60R/page/issues/7): 236 | 237 | ```zsh 238 | export PAGER="page -q 90000" 239 | 240 | # Alternatively 241 | 242 | export PAGER="page -z 90000" # will pagerize output 243 | 244 | # And you can combine both 245 | 246 | export PAGER="page -q 90000 -z 90000" 247 | ``` 248 | 249 | To configure: 250 | 251 | ```zsh 252 | export PAGER="page -WfC -q 90000 -z 90000" # some sensible flags 253 | alias page="$PAGER" 254 | 255 | # Usage 256 | ls | page -q 100 # you can specify the same flag multiple times: 257 | # last provided will override previous 258 | ``` 259 | 260 | To use as `$MANPAGER`: 261 | 262 | ```zsh 263 | export MANPAGER="page -t man" 264 | 265 | # Alternatively, to pick a bit better `man` highlighting: 266 | 267 | man () { 268 | PROGRAM="${@[-1]}" 269 | SECTION="${@[-2]}" 270 | page -W "man://$PROGRAM${SECTION:+($SECTION)}" 271 | } 272 | ``` 273 | 274 | To set `nv` as popup `git` commit message editor: 275 | 276 | ```zsh 277 | # Will spawn popup editor and exit on first write 278 | git config --global core.editor "nv -K -+-R 80 -B" 279 | ``` 280 | 281 | To cd into directory passed to `nv` 282 | 283 | ```zsh 284 | nv() { 285 | #stdin_is_term #one_argument #it's_dir 286 | if [ -t 1 ] && [ 1 -eq $# ] && [ -d $1 ]; then 287 | cd $1 288 | else 289 | nv $* 290 | fi 291 | } 292 | 293 | compdef _nv nv # if you have completions installed 294 | ``` 295 | 296 | To automatically `lcd` into terminal's directory: 297 | 298 | ```zsh 299 | chpwd () { 300 | [ ! -z "$NVIM" ] && nv -x "lcd $PWD" 301 | } 302 | ``` 303 | 304 | To circumvent neovim config picking: 305 | 306 | ```zsh 307 | page -c NONE 308 | 309 | # Alternatively, to override neovim config create this file: 310 | 311 | touch $XDG_CONFIG_HOME/page/init.lua # init.vim is also supported 312 | ``` 313 | 314 | To set output buffer name as first two words from invoked command (zsh only): 315 | 316 | ```zsh 317 | preexec () { 318 | if [ -z "$NVIM" ]; then 319 | export PAGE_BUFFER_NAME="page" 320 | else 321 | WORDS=(${1// *|*}) 322 | export PAGE_BUFFER_NAME="${WORDS[@]:0:2}" 323 | fi 324 | } 325 | ``` 326 | 327 | ## Buffer defaults (pager) 328 | 329 | 330 |
expand 331 | 332 | These commands are run on each `page` buffer creation: 333 | 334 | ```lua 335 | vim.b.page_alternate_bufnr = {$initial_buf_nr} 336 | if vim.wo.scrolloff > 999 or vim.wo.scrolloff < 0 then 337 | vim.g.page_scrolloff_backup = 0 338 | else 339 | vim.g.page_scrolloff_backup = vim.wo.scrolloff 340 | end 341 | vim.bo.scrollback, vim.wo.scrolloff, vim.wo.signcolumn, vim.wo.number = 342 | 100000, 999, 'no', false 343 | {$filetype} 344 | {$edit} 345 | vim.api.nvim_create_autocmd('BufEnter', { 346 | buffer = 0, 347 | callback = function() vim.wo.scrolloff = 999 end 348 | }) 349 | vim.api.nvim_create_autocmd('BufLeave', { 350 | buffer = 0, 351 | callback = function() vim.wo.scrolloff = vim.g.page_scrolloff_backup end 352 | }) 353 | {$notify_closed} 354 | {$pre} 355 | vim.cmd 'silent doautocmd User PageOpen | redraw' 356 | {$lua_provided_by_user} 357 | {$cmd_provided_by_user} 358 | {$after} 359 | ``` 360 | 361 | Where: 362 | 363 | ```lua 364 | --{$initial_buf_nr} 365 | -- Is always set on all buffers created by page 366 | 367 | 'number of parent :term buffer or -1 when page isn't spawned from :term' 368 | ``` 369 | 370 | ```lua 371 | --{$filetype} 372 | -- Is set only on output buffers. 373 | -- On files buffers filetypes are detected automatically. 374 | 375 | vim.bo.filetype='value of -t argument or "pager"' 376 | ``` 377 | 378 | ```lua 379 | --{$edit} 380 | -- Is appended when no -w option provided 381 | 382 | vim.bo.modifiable = false 383 | _G.page_echo_notification = function(message) 384 | vim.defer_fn(function() 385 | local msg = "-- [PAGE] " .. message .. " --" 386 | vim.api.nvim_echo({{ msg, 'Comment' }, }, false, {}) 387 | vim.cmd 'au CursorMoved ++once echo' 388 | end, 64) 389 | end 390 | _G.page_bound = function(top, message, move) 391 | local row, col, search 392 | if top then 393 | row, col, search = 1, 1, { '\\S', 'c' } 394 | else 395 | row, col, search = 9999999999, 9999999999, { '\\S', 'bc' } 396 | end 397 | vim.api.nvim_call_function('cursor', { row, col }) 398 | vim.api.nvim_call_function('search', search) 399 | if move ~= nil then move() end 400 | _G.page_echo_notification(message) 401 | end 402 | _G.page_scroll = function(top, message) 403 | vim.wo.scrolloff = 0 404 | local move 405 | if top then 406 | local key = vim.api.nvim_replace_termcodes('zM', true, false, true) 407 | move = function() vim.api.nvim_feedkeys(key, 'nx', true) end 408 | else 409 | move = function() vim.api.nvim_feedkeys('z-M', 'nx', false) end 410 | end 411 | _G.page_bound(top, message, move) 412 | vim.wo.scrolloff = 999 413 | end 414 | _G.page_close = function() 415 | local buf = vim.api.nvim_get_current_buf() 416 | if buf ~= vim.b.page_alternate_bufnr and 417 | vim.api.nvim_buf_is_loaded(vim.b.page_alternate_bufnr) 418 | then 419 | vim.api.nvim_set_current_buf(vim.b.page_alternate_bufnr) 420 | end 421 | vim.api.nvim_buf_delete(buf, { force = true }) 422 | local exit = true 423 | for _, b in ipairs(vim.api.nvim_list_bufs()) do 424 | local bt = vim.api.nvim_buf_get_option(b, 'buftype') 425 | if bt == "" or bt == "acwrite" or bt == "terminal" or bt == "prompt" then 426 | local bm = vim.api.nvim_buf_get_option(b, 'modified') 427 | if bm then 428 | exit = false 429 | break 430 | end 431 | local bl = vim.api.nvim_buf_get_lines(b, 0, -1, false) 432 | if #bl ~= 0 and bl[1] ~= "" and #bl > 1 then 433 | exit = false 434 | break 435 | end 436 | end 437 | end 438 | if exit then 439 | vim.cmd "qa!" 440 | end 441 | end 442 | local function page_map(key, expr) 443 | vim.api.nvim_buf_set_keymap(0, '', key, expr, { nowait = true }) 444 | end 445 | page_map('I', 'lua _G.page_scroll(true, "in the beginning of scroll")') 446 | page_map('A', 'lua _G.page_scroll(false, "at the end of scroll")') 447 | page_map('i', 'lua _G.page_bound(true, "in the beginning")') 448 | page_map('a', 'lua _G.page_bound(false, "at the end")') 449 | page_map('q', 'lua _G.page_close()') 450 | page_map('u', '') 451 | page_map('d', '') 452 | page_map('x', 'G') 453 | ``` 454 | 455 | ```lua 456 | --{$notify_closed} 457 | -- Is set only on output buffers 458 | 459 | local closed = 'rpcnotify({channel}, "page_buffer_closed", "{page_id}")' 460 | vim.api.nvim_create_autocmd('BufDelete', { 461 | buffer = 0, 462 | command = 'silent! call ' .. closed 463 | }) 464 | ``` 465 | 466 | ```lua 467 | --{$pre} 468 | -- Is appended when -q provided 469 | 470 | vim.b.page_query_size = {$query_lines_count} 471 | local def_args = '{channel}, "page_fetch_lines", "{page_id}", ' 472 | local def = 'command! -nargs=? Page call rpcnotify(' .. def_args .. ')' 473 | vim.cmd(def) 474 | vim.api.create_autocmd('BufEnter', { 475 | buffer = 0, 476 | command = def, 477 | }) 478 | 479 | -- Also if -q provided and no -w provided 480 | 481 | page_map('r', 'call rpcnotify(' .. def_args .. 'b:page_query_size * v:count1)') 482 | page_map('R', 'call rpcnotify(' .. def_args .. '99999)') 483 | 484 | -- If -P provided ({pwd} is $PWD value) 485 | 486 | vim.b.page_lcd_backup = getcwd() 487 | vim.cmd 'lcd {pwd}' 488 | vim.api.nvim_create_autocmd('BufEnter', { 489 | buffer = 0, 490 | command = 'lcd {pwd}' 491 | }) 492 | vim.api.nvim_create_autocmd('BufLeave', { 493 | buffer = 0, 494 | command = 'exe "lcd" . b:page_lcd_backup' 495 | }) 496 | ``` 497 | 498 | ```lua 499 | --{$lua_provided_by_user} 500 | -- Is appended when --e provided 501 | 502 | 'value of --e flag' 503 | ``` 504 | 505 | ```lua 506 | --{$cmd_provided_by_user} 507 | -- Is appended when -e provided 508 | 509 | vim.cmd [====[{$command}]====] 510 | ``` 511 | 512 | ```lua 513 | --{$after} 514 | -- Is appended only on file buffers 515 | 516 | vim.api.nvim_exec_autocmds('User', { 517 | pattern = 'PageOpenFile', 518 | }) 519 | ``` 520 | 521 |
522 | 523 | ## Limitations (pager) 524 | 525 | * Only ~100000 lines can be displayed (that's neovim terminal limit) 526 | * No reflow: text that doesnt't fit into window will be lost on resize ([due to data structures inherited from vim](https://github.com/neovim/neovim/issues/2514#issuecomment-580035346)) 527 | 528 | ## Installation 529 | 530 | * From binaries 531 | * Grab binary for your platform from [releases](https://github.com/I60R/page/releases) (currently Linux and OSX are supported) 532 | 533 | * Arch Linux: 534 | * Package [page-git](https://aur.archlinux.org/packages/page-git/) is available on AUR 535 | * Or: `git clone git@github.com:I60R/page.git && cd page && makepkg -ef && sudo pacman -U page-git*.pkg.tar.xz` 536 | 537 | * Homebrew: 538 | * Package [page](https://formulae.brew.sh/formula/page) is available on [Homebrew](https://brew.sh/) 539 | 540 | * Manually: 541 | * Install `rustup` from your distribution package manager 542 | * Configure toolchain: `rustup install stable && rustup default stable` 543 | * `git clone git@github.com:I60R/page.git && cd page && cargo install --path .` 544 | -------------------------------------------------------------------------------- /build_shell_completions_and_man_pages.rs: -------------------------------------------------------------------------------- 1 | use clap_complete::shells::{Zsh, Bash, Fish}; 2 | use clap::CommandFactory; 3 | 4 | use std::{fs, path::{PathBuf, Path}}; 5 | 6 | 7 | #[cfg(feature = "pager")] 8 | #[allow(dead_code)] 9 | mod pager { 10 | include!("src/pager/cli.rs"); 11 | } 12 | 13 | #[cfg(feature = "picker")] 14 | #[allow(dead_code)] 15 | mod picker { 16 | include!("src/picker/cli.rs"); 17 | } 18 | 19 | 20 | fn main() -> Result<(), Box> { 21 | let out_dir = PathBuf::from( 22 | std::env::var("OUT_DIR") 23 | .unwrap() 24 | ).join("assets"); 25 | 26 | fs::create_dir_all(&out_dir)?; 27 | eprintln!("Assets would be generated in: {}", out_dir.display()); 28 | 29 | #[cfg(feature = "pager")] 30 | { 31 | let mut app = pager::Options::command(); 32 | clap_complete::generate_to(Zsh , &mut app, "page", &out_dir)?; 33 | clap_complete::generate_to(Bash, &mut app, "page", &out_dir)?; 34 | clap_complete::generate_to(Fish, &mut app, "page", &out_dir)?; 35 | 36 | let page_1 = Path::new(&out_dir) 37 | .join("page.1"); 38 | let mut page_1 = fs::File::create(page_1)?; 39 | let man = clap_mangen::Man::new(app); 40 | man.render_title(&mut page_1)?; 41 | man.render_description_section(&mut page_1)?; 42 | let mut options_section = vec![]; 43 | man.render_options_section(&mut options_section)?; 44 | let options_section = String::from_utf8(options_section)? 45 | .replace("{n} ~ ~ ~", "") 46 | .replace("{n} ^ ~ ~ ~", "") 47 | .replace("[", "【\\fB") 48 | .replace("]", "\\fR】") 49 | .replace("【\\fBFILE\\fR】", "[FILE]..") 50 | .replace("【\\fB\\fIFILE\\fR\\fR】", "[FILE].."); 51 | use std::io::Write; 52 | write!(page_1, "{options_section}")?; 53 | man.render_authors_section(&mut page_1)?; 54 | } 55 | 56 | #[cfg(feature = "picker")] 57 | { 58 | let mut app = picker::Options::command(); 59 | clap_complete::generate_to(Zsh , &mut app, "nv", &out_dir)?; 60 | clap_complete::generate_to(Bash, &mut app, "nv", &out_dir)?; 61 | clap_complete::generate_to(Fish, &mut app, "nv", &out_dir)?; 62 | 63 | let nv_1 = Path::new(&out_dir) 64 | .join("nv.1"); 65 | let mut nv_1 = fs::File::create(nv_1)?; 66 | let man = clap_mangen::Man::new(app); 67 | man.render_title(&mut nv_1)?; 68 | man.render_description_section(&mut nv_1)?; 69 | let mut options_section = vec![]; 70 | man.render_options_section(&mut options_section)?; 71 | let options_section = String::from_utf8(options_section)? 72 | .replace("{n} ~ ~ ~", "") 73 | .replace("{n} ^ ~ ~ ~", "") 74 | .replace("[", "【\\fB") 75 | .replace("]", "\\fR】") 76 | .replace("【\\fBFILE\\fR】", "[FILE]..") 77 | .replace("【\\fB\\fIFILE\\fR\\fR】", "[FILE].."); 78 | use std::io::Write; 79 | write!(nv_1, "{options_section}")?; 80 | man.render_authors_section(&mut nv_1)?; 81 | } 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /src/connection.rs: -------------------------------------------------------------------------------- 1 | pub use crate::{ 2 | io_handler::{ 3 | PipeOrSocketHandler, 4 | NotificationFromNeovim 5 | }, 6 | io_pipe_or_socket::{ 7 | PipeOrSocketWrite as IoWrite, 8 | PipeOrSocketRead as IoRead 9 | } 10 | }; 11 | pub use nvim_rs::{ 12 | neovim::Neovim, 13 | Buffer, 14 | Window, 15 | Value 16 | }; 17 | 18 | use tokio_util::compat::{ 19 | TokioAsyncReadCompatExt, 20 | TokioAsyncWriteCompatExt 21 | }; 22 | 23 | use std::{ 24 | path::Path, 25 | process::ExitStatus 26 | }; 27 | 28 | 29 | pub fn init_logger() { 30 | let exec_time = std::time::Instant::now(); 31 | 32 | let dispatch = fern::Dispatch::new().format(move |cb, msg, log_record| { 33 | let time = exec_time 34 | .elapsed() 35 | .as_micros(); 36 | 37 | let lvl = log_record.level(); 38 | let target = log_record.target(); 39 | 40 | let mut module = log_record 41 | .module_path() 42 | .unwrap_or_default(); 43 | let mut prep = " in "; 44 | if target == module { 45 | module = ""; 46 | prep = ""; 47 | }; 48 | 49 | const BOLD: &str = "\x1B[1m"; 50 | const UNDERL: &str = "\x1B[4m"; 51 | const GRAY: &str = "\x1B[0;90m"; 52 | const CLEAR: &str = "\x1B[0m"; 53 | 54 | let mut msg_color = GRAY; 55 | if module.starts_with("page") { 56 | msg_color = ""; 57 | }; 58 | 59 | cb.finish(format_args!( 60 | "{BOLD}{UNDERL}[ {time:010} | {lvl:5} | \ 61 | {target}{prep}{module} ]{CLEAR}\n{msg_color}{msg}{CLEAR}\n", 62 | )); 63 | }); 64 | 65 | let log_lvl_filter = std::str::FromStr::from_str( 66 | std::env::var("PAGE_LOG") 67 | .as_deref() 68 | .unwrap_or("warn") 69 | ).expect("Cannot parse $PAGE_LOG value"); 70 | 71 | dispatch 72 | .level(log_lvl_filter) 73 | .chain(std::io::stderr()) 74 | // .chain(fern::log_file("page.log").unwrap()) 75 | // .filter(|f| f.target() != "nvim_rs::neovim") 76 | .apply() 77 | .expect("Cannot initialize logger"); 78 | } 79 | 80 | 81 | // If neovim dies unexpectedly it messes the terminal 82 | // so terminal state must be cleaned 83 | pub fn init_panic_hook() { 84 | let default_panic_hook = std::panic::take_hook(); 85 | 86 | std::panic::set_hook(Box::new(move |panic_info| { 87 | let try_spawn_reset = std::process::Command::new("reset") 88 | .spawn() 89 | .and_then(|mut child| child.wait()); 90 | 91 | match try_spawn_reset { 92 | Ok(exit_code) if exit_code.success() => {} 93 | 94 | Ok(err_exit_code) => { 95 | log::error!( 96 | target: "termreset", 97 | "`reset` exited with status: {err_exit_code}" 98 | ); 99 | } 100 | Err(e) => { 101 | log::error!(target: "termreset", "`reset` failed: {e:?}"); 102 | } 103 | } 104 | 105 | default_panic_hook(panic_info); 106 | })); 107 | } 108 | 109 | 110 | /// This struct contains all neovim-related data which is 111 | /// required by page after connection with neovim is established 112 | pub struct NeovimConnection>> { 113 | pub nvim_proc: Option>>, 114 | pub nvim_actions: Apis, 115 | pub initial_buf_number: i64, 116 | pub channel: u64, 117 | pub initial_win_and_buf: (Window, Buffer), 118 | pub rx: tokio::sync::mpsc::Receiver, 119 | handle: tokio::task::JoinHandle>>, 120 | } 121 | 122 | /// Connects to parent neovim session or spawns 123 | /// a new neovim process and connects to it through socket. 124 | /// Replacement for `nvim_rs::Session::new_child()`, 125 | /// since it uses --embed flag and steals page stdin 126 | pub async fn open>>( 127 | tmp_dir: &Path, 128 | page_id: u128, 129 | nvim_listen_addr: &Option, 130 | config_path: &Option, 131 | custom_nvim_args: &Option, 132 | print_protection: bool, 133 | ) -> NeovimConnection { 134 | 135 | let (tx, rx) = tokio::sync::mpsc::channel(16); 136 | 137 | let handler = PipeOrSocketHandler { 138 | page_id: page_id.to_string(), 139 | tx 140 | }; 141 | 142 | let mut nvim_proc = None; 143 | 144 | let (nvim, handle) = match nvim_listen_addr.as_deref() { 145 | Some(nvim_listen_addr) 146 | if nvim_listen_addr.parse::() 147 | .is_ok() => 148 | { 149 | let tcp = tokio::net::TcpStream::connect(nvim_listen_addr) 150 | .await 151 | .expect("Cannot connect to neovim at TCP/IP address"); 152 | 153 | let (rx, tx) = tokio::io::split(tcp); 154 | let (rx, tx) = (IoRead::Tcp(rx.compat()), IoWrite::Tcp(tx.compat_write())); 155 | let (nvim, io) = Neovim::::new(rx, tx, handler); 156 | let io_handle = tokio::task::spawn(io); 157 | 158 | (nvim, io_handle) 159 | } 160 | 161 | Some(nvim_listen_addr) => { 162 | let ipc = parity_tokio_ipc::Endpoint::connect(nvim_listen_addr) 163 | .await 164 | .expect("Cannot connect to neovim at path"); 165 | 166 | let (rx, tx) = tokio::io::split(ipc); 167 | let (rx, tx) = (IoRead::Ipc(rx.compat()), IoWrite::Ipc(tx.compat_write())); 168 | let (nvim, io) = Neovim::::new(rx, tx, handler); 169 | let io_handle = tokio::task::spawn(io); 170 | 171 | (nvim, io_handle) 172 | } 173 | 174 | None => { 175 | let (nvim, io_handle, child) = create_new_neovim_process_ipc( 176 | tmp_dir, 177 | page_id, 178 | config_path, 179 | custom_nvim_args, 180 | print_protection, 181 | handler 182 | ) 183 | .await; 184 | nvim_proc = Some(child); 185 | 186 | (nvim, io_handle) 187 | } 188 | }; 189 | 190 | let channel = nvim 191 | .get_api_info() 192 | .await 193 | .expect("No API info") 194 | .get(0) 195 | .expect("No channel") 196 | .as_u64() 197 | .expect("Channel not a number"); 198 | 199 | let initial_win = nvim 200 | .get_current_win() 201 | .await 202 | .expect("Cannot get initial window"); 203 | 204 | let initial_buf = nvim 205 | .get_current_buf() 206 | .await 207 | .expect("Cannot get initial buffer"); 208 | 209 | let initial_buf_number = initial_buf 210 | .get_number() 211 | .await 212 | .expect("Cannot get initial buffer number"); 213 | 214 | NeovimConnection { 215 | nvim_proc, 216 | nvim_actions: From::from(nvim), 217 | initial_buf_number, 218 | channel, 219 | initial_win_and_buf: (initial_win, initial_buf), 220 | rx, 221 | handle 222 | } 223 | } 224 | 225 | 226 | /// Waits until child neovim closes. 227 | /// If no child neovim process spawned then it's safe to just exit from page 228 | pub async fn close_and_exit>>( 229 | nvim_connection: &mut NeovimConnection 230 | ) -> ! { 231 | log::trace!(target: "exit", "close and exit"); 232 | 233 | if let Some(ref mut process) = nvim_connection.nvim_proc { 234 | if !process.is_finished() { 235 | process 236 | .await 237 | .expect("Neovim process was spawned with error") 238 | .expect("Neovim process died unexpectedly"); 239 | } 240 | } 241 | 242 | nvim_connection.handle 243 | .abort(); 244 | 245 | log::logger() 246 | .flush(); 247 | 248 | std::process::exit(0) 249 | } 250 | 251 | 252 | /// Creates a new session using UNIX socket. 253 | /// Also prints protection from shell redirection 254 | /// that could cause some harm (see --help[-W]) 255 | async fn create_new_neovim_process_ipc( 256 | tmp_dir: &Path, 257 | page_id: u128, 258 | config: &Option, 259 | custom_args: &Option, 260 | print_protection: bool, 261 | handler: PipeOrSocketHandler 262 | ) -> ( 263 | Neovim, 264 | tokio::task::JoinHandle>>, 265 | tokio::task::JoinHandle> 266 | ) { 267 | if print_protection { 268 | print_redirect_protection(tmp_dir); 269 | } 270 | 271 | let nvim_listen_addr = tmp_dir 272 | .join(&format!("socket-{page_id}")); 273 | 274 | let mut nvim_proc = tokio::task::spawn({ 275 | let (config, custom_args, nvim_listen_addr) = ( 276 | config.clone(), 277 | custom_args.clone(), 278 | nvim_listen_addr.clone() 279 | ); 280 | async move { 281 | spawn_child_nvim_process( 282 | &config, 283 | &custom_args, 284 | &nvim_listen_addr 285 | ) 286 | } 287 | }); 288 | 289 | tokio::time::sleep(std::time::Duration::from_millis(128)).await; 290 | 291 | let mut i = 0; 292 | let e = loop { 293 | 294 | let connection = parity_tokio_ipc::Endpoint::connect(&nvim_listen_addr).await; 295 | match connection { 296 | Ok(ipc) => { 297 | log::trace!(target: "child neovim spawned", "attempts={i}"); 298 | 299 | let (rx, tx) = tokio::io::split(ipc); 300 | let (rx, tx) = (IoRead::Ipc(rx.compat()), IoWrite::Ipc(tx.compat_write())); 301 | let (neovim, io) = Neovim::::new(rx, tx, handler); 302 | let io_handle = tokio::task::spawn(io); 303 | 304 | return (neovim, io_handle, nvim_proc) 305 | } 306 | 307 | Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => { 308 | if i == 256 { 309 | break e 310 | } 311 | 312 | use std::task::Poll::{Ready, Pending}; 313 | let poll = futures::poll!(std::pin::Pin::new(&mut nvim_proc)); 314 | 315 | match poll { 316 | Ready(Err(join_e)) => { 317 | log::error!(target: "child neovim didn't start", "{join_e}"); 318 | 319 | break join_e.into() 320 | }, 321 | Ready(Ok(child)) => { 322 | log::error!(target: "child neovim finished", "{child:?}"); 323 | 324 | break e 325 | }, 326 | 327 | Pending => {}, 328 | } 329 | 330 | tokio::time::sleep(std::time::Duration::from_millis(16)).await; 331 | 332 | i += 1; 333 | } 334 | 335 | Err(e) => break e 336 | } 337 | }; 338 | 339 | panic!("Cannot connect to neovim: attempts={i}, address={nvim_listen_addr:?}, {e:?}"); 340 | } 341 | 342 | 343 | /// This is hack to prevent behavior (or bug) in some shells (see --help[-W]) 344 | fn print_redirect_protection(tmp_dir: &Path) { 345 | let d = tmp_dir 346 | .join("DO-NOT-REDIRECT-OUTSIDE-OF-NVIM-TERM(--help[-W])"); 347 | 348 | if let Err(e) = std::fs::create_dir_all(&d) { 349 | panic!("Cannot create protection directory '{}': {e:?}", d.display()) 350 | } 351 | 352 | println!("{}", d.to_string_lossy()); 353 | } 354 | 355 | /// Spawns child neovim process on top of page, 356 | /// which further will be connected to page with UNIX socket. 357 | /// In this way neovim UI is displayed properly on top of page, 358 | /// and page as well is able to handle its own input to redirect it 359 | /// unto proper target (which is impossible with methods provided by 360 | /// `neovim_lib`). Also custom neovim config will be picked 361 | /// if it exists on corresponding locations. 362 | fn spawn_child_nvim_process( 363 | config: &Option, 364 | custom_args: &Option, 365 | nvim_listen_addr: &Path 366 | ) -> Result { 367 | 368 | let nvim_args = { 369 | let mut a = String::new(); 370 | a += "--cmd 'set shortmess+=I' "; 371 | a += "--listen "; 372 | a += &nvim_listen_addr.to_string_lossy(); 373 | 374 | if let Some(config) = config 375 | .clone() 376 | .or_else(default_config_path) 377 | { 378 | a += " "; 379 | a += "-u "; 380 | a += &config; 381 | } 382 | 383 | if let Some(custom_args) = custom_args.as_ref() { 384 | a += " "; 385 | a += custom_args; 386 | } 387 | 388 | shell_words::split(&a) 389 | .expect("Cannot parse neovim arguments") 390 | }; 391 | 392 | log::trace!(target: "new neovim process", "Args: {nvim_args:?}"); 393 | 394 | let term = current_term(); 395 | 396 | std::process::Command::new("nvim") 397 | .args(&nvim_args) 398 | .stdin(term) 399 | .spawn() 400 | .expect("Cannot spawn a child neovim process") 401 | .wait() 402 | } 403 | 404 | 405 | fn current_term() -> std::fs::File { 406 | #[cfg(windows)] 407 | let dev = "CON:"; 408 | #[cfg(not(windows))] 409 | let dev = "/dev/tty"; 410 | 411 | std::fs::OpenOptions::new() 412 | .read(true) 413 | .open(dev) 414 | .expect("Cannot open current terminal device") 415 | } 416 | 417 | 418 | /// Returns path to custom neovim config if 419 | /// it's present in a corresponding locations 420 | fn default_config_path() -> Option { 421 | use std::path::PathBuf; 422 | 423 | let page_home = std::env::var("XDG_CONFIG_HOME") 424 | .map(|xdg_config_home| { 425 | PathBuf::from(xdg_config_home) 426 | .join("page") 427 | }); 428 | 429 | let page_home = page_home.or_else(|_| std::env::var("HOME") 430 | .map(|home| { 431 | PathBuf::from(home) 432 | .join(".config/page") 433 | })); 434 | 435 | log::trace!(target: "config", "directory is: {page_home:?}"); 436 | 437 | let Ok(page_home) = page_home else { 438 | return None; 439 | }; 440 | 441 | let init_lua = page_home 442 | .join("init.lua"); 443 | if init_lua.exists() { 444 | let p = init_lua.to_string_lossy().to_string(); 445 | log::trace!(target: "config", "use init.lua"); 446 | return Some(p) 447 | } 448 | 449 | let init_vim = page_home 450 | .join("init.vim"); 451 | if init_vim.exists() { 452 | let p = init_vim.to_string_lossy().to_string(); 453 | log::trace!(target: "config", "use init.vim"); 454 | return Some(p) 455 | } 456 | 457 | None 458 | } 459 | 460 | 461 | mod io_pipe_or_socket { 462 | use parity_tokio_ipc::Connection; 463 | use tokio::{ 464 | io::{ReadHalf, WriteHalf}, 465 | net::TcpStream 466 | }; 467 | use tokio_util::compat::Compat; 468 | use std::pin::Pin; 469 | 470 | pub enum PipeOrSocketRead { 471 | Ipc(Compat>), 472 | Tcp(Compat>), 473 | } 474 | 475 | pub enum PipeOrSocketWrite { 476 | Ipc(Compat>), 477 | Tcp(Compat>), 478 | } 479 | 480 | macro_rules! delegate { 481 | ($self:ident => $method:ident($($args:expr),*)) => { 482 | match $self.get_mut() { 483 | Self::Ipc(rw) => Pin::new(rw).$method($($args),*), 484 | Self::Tcp(rw) => Pin::new(rw).$method($($args),*), 485 | } 486 | }; 487 | } 488 | 489 | impl futures::AsyncRead for PipeOrSocketRead { 490 | fn poll_read( 491 | self: Pin<&mut Self>, 492 | cx: &mut std::task::Context<'_>, 493 | buf: &mut [u8] 494 | ) -> std::task::Poll> { 495 | delegate!(self => poll_read(cx, buf)) 496 | } 497 | } 498 | 499 | impl futures::AsyncWrite for PipeOrSocketWrite { 500 | fn poll_write( 501 | self: Pin<&mut Self>, 502 | cx: &mut std::task::Context<'_>, 503 | buf: &[u8] 504 | ) -> std::task::Poll> { 505 | delegate!(self => poll_write(cx, buf)) 506 | } 507 | 508 | 509 | fn poll_flush( 510 | self: Pin<&mut Self>, 511 | cx: &mut std::task::Context<'_> 512 | ) -> std::task::Poll> { 513 | delegate!(self => poll_flush(cx)) 514 | } 515 | 516 | 517 | fn poll_close( 518 | self: Pin<&mut Self>, 519 | cx: &mut std::task::Context<'_> 520 | ) -> std::task::Poll> { 521 | delegate!(self => poll_close(cx)) 522 | } 523 | } 524 | } 525 | 526 | 527 | mod io_handler { 528 | use super::{io_pipe_or_socket::PipeOrSocketWrite, Neovim, Value}; 529 | 530 | /// Receives and collects notifications from neovim side over IPC or TCP/IP 531 | #[derive(Clone)] 532 | pub struct PipeOrSocketHandler { 533 | pub tx: tokio::sync::mpsc::Sender, 534 | pub page_id: String, 535 | } 536 | 537 | #[async_trait::async_trait] 538 | impl nvim_rs::Handler for PipeOrSocketHandler { 539 | type Writer = PipeOrSocketWrite; 540 | 541 | async fn handle_request( 542 | &self, 543 | request: String, 544 | args: Vec, 545 | _: Neovim 546 | ) -> Result { 547 | log::warn!(target: "unhandled", "{request}: {args:?}"); 548 | 549 | Ok(Value::from(0)) 550 | } 551 | 552 | async fn handle_notify( 553 | &self, 554 | notification: String, 555 | args: Vec, 556 | _: Neovim 557 | ) { 558 | log::trace!(target: "notification", "{}: {:?} ", notification, args); 559 | 560 | let page_id = args 561 | .get(0) 562 | .and_then(Value::as_str); 563 | 564 | let same_page_id = page_id 565 | .map_or(false, |page_id| page_id == self.page_id); 566 | if !same_page_id { 567 | log::warn!(target: "invalid page id", "{page_id:?}"); 568 | 569 | return 570 | } 571 | 572 | let notification_from_neovim = match notification.as_str() { 573 | "page_fetch_lines" => { 574 | let count = args.get(1) 575 | .and_then(Value::as_u64); 576 | 577 | if let Some(lines_count) = count { 578 | NotificationFromNeovim::FetchLines(lines_count as usize) 579 | } else { 580 | NotificationFromNeovim::FetchPart 581 | } 582 | }, 583 | "page_buffer_closed" => { 584 | NotificationFromNeovim::BufferClosed 585 | }, 586 | 587 | unknown => { 588 | log::warn!(target: "unhandled notification", "{unknown}"); 589 | 590 | return 591 | } 592 | }; 593 | 594 | self.tx 595 | .send(notification_from_neovim) 596 | .await 597 | .expect("Cannot receive notification"); 598 | } 599 | } 600 | 601 | 602 | /// This enum represents all notifications 603 | /// that could be sent from page's commands on neovim side 604 | #[derive(Debug)] 605 | pub enum NotificationFromNeovim { 606 | FetchPart, 607 | FetchLines(usize), 608 | BufferClosed, 609 | } 610 | } 611 | -------------------------------------------------------------------------------- /src/pager/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{ 2 | Parser, 3 | ArgGroup, 4 | ArgAction, 5 | ValueHint, 6 | }; 7 | 8 | 9 | /// Pager for neovim inspired by neovim-remote 10 | #[derive(Parser, Debug)] 11 | #[clap( 12 | author, 13 | disable_help_subcommand = true, 14 | allow_negative_numbers = true, 15 | args_override_self = true, 16 | group = splits_arg_group(), 17 | group = back_arg_group(), 18 | group = follow_arg_group(), 19 | group = instance_use_arg_group(), 20 | )] 21 | pub struct Options { 22 | /// Set title for output buffer (to display it in statusline) 23 | #[clap(display_order=10, short='n', env="PAGE_BUFFER_NAME")] 24 | pub name: Option, 25 | 26 | /// TCP/IP socket address or path to named pipe listened 27 | /// by running host neovim process 28 | #[clap(display_order=100, short='a', env="NVIM")] 29 | pub address: Option, 30 | 31 | /// Arguments that will be passed to child neovim process 32 | /// spawned when
is missing 33 | #[clap(display_order=101, short='A', env="NVIM_PAGE_ARGS")] 34 | pub arguments: Option, 35 | 36 | /// Config that will be used by child neovim process spawned 37 | /// when
is missing [file: $XDG_CONFIG_HOME/page/init.vim] 38 | #[clap(display_order=102, short='c', value_hint=ValueHint::AnyPath)] 39 | pub config: Option, 40 | 41 | /// Run command on output buffer after it was created 42 | /// or connected as instance 43 | #[clap(display_order=106, short='E')] 44 | pub command_post: Option, 45 | 46 | /// Run lua expr on output buffer after it was created 47 | /// or connected as instance {n} 48 | /// ~ ~ ~ 49 | #[clap(display_order=107, long="E")] 50 | pub lua_post: Option, 51 | 52 | /// Create output buffer with tag or use existed 53 | /// with replacing its content by text from page's stdin 54 | #[clap(display_order=200, short='i')] 55 | pub instance: Option, 56 | 57 | /// Create output buffer with tag or use existed 58 | /// with appending to its content text from page's stdin 59 | #[clap(display_order=201, short='I')] 60 | pub instance_append: Option, 61 | 62 | /// Close output buffer with tag if it exists 63 | /// [without other flags revokes implied by defalt -o or -p option] {n} 64 | /// ~ ~ ~ 65 | #[clap(display_order=202, short='x')] 66 | pub instance_close: Option, 67 | 68 | /// Create and use output buffer (to redirect text from page's stdin) 69 | /// [implied by default unless -x and/or provided without 70 | /// other flags] 71 | #[clap(display_order=0, short='o')] 72 | pub output_open: bool, 73 | 74 | /// Print path of pty device associated with output buffer (to redirect 75 | /// text from commands respecting output buffer size and preserving colors) 76 | /// [implied if page isn't piped unless -x and/or provided without other flags] 77 | #[clap(display_order=2, short='p')] 78 | pub pty_path_print: bool, 79 | 80 | /// Cursor follows content of output buffer as it appears 81 | /// instead of keeping top position (like `tail -f`) 82 | #[clap(display_order=5, short='f')] 83 | pub follow: bool, 84 | 85 | /// Cursor follows content of output and buffers 86 | /// as it appears instead of keeping top position 87 | #[clap(display_order=6, short='F')] 88 | pub follow_all: bool, 89 | 90 | /// Return back to current buffer 91 | #[clap(display_order=8, short='b')] 92 | pub back: bool, 93 | 94 | /// Return back to current buffer and enter into INSERT/TERMINAL mode 95 | #[clap(display_order=9, short='B')] 96 | pub back_restore: bool, 97 | 98 | /// Enable PageConnect PageDisconnect autocommands 99 | #[clap(display_order=103, short='C')] 100 | pub command_auto: bool, 101 | 102 | /// Flush redirection protection that prevents from producing junk 103 | /// and possible overwriting of existed files by invoking commands like 104 | /// `ls > $(NVIM= page -E q)` where the RHS of > operator 105 | /// evaluates not into /path/to/pty as expected but into a bunch 106 | /// of whitespace-separated strings/escape sequences from neovim UI; 107 | /// bad things happens when some shells interpret this as many valid 108 | /// targets for text redirection. The protection is only printing of a path 109 | /// to the existed dummy directory always first before printing 110 | /// of a neovim UI might occur; this makes the first target for text 111 | /// redirection from page's output invalid and disrupts the 112 | /// whole redirection early before other harmful writes might occur. 113 | /// [env: PAGE_REDIRECTION_PROTECT; (0 to disable)] {n} 114 | /// ~ ~ ~ 115 | #[clap(display_order=800, short='W')] 116 | pub page_no_protect: bool, 117 | 118 | /// Pagerize output when it exceeds lines 119 | /// (to view `journalctl`) [default: disabled; empty: 90_000] {n} 120 | /// ~ ~ ~ 121 | #[clap(display_order=12, short='z')] 122 | pub pagerize: Option>, 123 | 124 | #[clap(long="pagerize-hidden", hide = true, number_of_values = 2)] 125 | pub pagerize_hidden: Option>, 126 | 127 | /// Open provided file in a separate buffer 128 | /// [without other flags revokes implied by default -o or -p option] 129 | #[clap(name="FILE", value_hint=ValueHint::AnyPath)] 130 | pub files: Vec, 131 | 132 | 133 | #[clap(flatten)] 134 | pub output: OutputOptions, 135 | 136 | 137 | #[clap(skip)] 138 | output_implied: once_cell::unsync::OnceCell, 139 | 140 | #[clap(skip)] 141 | output_split_implied: once_cell::unsync::OnceCell, 142 | } 143 | 144 | impl Options { 145 | pub fn is_output_implied(&self) -> bool { 146 | *self.output_implied.get_or_init(|| 147 | self.back || 148 | self.back_restore || 149 | self.follow || 150 | self.follow_all || 151 | self.output_open || 152 | self.pty_path_print || 153 | self.instance.is_some() || 154 | self.instance_append.is_some() || 155 | self.command_post.is_some() || 156 | self.lua_post.is_some() || 157 | self.output.command.is_some() || 158 | self.output.lua.is_some() || 159 | self.output.pwd || 160 | self.output.filetype != "pager" 161 | ) 162 | } 163 | 164 | 165 | pub fn is_output_split_implied(&self) -> bool { 166 | *self.output_split_implied.get_or_init(|| 167 | self.output.split.split_left_cols.is_some() || 168 | self.output.split.split_right_cols.is_some() || 169 | self.output.split.split_above_rows.is_some() || 170 | self.output.split.split_below_rows.is_some() || 171 | self.output.split.split_left > 0u8 || 172 | self.output.split.split_right > 0u8 || 173 | self.output.split.split_above > 0u8 || 174 | self.output.split.split_below > 0u8 175 | ) 176 | } 177 | 178 | pub fn pagerized(&mut self) { 179 | self.arguments = None; 180 | self.config = None; 181 | self.command_post = None; 182 | self.lua_post = None; 183 | self.instance = None; 184 | self.instance_append = None; 185 | self.instance_close = None; 186 | self.page_no_protect = false; 187 | self.output.lua = None; 188 | self.output.command = None; 189 | self.output.noopen_lines = None; 190 | self.output.split.split_left = 0; 191 | self.output.split.split_right = 0; 192 | self.output.split.split_above = 0; 193 | self.output.split.split_below = 0; 194 | self.output.split.split_left_cols = None; 195 | self.output.split.split_right_cols = None; 196 | self.output.split.split_above_rows = None; 197 | self.output.split.split_below_rows = None; 198 | self.files = vec![]; 199 | } 200 | } 201 | 202 | 203 | // Options that are required on output buffer creation 204 | #[derive(Parser, Debug)] 205 | pub struct OutputOptions { 206 | /// Run command on output buffer after it was created 207 | #[clap(display_order=104, short='e')] 208 | pub command: Option, 209 | 210 | /// Run lua expr on output buffer after it was created 211 | #[clap(display_order=105, long="e")] 212 | pub lua: Option, 213 | 214 | /// Prefetch from page's stdin or [FILE]: if all 215 | /// input fits then print it to stdout and exit without neovim usage 216 | /// (to emulate `less --quit-if-one-screen`) 217 | /// [empty: term height - 3 (space for prompt); 218 | /// negative: term height - ; 219 | /// 0: disabled and default; 220 | /// ignored with -o, -p, -x and when page isn't piped] 221 | #[clap(display_order=1, short='O')] 222 | pub noopen_lines: Option>, 223 | 224 | /// Read no more than from page's stdin: 225 | /// next lines should be fetched by invoking 226 | /// :Page command or 'r'/'R' keypress on neovim side 227 | /// [empty: term height - 2 (space for tab and buffer lines); 228 | /// negative: term height - ; 229 | /// 0: disabled and default; 230 | /// is optional and defaults to ; 231 | /// doesn't take effect on buffers] 232 | #[clap(display_order=4, short='q')] 233 | pub query_lines: Option>, 234 | 235 | /// Set filetype on output buffer (to enable syntax highlighting) 236 | /// [pager: default; not works with text echoed by -O] 237 | #[clap(display_order=7, short='t', default_value="pager", hide_default_value=true)] 238 | pub filetype: String, 239 | 240 | /// Do not remap i, I, a, A, u, d, x, q (and r, R with -q) keys 241 | /// [wouldn't unmap on connected instance output buffer] 242 | #[clap(display_order=11, short='w')] 243 | pub writable: bool, 244 | 245 | /// Set $PWD as working directory at output buffer 246 | /// (to navigate paths with `gf`) 247 | #[clap(display_order=3, short='P')] 248 | pub pwd: bool, 249 | 250 | 251 | #[clap(flatten)] 252 | pub split: SplitOptions, 253 | } 254 | 255 | 256 | // Options for split 257 | #[derive(Parser, Debug)] 258 | pub struct SplitOptions { 259 | /// Split left with ratio: window_width * 3 / ( + 1) 260 | #[clap(display_order=900, short='l', action=ArgAction::Count)] 261 | pub split_left: u8, 262 | 263 | /// Split right with ratio: window_width * 3 / ( + 1) 264 | #[clap(display_order=901, short='r', action=ArgAction::Count)] 265 | pub split_right: u8, 266 | 267 | /// Split above with ratio: window_height * 3 / ( + 1) 268 | #[clap(display_order=902, short='u', action=ArgAction::Count)] 269 | pub split_above: u8, 270 | 271 | /// Split below with ratio: window_height * 3 / ( + 1) 272 | #[clap(display_order=903, short='d', action=ArgAction::Count)] 273 | pub split_below: u8, 274 | 275 | /// Split left and resize to columns 276 | #[clap(display_order=904, short='L')] 277 | pub split_left_cols: Option, 278 | 279 | /// Split right and resize to columns 280 | #[clap(display_order=905, short='R')] 281 | pub split_right_cols: Option, 282 | 283 | /// Split above and resize to rows 284 | #[clap(display_order=906, short='U')] 285 | pub split_above_rows: Option, 286 | 287 | /// Split below and resize to rows {n} 288 | /// ^ 289 | #[clap(display_order=907, short='D')] 290 | pub split_below_rows: Option, 291 | 292 | /// With any of -r -l -u -d -R -L -U -D open floating window instead of split 293 | /// [to not overwrite data in the current terminal] {n} 294 | /// ~ ~ ~ 295 | #[clap(display_order=908, short='+')] 296 | pub popup: bool, 297 | } 298 | 299 | 300 | fn instance_use_arg_group() -> ArgGroup { 301 | ArgGroup::new("instances") 302 | .args(["instance", "instance_append"]) 303 | .multiple(false) 304 | } 305 | 306 | fn back_arg_group() -> ArgGroup { 307 | ArgGroup::new("focusing") 308 | .args(["back", "back_restore"]) 309 | .multiple(false) 310 | } 311 | 312 | fn follow_arg_group() -> ArgGroup { 313 | ArgGroup::new("following") 314 | .args(["follow", "follow_all"]) 315 | .multiple(false) 316 | } 317 | 318 | fn splits_arg_group() -> ArgGroup { 319 | ArgGroup::new("splits") 320 | .args([ 321 | "split_left", 322 | "split_right", 323 | "split_above", 324 | "split_below" 325 | ]) 326 | .args([ 327 | "split_left_cols", 328 | "split_right_cols", 329 | "split_above_rows", 330 | "split_below_rows" 331 | ]) 332 | .multiple(false) 333 | } 334 | 335 | 336 | pub fn get_options() -> Options { 337 | Options::parse() 338 | } 339 | 340 | 341 | #[derive(Debug, Clone)] 342 | pub enum FileOption { 343 | Uri(String), 344 | Path(String), 345 | } 346 | 347 | impl From<&std::ffi::OsStr> for FileOption { 348 | fn from(value: &std::ffi::OsStr) -> Self { 349 | let s = value.to_string_lossy(); 350 | let mut chars = s.chars(); 351 | 352 | loop { 353 | match chars.next() { 354 | Some('+' | '-' | '.') => continue, 355 | 356 | Some(c) if c.is_alphanumeric() => continue, 357 | 358 | Some(c) if c == ':' && 359 | matches!(chars.next(), Some('/')) && 360 | matches!(chars.next(), Some('/')) => 361 | 362 | return FileOption::Uri(String::from(s)), 363 | 364 | _ => {} 365 | } 366 | 367 | return FileOption::Path(String::from(s)) 368 | } 369 | } 370 | } 371 | 372 | impl FileOption { 373 | pub fn as_str(&self) -> &str { 374 | let (FileOption::Uri(s) | FileOption::Path(s)) = self; 375 | s 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/pager/context.rs: -------------------------------------------------------------------------------- 1 | /// A module that contains data collected throughout page invocation 2 | 3 | pub use gather_env::Env; 4 | pub use check_usage::Usage; 5 | pub use connect_neovim::Neovim; 6 | pub use output_buffer_available::Output; 7 | 8 | 9 | pub mod gather_env { 10 | /// Contains data available after cli options parsed 11 | #[derive(Debug)] 12 | pub struct Env { 13 | pub opt: crate::cli::Options, 14 | pub prefetch_usage: PrefetchLinesUsage, 15 | pub query_lines_count: usize, 16 | pub input_from_pipe: bool, 17 | } 18 | 19 | pub fn enter() -> Env { 20 | let input_from_pipe = !atty::is(atty::Stream::Stdin); 21 | 22 | let opt = parse_and_alter_opts(input_from_pipe); 23 | 24 | let (term_height, prefetch_usage) = determine_prefetch_usage( 25 | opt.output.noopen_lines, 26 | opt.pagerize, 27 | &opt.files, 28 | input_from_pipe 29 | ); 30 | 31 | let query_lines_count = determine_query_lines_count( 32 | opt.output.query_lines, 33 | term_height 34 | ); 35 | 36 | Env { 37 | opt, 38 | prefetch_usage, 39 | query_lines_count, 40 | input_from_pipe, 41 | } 42 | } 43 | 44 | 45 | fn parse_and_alter_opts(input_from_pipe: bool) -> crate::cli::Options { 46 | let mut opt = crate::cli::get_options(); 47 | 48 | // Remove some arguments from pagerized invocation 49 | if opt.pagerize_hidden.is_some() { 50 | opt.pagerized(); 51 | } 52 | 53 | // Don't pagerize with -p enabled or when not read from pipe 54 | if opt.pty_path_print || !input_from_pipe { 55 | opt.pagerize = None; 56 | } 57 | 58 | // Fallback for neovim < 8.0 which don't uses $NVIM 59 | if opt.address.is_none() { 60 | if let Ok(address) = std::env::var("NVIM_LISTEN_ADDRESS") { 61 | opt.address.replace(address); 62 | } 63 | } 64 | 65 | // Treat empty -a value as if it wasn't provided 66 | if opt.address.as_deref().map_or(false, str::is_empty) { 67 | opt.address = None; 68 | } 69 | 70 | // Override -O by -o, -p and -x flags and when page don't read from pipe 71 | if opt.output_open || 72 | opt.pty_path_print || 73 | opt.instance_close.is_some() || 74 | (!input_from_pipe && opt.files.len() != 1) 75 | { 76 | opt.output.noopen_lines = None; 77 | } 78 | 79 | opt 80 | } 81 | 82 | 83 | type TermHeight = usize; 84 | 85 | fn determine_prefetch_usage( 86 | noopen_lines: Option>, 87 | pagerize: Option>, 88 | files: &Vec, 89 | input_from_pipe: bool 90 | ) -> (TermHeight, PrefetchLinesUsage) { 91 | use once_cell::unsync::Lazy; 92 | 93 | let term_dimensions = Lazy::new(|| { 94 | term_size::dimensions() 95 | .expect("Cannot get terminal dimensions") 96 | }); 97 | 98 | let (term_width, term_height) = ( 99 | Lazy::new(|| term_dimensions.0), 100 | Lazy::new(|| term_dimensions.1), 101 | ); 102 | 103 | let mut prefetch_lines_count = match noopen_lines { 104 | Some(Some(positive_number @ 0..)) => positive_number as usize, 105 | Some(Some(negative_number)) => term_height 106 | .saturating_sub(negative_number.unsigned_abs()), 107 | Some(None) => term_height 108 | .saturating_sub(3), 109 | None => 0 110 | }; 111 | 112 | if prefetch_lines_count > 0 { 113 | match pagerize { 114 | Some(Some(n)) if prefetch_lines_count > n => { 115 | prefetch_lines_count = n; 116 | }, 117 | Some(None) if prefetch_lines_count > 90_000 => { 118 | prefetch_lines_count = 90_000; 119 | } 120 | _ => {} 121 | } 122 | } 123 | 124 | let mut prefetch_usage = PrefetchLinesUsage::Disabled; 125 | if prefetch_lines_count != 0 && files.is_empty() && input_from_pipe { 126 | prefetch_usage = PrefetchLinesUsage::Enabled { 127 | line_count: prefetch_lines_count, 128 | term_width: *term_width, 129 | source: PrefetchLinesSource::Stdin, 130 | }; 131 | } else if prefetch_lines_count != 0 && files.len() == 1 && !input_from_pipe { 132 | let last_file = files 133 | .last() 134 | .unwrap(); 135 | 136 | if let crate::cli::FileOption::Path(f) = last_file { 137 | prefetch_usage = PrefetchLinesUsage::Enabled { 138 | line_count: prefetch_lines_count, 139 | term_width: *term_width, 140 | source: PrefetchLinesSource::File(f.clone()), 141 | } 142 | } 143 | } 144 | 145 | (*term_height, prefetch_usage) 146 | } 147 | 148 | 149 | fn determine_query_lines_count( 150 | query_lines: Option>, 151 | term_height: TermHeight, 152 | ) -> usize { 153 | match query_lines { 154 | Some(Some(positive_number @ 0..)) => positive_number as usize, 155 | Some(Some(negative_number)) => term_height 156 | .saturating_sub(negative_number.unsigned_abs()), 157 | Some(None) => term_height 158 | .saturating_sub(3), 159 | None => 0, 160 | } 161 | } 162 | 163 | 164 | #[derive(Debug)] 165 | pub enum PrefetchLinesUsage { 166 | Enabled { 167 | line_count: usize, 168 | term_width: usize, 169 | source: PrefetchLinesSource 170 | }, 171 | Disabled, 172 | } 173 | 174 | #[derive(Debug)] 175 | pub enum PrefetchLinesSource { 176 | Stdin, 177 | File(String), 178 | } 179 | } 180 | 181 | 182 | pub mod check_usage { 183 | 184 | /// Contains data available after page was spawned from shell 185 | #[derive(Debug)] 186 | pub struct Usage { 187 | pub opt: crate::cli::Options, 188 | pub tmp_dir: std::path::PathBuf, 189 | pub page_id: u128, 190 | pub prefetched_lines: PrefetchedLines, 191 | pub query_lines_count: usize, 192 | pub input_from_pipe: bool, 193 | pub print_protection: bool, 194 | } 195 | 196 | impl Usage { 197 | pub fn is_focus_on_existed_instance_buffer_implied(&self) -> bool { 198 | let Usage { opt, .. } = self; 199 | 200 | // Should focus in order to scroll buffer down 201 | opt.follow || 202 | 203 | // Autocommands should run on focused buffer 204 | opt.command_auto || 205 | 206 | // User command should run on focused buffer 207 | opt.command_post.is_some() || 208 | 209 | // Same with lua user command 210 | opt.lua_post.is_some() || 211 | 212 | // Otherwise, without -b and -B flags output buffer should be focused 213 | (!opt.back && !opt.back_restore) 214 | } 215 | 216 | 217 | pub fn lines_has_been_prefetched(&mut self, lines: Vec>) { 218 | self.prefetched_lines = PrefetchedLines(lines); 219 | } 220 | } 221 | 222 | 223 | pub fn enter(env_ctx: super::Env) -> Usage { 224 | 225 | let super::Env { 226 | input_from_pipe, 227 | opt, 228 | query_lines_count, 229 | .. 230 | } = env_ctx; 231 | 232 | let prefetched_lines = PrefetchedLines(vec![]); 233 | 234 | let tmp_dir = create_temp_directory(); 235 | 236 | let page_id = if let Some([_, page_id]) = opt.pagerize_hidden.as_deref() { 237 | *page_id 238 | } else { 239 | create_page_id() 240 | }; 241 | 242 | let print_protection = determine_if_should_print_protection( 243 | input_from_pipe, 244 | opt.page_no_protect, 245 | ); 246 | 247 | Usage { 248 | opt, 249 | tmp_dir, 250 | page_id, 251 | prefetched_lines, 252 | query_lines_count, 253 | input_from_pipe, 254 | print_protection, 255 | } 256 | } 257 | 258 | fn create_temp_directory() -> std::path::PathBuf { 259 | let d = std::env::temp_dir() 260 | .join("neovim-page"); 261 | std::fs::create_dir_all(&d) 262 | .expect("Cannot create temporary directory for page"); 263 | d 264 | } 265 | 266 | fn create_page_id() -> u128 { 267 | // This should provide enough entropy for current use case 268 | std::time::UNIX_EPOCH 269 | .elapsed() 270 | .unwrap() 271 | .as_nanos() 272 | } 273 | 274 | 275 | fn determine_if_should_print_protection( 276 | input_from_pipe: bool, 277 | page_no_protect: bool, 278 | ) -> bool { 279 | !input_from_pipe && !page_no_protect && 280 | std::env::var_os("PAGE_REDIRECTION_PROTECT") 281 | .map_or(true, |protect| protect != "" && protect != "0") 282 | } 283 | 284 | pub struct PrefetchedLines(pub Vec>); 285 | 286 | impl std::fmt::Debug for PrefetchedLines { 287 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 288 | write!(f, "{} Strings", self.0.len()) 289 | } 290 | } 291 | } 292 | 293 | 294 | pub mod connect_neovim { 295 | /// Contains data available after neovim is connected to page 296 | #[derive(Debug)] 297 | pub struct Neovim { 298 | pub opt: crate::cli::Options, 299 | pub page_id: u128, 300 | pub prefetched_lines: super::check_usage::PrefetchedLines, 301 | pub query_lines_count: usize, 302 | pub inst_usage: InstanceUsage, 303 | pub outp_buf_usage: OutputBufferUsage, 304 | pub nvim_child_proc_spawned: bool, 305 | pub input_from_pipe: bool, 306 | } 307 | 308 | impl Neovim { 309 | pub fn is_split_flag_given_with_files(&self) -> bool { 310 | self.outp_buf_usage.is_create_split() && 311 | !self.opt.files.is_empty() 312 | } 313 | 314 | 315 | pub fn child_neovim_process_has_been_spawned(&mut self) { 316 | self.nvim_child_proc_spawned = true; 317 | 318 | if !self.outp_buf_usage.is_disabled() { 319 | self.outp_buf_usage = OutputBufferUsage::CreateSubstituting; 320 | } 321 | } 322 | } 323 | 324 | 325 | pub fn enter(cli_ctx: super::Usage) -> Neovim { 326 | let should_focus_on_existed_instance_buffer = cli_ctx 327 | .is_focus_on_existed_instance_buffer_implied(); 328 | 329 | let super::Usage { 330 | opt, 331 | input_from_pipe, 332 | page_id, 333 | prefetched_lines, 334 | query_lines_count, 335 | .. 336 | } = cli_ctx; 337 | 338 | let inst_usage = determine_instance_usage( 339 | &opt.instance, 340 | &opt.instance_append, 341 | should_focus_on_existed_instance_buffer 342 | ); 343 | 344 | let outp_buf_usage = determine_output_buffer_usage( 345 | opt.is_output_split_implied(), 346 | opt.is_output_implied(), 347 | &opt.instance_close, 348 | &opt.files, 349 | input_from_pipe 350 | ); 351 | 352 | Neovim { 353 | opt, 354 | page_id, 355 | prefetched_lines, 356 | query_lines_count, 357 | inst_usage, 358 | outp_buf_usage, 359 | input_from_pipe, 360 | nvim_child_proc_spawned: false, 361 | } 362 | } 363 | 364 | fn determine_instance_usage( 365 | instance: &Option, 366 | instance_append: &Option, 367 | should_focus_on_existed_instance_buffer: bool 368 | ) -> InstanceUsage { 369 | let mut inst_usage = InstanceUsage::Disabled; 370 | 371 | if let Some(name) = instance.clone() { 372 | inst_usage = InstanceUsage::Enabled { 373 | name, 374 | focused: true, 375 | replace_content: true 376 | } 377 | } else if let Some(name) = instance_append.clone() { 378 | inst_usage = InstanceUsage::Enabled { 379 | name, 380 | focused: should_focus_on_existed_instance_buffer, 381 | replace_content: false 382 | } 383 | } 384 | 385 | inst_usage 386 | } 387 | 388 | fn determine_output_buffer_usage( 389 | is_output_split_implied: bool, 390 | is_output_implied: bool, 391 | instance_close: &Option, 392 | files: &Vec, 393 | input_from_pipe: bool, 394 | ) -> OutputBufferUsage { 395 | let mut outp_buf_usage = OutputBufferUsage::Disabled; 396 | 397 | if is_output_split_implied { 398 | outp_buf_usage = OutputBufferUsage::CreateSplit; 399 | } else if input_from_pipe || is_output_implied || 400 | (instance_close.is_none() && files.is_empty()) 401 | { 402 | outp_buf_usage = OutputBufferUsage::CreateSubstituting; 403 | } 404 | 405 | outp_buf_usage 406 | } 407 | 408 | 409 | #[derive(Debug)] 410 | pub enum InstanceUsage { 411 | Enabled { 412 | name: String, 413 | focused: bool, 414 | replace_content: bool 415 | }, 416 | Disabled, 417 | } 418 | 419 | impl InstanceUsage { 420 | pub fn is_enabled_and_should_be_focused(&self) -> bool { 421 | matches!(self, Self::Enabled { focused: true, .. }) 422 | } 423 | 424 | 425 | pub fn is_enabled_but_should_be_unfocused(&self) -> bool { 426 | matches!(self, Self::Enabled { focused: false, .. }) 427 | } 428 | 429 | 430 | pub fn is_enabled_and_should_replace_its_content(&self) -> bool { 431 | matches!(self, Self::Enabled { replace_content: true, .. }) 432 | } 433 | } 434 | 435 | 436 | #[derive(Debug)] 437 | pub enum OutputBufferUsage { 438 | CreateSubstituting, 439 | CreateSplit, 440 | Disabled, 441 | } 442 | 443 | impl OutputBufferUsage { 444 | pub fn is_disabled(&self) -> bool { 445 | matches!(self, Self::Disabled) 446 | } 447 | 448 | 449 | pub fn is_create_split(&self) -> bool { 450 | matches!(self, Self::CreateSplit) 451 | } 452 | } 453 | } 454 | 455 | 456 | pub mod output_buffer_available { 457 | /// Contains data available after buffer for output was found 458 | #[derive(Debug)] 459 | pub struct Output { 460 | pub opt: crate::cli::Options, 461 | pub buf_pty_path: std::path::PathBuf, 462 | pub prefetched_lines: super::check_usage::PrefetchedLines, 463 | pub query_lines_count: usize, 464 | pub inst_usage: super::connect_neovim::InstanceUsage, 465 | pub input_from_pipe: bool, 466 | pub restore_initial_buf_focus: RestoreInitialBufferFocus, 467 | pub nvim_child_proc_spawned: bool, 468 | pub print_output_buf_pty: bool, 469 | pub page_id: u128, 470 | pub pagerized_page_size: Option, 471 | } 472 | 473 | impl Output { 474 | pub fn instance_output_buffer_has_been_created(&mut self) { 475 | if let super::connect_neovim::InstanceUsage::Enabled { 476 | focused, 477 | .. 478 | } = &mut self.inst_usage { 479 | 480 | // Obtains focus on buffer creation 481 | *focused = true; 482 | } 483 | } 484 | 485 | pub fn should_pagerize(&self, lines_displayed: usize) -> bool { 486 | self.pagerized_page_size 487 | .map_or(false, |pps| lines_displayed >= pps) 488 | } 489 | } 490 | 491 | 492 | pub fn enter( 493 | nvim_ctx: super::Neovim, 494 | buf_pty_path: std::path::PathBuf 495 | ) -> Output { 496 | 497 | let super::Neovim { 498 | opt, 499 | nvim_child_proc_spawned, 500 | input_from_pipe, 501 | inst_usage, 502 | prefetched_lines, 503 | query_lines_count, 504 | page_id, 505 | .. 506 | } = nvim_ctx; 507 | 508 | let restore_initial_buf_focus = determine_restore_initial_buffer_focus( 509 | nvim_child_proc_spawned, 510 | opt.back, 511 | opt.back_restore 512 | ); 513 | 514 | let pagerized_page_size = determine_pagerized_page_size(&opt.pagerize); 515 | 516 | let print_output_buf_pty = opt.pty_path_print || 517 | (!nvim_child_proc_spawned && !input_from_pipe); 518 | 519 | Output { 520 | opt, 521 | buf_pty_path, 522 | prefetched_lines, 523 | query_lines_count, 524 | inst_usage, 525 | input_from_pipe, 526 | restore_initial_buf_focus, 527 | nvim_child_proc_spawned, 528 | print_output_buf_pty, 529 | page_id, 530 | pagerized_page_size, 531 | } 532 | } 533 | 534 | fn determine_restore_initial_buffer_focus( 535 | nvim_child_proc_spawned: bool, 536 | back: bool, 537 | back_restore: bool, 538 | ) -> RestoreInitialBufferFocus { 539 | let mut restore_initial_buf_focus = RestoreInitialBufferFocus::Disabled; 540 | 541 | if !nvim_child_proc_spawned { 542 | if back { 543 | restore_initial_buf_focus = RestoreInitialBufferFocus::ViModeNormal; 544 | } else if back_restore { 545 | restore_initial_buf_focus = RestoreInitialBufferFocus::ViModeInsert; 546 | } 547 | } 548 | 549 | restore_initial_buf_focus 550 | } 551 | 552 | 553 | fn determine_pagerized_page_size( 554 | pagerize: &Option> 555 | ) -> Option { 556 | match pagerize { 557 | Some(Some(number)) => Some(*number), 558 | Some(None) => Some(90_000), 559 | None => None, 560 | } 561 | } 562 | 563 | 564 | #[derive(Debug)] 565 | pub enum RestoreInitialBufferFocus { 566 | ViModeNormal, 567 | ViModeInsert, 568 | Disabled, 569 | } 570 | 571 | impl RestoreInitialBufferFocus { 572 | pub fn is_disabled(&self) -> bool { 573 | matches!(self, Self::Disabled) 574 | } 575 | 576 | 577 | pub fn is_vi_mode_insert(&self) -> bool { 578 | matches!(self, Self::ViModeInsert) 579 | } 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /src/pager/main.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod cli; 2 | pub(crate) mod neovim; 3 | pub(crate) mod context; 4 | 5 | pub type NeovimConnection = connection::NeovimConnection; 6 | pub type NeovimBuffer = connection::Buffer; 7 | 8 | 9 | #[tokio::main(worker_threads=2)] 10 | async fn main() { 11 | 12 | connection::init_logger(); 13 | 14 | let env_ctx = context::gather_env::enter(); 15 | 16 | main::warn_if_incompatible_options(&env_ctx.opt); 17 | 18 | validate_files(env_ctx).await; 19 | } 20 | 21 | mod main { 22 | 23 | // Some options takes effect only when page would be 24 | // spawned from neovim's terminal 25 | pub fn warn_if_incompatible_options(opt: &super::cli::Options) { 26 | if opt.address.is_some() { 27 | return 28 | } 29 | 30 | if opt.instance_close.is_some() { 31 | log::warn!( 32 | target: "usage", 33 | "Instance close (-x) is ignored \ 34 | if address (-a or $NVIM) isn't set" 35 | ); 36 | } 37 | if opt.is_output_split_implied() { 38 | log::warn!( 39 | target: "usage", 40 | "Split (-r -l -u -d -R -L -U -D) is ignored \ 41 | if address (-a or $NVIM) isn't set" 42 | ); 43 | } 44 | if opt.back || opt.back_restore { 45 | log::warn!( 46 | target: "usage", 47 | "Switch back (-b -B) is ignored \ 48 | if address (-a or $NVIM) isn't set" 49 | ); 50 | } 51 | } 52 | } 53 | 54 | 55 | async fn validate_files(mut env_ctx: context::Env) { 56 | log::info!(target: "context", "{env_ctx:#?}"); 57 | 58 | let files_count = env_ctx.opt.files.len(); 59 | for i in 0..files_count { 60 | 61 | use cli::FileOption::Path; 62 | let Path(path) = &mut env_ctx.opt.files[i] else { 63 | // Uri 64 | continue 65 | }; 66 | 67 | match std::fs::canonicalize(&path) { 68 | Ok(canonical) => { 69 | 70 | *path = canonical 71 | .to_string_lossy() 72 | .to_string(); 73 | } 74 | Err(e) => { 75 | log::error!( 76 | target: "open file", 77 | r#"Cannot open "{path}": {e}"#); 78 | 79 | env_ctx.opt.files 80 | .remove(i); 81 | } 82 | } 83 | } 84 | 85 | let all_files_not_exists = files_count > 0 86 | && env_ctx.opt.files.is_empty(); 87 | if all_files_not_exists && 88 | !env_ctx.input_from_pipe && 89 | !env_ctx.opt.is_output_implied() && 90 | !env_ctx.opt.is_output_split_implied() 91 | { 92 | std::process::exit(1) 93 | } 94 | 95 | prefetch_lines(env_ctx).await; 96 | } 97 | 98 | 99 | async fn prefetch_lines(env_ctx: context::Env) { 100 | log::info!(target: "context", "{env_ctx:#?}"); 101 | 102 | use context::gather_env::PrefetchLinesUsage; 103 | use context::gather_env::PrefetchLinesSource; 104 | 105 | let PrefetchLinesUsage::Enabled { 106 | line_count, 107 | term_width, 108 | source, 109 | } = &env_ctx.prefetch_usage else { 110 | 111 | let cli_ctx = context::check_usage::enter(env_ctx); 112 | connect_neovim(cli_ctx).await; 113 | 114 | return 115 | }; 116 | 117 | let mut prefetch_source_stdin; 118 | let mut prefetch_source_file; 119 | let prefetch_source: &mut dyn std::io::Read; 120 | 121 | match source { 122 | PrefetchLinesSource::Stdin => { 123 | prefetch_source_stdin = std::io::stdin(); 124 | 125 | prefetch_source = &mut prefetch_source_stdin; 126 | }, 127 | 128 | PrefetchLinesSource::File(path) => { 129 | let file = std::fs::File::open(path) 130 | .expect("Cannot open file"); 131 | prefetch_source_file = std::io::BufReader::new(file); 132 | 133 | prefetch_source = &mut prefetch_source_file; 134 | }, 135 | } 136 | 137 | let mut i = line_count + 1; 138 | let mut prefetched_lines = Vec::with_capacity(i); 139 | let mut bytes = std::io::Read::bytes(prefetch_source); 140 | 141 | 'read_next_ln: while i > 0 { 142 | let mut ln = Vec::with_capacity(*term_width); 143 | 144 | for b in bytes.by_ref() { 145 | match b { 146 | Err(e) => { 147 | panic!("Failed to prefetch line from stdin: {e}") 148 | } 149 | Ok(eol @ b'\n') => { 150 | ln.push(eol); 151 | ln.shrink_to_fit(); 152 | prefetched_lines.push(ln); 153 | i -= 1; 154 | continue 'read_next_ln; 155 | } 156 | Ok(b) => { 157 | ln.push(b); 158 | if ln.len() == *term_width { 159 | prefetched_lines.push(ln); 160 | i -= 1; 161 | continue 'read_next_ln; 162 | } 163 | } 164 | } 165 | } 166 | 167 | prefetched_lines.push(ln); 168 | 169 | if let PrefetchLinesUsage::Enabled { 170 | source: PrefetchLinesSource::File(path), 171 | .. 172 | } = env_ctx.prefetch_usage { 173 | 174 | let extenstion = std::path::Path::new(&path) 175 | .extension() 176 | .map_or_else( 177 | || String::from(&env_ctx.opt.output.filetype), 178 | |s| s.to_string_lossy().to_string() 179 | ); 180 | 181 | dump_prefetched_lines_and_exit( 182 | prefetched_lines, 183 | &extenstion 184 | ) 185 | } else { 186 | 187 | dump_prefetched_lines_and_exit( 188 | prefetched_lines, 189 | &env_ctx.opt.output.filetype, 190 | ) 191 | }; 192 | } 193 | 194 | let mut cli_ctx = context::check_usage::enter(env_ctx); 195 | cli_ctx 196 | .lines_has_been_prefetched(prefetched_lines); 197 | 198 | connect_neovim(cli_ctx).await; 199 | } 200 | 201 | 202 | fn dump_prefetched_lines_and_exit(lines: Vec>, filetype: &str) -> ! { 203 | log::info!(target: "dump", "{filetype}: {} lines", lines.len()); 204 | 205 | let stdout; 206 | let mut stdout_lock; 207 | let mut bat_proc = None; 208 | 209 | let output: &mut dyn std::io::Write; 210 | 211 | if !filetype.is_empty() && filetype != "pager" { 212 | let try_spawn_bat = std::process::Command::new("bat") 213 | .arg("--plain") 214 | .arg("--paging=never") 215 | .arg("--color=always") 216 | .arg(&format!("--language={}", filetype)) 217 | .stdin(std::process::Stdio::piped()) 218 | .spawn(); 219 | 220 | match try_spawn_bat { 221 | Ok(proc) => { 222 | log::info!(target: "dump", "use bat"); 223 | 224 | let proc = bat_proc.get_or_insert(proc); 225 | output = proc.stdin 226 | .as_mut() 227 | .expect("Cannot get bat stdin"); 228 | } 229 | Err(e) => { 230 | log::warn!(target: "dump", "cannot spawn bat, use stdout: {e:?}"); 231 | 232 | stdout = std::io::stdout(); 233 | stdout_lock = stdout.lock(); 234 | output = &mut stdout_lock; 235 | } 236 | } 237 | } else { 238 | log::info!(target: "dump", "use stdout"); 239 | 240 | stdout = std::io::stdout(); 241 | stdout_lock = stdout.lock(); 242 | output = &mut stdout_lock; 243 | } 244 | 245 | for ln in lines { 246 | std::io::Write::write_all(output, &ln) 247 | .expect("Cannot dump prefetched line"); 248 | } 249 | output.flush() 250 | .expect("Cannot flush"); 251 | 252 | if let Some(mut proc) = bat_proc { 253 | proc.wait() 254 | .expect("bat process ended unexpectedly"); 255 | } 256 | 257 | std::process::exit(0) 258 | } 259 | 260 | 261 | async fn connect_neovim(cli_ctx: context::Usage) { 262 | log::info!(target: "context", "{cli_ctx:#?}"); 263 | 264 | connection::init_panic_hook(); 265 | 266 | let mut nvim_conn = connection::open( 267 | &cli_ctx.tmp_dir, 268 | cli_ctx.page_id, 269 | &cli_ctx.opt.address, 270 | &cli_ctx.opt.config, 271 | &cli_ctx.opt.config, 272 | cli_ctx.print_protection 273 | ).await; 274 | 275 | let mut nvim_ctx = context::connect_neovim::enter(cli_ctx); 276 | if nvim_conn.nvim_proc.is_some() { 277 | nvim_ctx 278 | .child_neovim_process_has_been_spawned(); 279 | } 280 | 281 | manage_page_state(&mut nvim_conn, nvim_ctx).await; 282 | } 283 | 284 | 285 | async fn manage_page_state( 286 | nvim_conn: &mut NeovimConnection, 287 | nvim_ctx: context::Neovim 288 | ) { 289 | log::info!(target: "context", "{nvim_ctx:#?}"); 290 | 291 | let mut api_actions = neovim_api_usage::begin(nvim_conn, &nvim_ctx); 292 | 293 | api_actions 294 | .close_page_instance_buffer() 295 | .await; 296 | api_actions 297 | .display_files() 298 | .await; 299 | 300 | use context::connect_neovim::OutputBufferUsage; 301 | if let OutputBufferUsage::Disabled = nvim_ctx.outp_buf_usage { 302 | 303 | connection::close_and_exit(nvim_conn).await; 304 | } 305 | 306 | use context::connect_neovim::InstanceUsage; 307 | if let InstanceUsage::Enabled { name, .. } = &nvim_ctx.inst_usage { 308 | 309 | let active_instance = api_actions 310 | .find_instance_buffer(name) 311 | .await; 312 | 313 | if let Some(active_inst_outp) = active_instance { 314 | 315 | let outp_ctx = context::output_buffer_available::enter( 316 | nvim_ctx, 317 | active_inst_outp.pty_path 318 | ); 319 | 320 | manage_output_buffer( 321 | nvim_conn, 322 | active_inst_outp.buf, 323 | outp_ctx 324 | ) 325 | .await; 326 | 327 | } else { 328 | let new_inst_outp = api_actions 329 | .create_instance_output_buffer(name) 330 | .await; 331 | 332 | let mut outp_ctx = context::output_buffer_available::enter( 333 | nvim_ctx, 334 | new_inst_outp.pty_path 335 | ); 336 | outp_ctx 337 | .instance_output_buffer_has_been_created(); 338 | 339 | manage_output_buffer( 340 | nvim_conn, 341 | new_inst_outp.buf, 342 | outp_ctx 343 | ) 344 | .await; 345 | } 346 | 347 | } else { 348 | let new_outp = api_actions 349 | .create_oneoff_output_buffer() 350 | .await; 351 | 352 | let outp_ctx = context::output_buffer_available::enter( 353 | nvim_ctx, 354 | new_outp.pty_path 355 | ); 356 | 357 | manage_output_buffer( 358 | nvim_conn, 359 | new_outp.buf, 360 | outp_ctx 361 | ) 362 | .await; 363 | }; 364 | } 365 | 366 | 367 | async fn manage_output_buffer( 368 | nvim_conn: &mut NeovimConnection, 369 | buf: NeovimBuffer, 370 | outp_ctx: context::Output 371 | ) { 372 | log::info!(target: "context", "{outp_ctx:#?}"); 373 | 374 | let mut outp_buf_actions = output_buffer_usage::begin( 375 | nvim_conn, 376 | &outp_ctx, 377 | buf, 378 | nvim_conn.channel, 379 | ); 380 | 381 | use context::connect_neovim::InstanceUsage; 382 | if let InstanceUsage::Enabled { name, .. } = &outp_ctx.inst_usage { 383 | 384 | outp_buf_actions 385 | .update_instance_buffer_title(name) 386 | .await; 387 | outp_buf_actions 388 | .focus_on_instance_buffer(name) 389 | .await; 390 | 391 | } else { 392 | 393 | outp_buf_actions 394 | .update_buffer_title() 395 | .await; 396 | } 397 | 398 | outp_buf_actions 399 | .execute_commands() 400 | .await; 401 | outp_buf_actions 402 | .focus_on_initial_buffer() 403 | .await; 404 | 405 | if outp_ctx.input_from_pipe { 406 | if outp_ctx.query_lines_count > 0 { 407 | outp_buf_actions 408 | .handle_query_output() 409 | .await; 410 | } else { 411 | outp_buf_actions 412 | .handle_output() 413 | .await; 414 | } 415 | } 416 | 417 | if outp_ctx.print_output_buf_pty { 418 | println!("{}", outp_ctx.buf_pty_path.to_string_lossy()); 419 | } 420 | 421 | outp_buf_actions 422 | .execute_disconnect_commands() 423 | .await; 424 | 425 | outp_buf_actions 426 | .done() 427 | .await; 428 | } 429 | 430 | 431 | 432 | mod neovim_api_usage { 433 | use super::{ 434 | NeovimConnection, 435 | context::Neovim, 436 | neovim::{OutputBuffer, OutputCommands} 437 | }; 438 | 439 | /// This struct implements actions that should be done 440 | /// before output buffer is available 441 | pub struct ApiActions<'a> { 442 | nvim_conn: &'a mut NeovimConnection, 443 | nvim_ctx: &'a Neovim, 444 | } 445 | 446 | pub fn begin<'a>( 447 | nvim_conn: &'a mut NeovimConnection, 448 | nvim_ctx: &'a Neovim 449 | ) -> ApiActions<'a> { 450 | ApiActions { 451 | nvim_conn, 452 | nvim_ctx, 453 | } 454 | } 455 | 456 | impl<'a> ApiActions<'a> { 457 | /// Closes buffer marked as instance, when mark is provided by -x argument 458 | pub async fn close_page_instance_buffer(&mut self) { 459 | let opt = &self.nvim_ctx.opt; 460 | 461 | if let Some(ref instance) = opt.instance_close { 462 | self.nvim_conn.nvim_actions 463 | .close_instance_buffer(instance) 464 | .await; 465 | } 466 | } 467 | 468 | 469 | /// Opens each file provided as free arguments in separate buffers. 470 | /// Resets focus to initial buffer and window if further 471 | /// there will be created output buffer in split window, 472 | /// since we want to see shell from which that output buffer was spawned 473 | pub async fn display_files(&mut self) { 474 | let ApiActions { 475 | nvim_conn: NeovimConnection { 476 | nvim_actions, 477 | initial_buf_number, 478 | initial_win_and_buf, 479 | .. 480 | }, 481 | nvim_ctx 482 | } = self; 483 | 484 | for f in &nvim_ctx.opt.files { 485 | if let Err(e) = nvim_actions.open_file_buffer(f.as_str()).await { 486 | log::warn!(target: "page file", r#"Error opening "{f:?}": {e}"#); 487 | 488 | continue; 489 | } 490 | 491 | let cmd_provided_by_user = &nvim_ctx.opt.output.command.as_deref() 492 | .unwrap_or_default(); 493 | let lua_provided_by_user = &nvim_ctx.opt.output.lua.as_deref() 494 | .unwrap_or_default(); 495 | let writeable = nvim_ctx.opt.output.writable; 496 | 497 | let file_buf_opts = OutputCommands::for_file_buffer( 498 | cmd_provided_by_user, 499 | lua_provided_by_user, 500 | writeable 501 | ); 502 | 503 | nvim_actions 504 | .prepare_output_buffer(*initial_buf_number, file_buf_opts) 505 | .await; 506 | 507 | if nvim_ctx.opt.follow_all { 508 | nvim_actions 509 | .set_current_buffer_follow_output_mode() 510 | .await; 511 | } else { 512 | nvim_actions 513 | .set_current_buffer_scroll_mode() 514 | .await; 515 | } 516 | } 517 | 518 | if nvim_ctx.is_split_flag_given_with_files() { 519 | // Split terminal buffer instead of file buffer 520 | nvim_actions 521 | .switch_to_window_and_buffer(initial_win_and_buf) 522 | .await; 523 | } 524 | } 525 | 526 | 527 | /// Returns buffer marked as instance, 528 | /// together with path to PTY device 529 | /// associated with it (if some exists) 530 | pub async fn find_instance_buffer( 531 | &mut self, 532 | inst_name: &str 533 | ) -> Option { 534 | let outp = self.nvim_conn.nvim_actions 535 | .find_instance_buffer(inst_name) 536 | .await; 537 | 538 | outp 539 | } 540 | 541 | 542 | /// Creates a new output buffer 543 | /// and then marks it as instance buffer 544 | pub async fn create_instance_output_buffer( 545 | &mut self, 546 | inst_name: &str 547 | ) -> OutputBuffer { 548 | let outp = self 549 | .create_oneoff_output_buffer() 550 | .await; 551 | 552 | self.nvim_conn.nvim_actions 553 | .mark_buffer_as_instance( 554 | &outp.buf, 555 | inst_name, 556 | &outp.pty_path.to_string_lossy() 557 | ) 558 | .await; 559 | 560 | outp 561 | } 562 | 563 | 564 | /// Creates a new output buffer using split window if required. 565 | /// Also sets some nvim options for better reading experience 566 | pub async fn create_oneoff_output_buffer(&mut self) -> OutputBuffer { 567 | let ApiActions { 568 | nvim_conn: NeovimConnection { 569 | nvim_actions, 570 | initial_buf_number, 571 | channel, 572 | nvim_proc, 573 | .. 574 | }, 575 | nvim_ctx 576 | } = self; 577 | 578 | let outp = if nvim_proc.is_some() && nvim_ctx.opt.files.is_empty() { 579 | nvim_actions 580 | .create_replacing_output_buffer() 581 | .await 582 | } else if nvim_ctx.outp_buf_usage.is_create_split() { 583 | nvim_actions 584 | .create_split_output_buffer(&nvim_ctx.opt.output.split) 585 | .await 586 | } else { 587 | nvim_actions 588 | .create_switching_output_buffer() 589 | .await 590 | }; 591 | 592 | let channel = if let Some(chan_id) = &nvim_ctx.opt.pagerize_hidden { 593 | chan_id[0] 594 | } else { 595 | u128::from(*channel) 596 | }; 597 | 598 | let outp_buf_opts = OutputCommands::for_output_buffer( 599 | nvim_ctx.page_id, 600 | channel, 601 | nvim_ctx.query_lines_count, 602 | &nvim_ctx.opt.output 603 | ); 604 | nvim_actions 605 | .prepare_output_buffer(*initial_buf_number, outp_buf_opts) 606 | .await; 607 | 608 | outp 609 | } 610 | } 611 | } 612 | 613 | mod output_buffer_usage { 614 | use super::{NeovimConnection, NeovimBuffer, context::Output}; 615 | use connection::NotificationFromNeovim; 616 | use std::io::{Read, Write}; 617 | 618 | /// This struct implements actions that should be done 619 | /// after output buffer is attached 620 | pub struct BufferActions<'a> { 621 | nvim_conn: &'a mut NeovimConnection, 622 | outp_ctx: &'a Output, 623 | buf: NeovimBuffer, 624 | sink: Option>, 625 | pagerize_lines_displayed: usize, 626 | channel: u64, 627 | } 628 | 629 | pub fn begin<'a>( 630 | nvim_conn: &'a mut NeovimConnection, 631 | outp_ctx: &'a Output, 632 | buf: NeovimBuffer, 633 | channel: u64, 634 | ) -> BufferActions<'a> { 635 | BufferActions { 636 | nvim_conn, 637 | outp_ctx, 638 | buf, 639 | sink: None, 640 | pagerize_lines_displayed: 0, 641 | channel, 642 | } 643 | } 644 | 645 | impl<'a> BufferActions<'a> { 646 | /// This function updates buffer title depending on -n value. 647 | /// Icon symbol is received from neovim side 648 | /// and is prepended to the left of buffer title 649 | pub async fn update_buffer_title(&mut self) { 650 | let BufferActions { 651 | outp_ctx, 652 | buf, 653 | nvim_conn: NeovimConnection { nvim_actions, .. }, 654 | .. 655 | } = self; 656 | 657 | let (page_icon_key, page_icon_default) = if outp_ctx.input_from_pipe { 658 | ("page_icon_pipe", " |") 659 | } else { 660 | ("page_icon_redirect", " >") 661 | }; 662 | let mut buf_title = nvim_actions 663 | .get_var_or(page_icon_key, page_icon_default) 664 | .await; 665 | 666 | if let Some(ref buf_name) = outp_ctx.opt.name { 667 | buf_title.insert_str(0, buf_name); 668 | } 669 | 670 | nvim_actions 671 | .update_buffer_title(buf, &buf_title) 672 | .await; 673 | } 674 | 675 | 676 | /// This function updates instance buffer title 677 | /// depending on its name and -n value. 678 | /// Instance name will be prepended to the left 679 | /// of the icon symbol. 680 | pub async fn update_instance_buffer_title(&mut self, inst_name: &str) { 681 | let BufferActions { 682 | outp_ctx, 683 | buf, 684 | nvim_conn: NeovimConnection { nvim_actions, .. }, 685 | .. 686 | } = self; 687 | 688 | let (page_icon_key, page_icon_default) = ("page_icon_instance", "@ "); 689 | let mut buf_title = nvim_actions 690 | .get_var_or(page_icon_key, page_icon_default) 691 | .await; 692 | buf_title.insert_str(0, inst_name); 693 | 694 | if let Some(ref buf_name) = outp_ctx.opt.name { 695 | if buf_name != inst_name { 696 | buf_title.push_str(buf_name); 697 | } 698 | } 699 | 700 | nvim_actions 701 | .update_buffer_title(buf, &buf_title) 702 | .await; 703 | } 704 | 705 | 706 | /// Resets instance buffer focus and content. 707 | /// This is required to provide some functionality 708 | /// not available through neovim API 709 | pub async fn focus_on_instance_buffer(&mut self, inst_name: &str) { 710 | let BufferActions { 711 | outp_ctx, 712 | nvim_conn: NeovimConnection { nvim_actions, .. }, 713 | .. 714 | } = self; 715 | 716 | if !outp_ctx.inst_usage.is_enabled_and_should_be_focused() { 717 | return 718 | } 719 | 720 | nvim_actions 721 | .focus_instance_buffer(inst_name) 722 | .await; 723 | 724 | if outp_ctx.inst_usage.is_enabled_and_should_replace_its_content() { 725 | 726 | const CLEAR_SCREEN_SEQ: &[u8] = b"\x1B[3J\x1B[H\x1b[2J"; 727 | self 728 | .get_sink() 729 | .write_all(CLEAR_SCREEN_SEQ) 730 | .expect("Cannot write clear screen sequence"); 731 | } 732 | } 733 | 734 | 735 | /// Executes `PageConnect` (-C) and post command (-E) 736 | /// on page buffer. If any of these flags are passed 737 | /// then output buffer should be already focused 738 | pub async fn execute_commands(&mut self) { 739 | let BufferActions { 740 | outp_ctx, 741 | nvim_conn: NeovimConnection { nvim_actions, .. }, 742 | .. 743 | } = self; 744 | 745 | if outp_ctx.opt.command_auto { 746 | nvim_actions 747 | .execute_connect_autocmd_on_current_buffer() 748 | .await; 749 | } 750 | 751 | if let Some(ref lua_expr) = outp_ctx.opt.lua_post { 752 | nvim_actions 753 | .execute_command_post_lua(lua_expr) 754 | .await; 755 | } 756 | if let Some(ref command) = outp_ctx.opt.command_post { 757 | nvim_actions 758 | .execute_command_post(command) 759 | .await; 760 | } 761 | } 762 | 763 | 764 | /// Sets cursor position on page buffer and on current buffer 765 | /// depending on -f, -b, and -B flags provided. 766 | /// First if condition on this function ensures 767 | /// that it's really necessary to do any action, 768 | /// to circumvent flicker with `page -I 769 | /// existed -b` and `page -I existed -B` invocations 770 | pub async fn focus_on_initial_buffer(&mut self) { 771 | let BufferActions { 772 | outp_ctx, 773 | nvim_conn: NeovimConnection { nvim_actions, initial_win_and_buf, .. }, 774 | .. 775 | } = self; 776 | 777 | if outp_ctx.inst_usage.is_enabled_but_should_be_unfocused() { 778 | return 779 | } 780 | 781 | if outp_ctx.opt.follow { 782 | nvim_actions 783 | .set_current_buffer_follow_output_mode() 784 | .await; 785 | } else { 786 | nvim_actions 787 | .set_current_buffer_scroll_mode() 788 | .await; 789 | } 790 | 791 | if outp_ctx.restore_initial_buf_focus.is_disabled() { 792 | return 793 | } 794 | 795 | nvim_actions 796 | .switch_to_window_and_buffer(initial_win_and_buf) 797 | .await; 798 | 799 | if outp_ctx.restore_initial_buf_focus.is_vi_mode_insert() { 800 | nvim_actions 801 | .set_current_buffer_insert_mode() 802 | .await; 803 | } 804 | } 805 | 806 | 807 | /// Writes lines from stdin directly into PTY device 808 | /// associated with output buffer. 809 | pub async fn handle_output(&mut self) { 810 | log::trace!(target: "output", "handle output"); 811 | 812 | // First write all prefetched lines if any available 813 | for ln in &self.outp_ctx.prefetched_lines.0[..] { 814 | 815 | self.display_line(ln) 816 | .await 817 | .expect("Cannot write next prefetched line"); 818 | 819 | if self.outp_ctx 820 | .should_pagerize(self.pagerize_lines_displayed) 821 | { 822 | self.pagerize_output(); 823 | } 824 | } 825 | 826 | // Then copy the rest of lines from stdin into buffer pty 827 | let mut ln = Vec::with_capacity(2048); 828 | for b in std::io::stdin().bytes() { 829 | 830 | match b { 831 | Err(e) => { 832 | log::warn!( 833 | target: "output", 834 | "Error reading line from stdin: {e}" 835 | ); 836 | 837 | break; 838 | } 839 | 840 | Ok(eol @ b'\n') => { 841 | ln.push(eol); 842 | 843 | self.display_line(&ln) 844 | .await 845 | .expect("Cannot write next line"); 846 | 847 | ln.clear(); 848 | 849 | if self.outp_ctx 850 | .should_pagerize(self.pagerize_lines_displayed) 851 | { 852 | self.pagerize_output(); 853 | } 854 | } 855 | 856 | Ok(b) => ln.push(b) 857 | } 858 | } 859 | 860 | log::trace!(target: "output", "got EOF"); 861 | 862 | self.close_sink(); 863 | 864 | self.display_line(&[b'\0']) 865 | .await 866 | .expect("Cannot write EOF sequence"); 867 | } 868 | 869 | 870 | /// In case if -q argument provided it 871 | /// might block until next line will be request from neovim side. 872 | pub async fn handle_query_output(&mut self) { 873 | log::trace!(target: "output", "handle query output"); 874 | 875 | let mut state = QueryState::default(); 876 | state.next_part(self.outp_ctx.query_lines_count); 877 | 878 | // First write all prefetched lines if any available 879 | let mut prefetched_lines_iter = self.outp_ctx.prefetched_lines.0.iter(); 880 | loop { 881 | self.exchange_query_messages(&mut state) 882 | .await; 883 | 884 | let Some(ln) = prefetched_lines_iter.next() else { 885 | log::info!(target: "output", "Proceed query with stdin"); 886 | 887 | break 888 | }; 889 | 890 | self.display_line(ln) 891 | .await 892 | .expect("Cannot write next prefetched queried line"); 893 | 894 | state.line_has_been_sent(); 895 | 896 | if self.outp_ctx 897 | .should_pagerize(self.pagerize_lines_displayed) 898 | { 899 | self.exchange_query_messages(&mut state) 900 | .await; 901 | 902 | self.pagerize_output(); 903 | } 904 | } 905 | 906 | self.exchange_query_messages(&mut state) 907 | .await; 908 | 909 | // Then copy the rest of lines from stdin into buffer pty 910 | let mut ln = Vec::with_capacity(2048); 911 | for b in std::io::stdin().bytes() { 912 | 913 | match b { 914 | Err(e) => { 915 | log::warn!( 916 | target: "output", 917 | "Error reading queried line from stdin: {e}" 918 | ); 919 | 920 | break; 921 | } 922 | 923 | Ok(eol @ b'\n') => { 924 | ln.push(eol); 925 | 926 | self.display_line(&ln) 927 | .await 928 | .expect("Cannot write next line"); 929 | 930 | state.line_has_been_sent(); 931 | self.exchange_query_messages(&mut state) 932 | .await; 933 | 934 | ln.clear(); 935 | 936 | if self.outp_ctx 937 | .should_pagerize(self.pagerize_lines_displayed) 938 | { 939 | self.pagerize_output(); 940 | } 941 | } 942 | 943 | Ok(b) => ln.push(b) 944 | } 945 | 946 | } 947 | 948 | log::trace!(target: "output", "got EOF"); 949 | 950 | self.close_sink(); 951 | 952 | self.nvim_conn.nvim_actions 953 | .notify_query_finished(state.how_many_lines_was_sent()) 954 | .await; 955 | 956 | self.nvim_conn.nvim_actions 957 | .notify_end_of_input() 958 | .await; 959 | } 960 | 961 | 962 | /// Writes line to PTY device and gracefully handles failures: 963 | /// if error occurs then page waits for `page_buffer_closed` 964 | /// notification that's sent on `BufDelete` event and signals 965 | /// that buffer was closed intentionally, so page must just exit. 966 | /// If no such notification was arrived then page crashes 967 | /// with the received IO error 968 | async fn display_line(&mut self, ln: &[u8]) -> std::io::Result<()> { 969 | let pty = self.get_sink(); 970 | 971 | if let Err(e) = pty.write_all(ln) { 972 | log::info!(target: "writeline", "got error: {e:?}"); 973 | 974 | let wait_secs = std::time::Duration::from_secs(1); 975 | let notification_future = self.nvim_conn.rx 976 | .recv(); 977 | 978 | match tokio::time::timeout(wait_secs, notification_future) 979 | .await 980 | { 981 | Ok(Some(NotificationFromNeovim::BufferClosed)) => { 982 | log::info!( 983 | target: "writeline", 984 | "Buffer was closed, not all input is shown" 985 | ); 986 | 987 | self.done() 988 | .await; 989 | }, 990 | Ok(None) if self.nvim_conn.nvim_proc.is_some() => { 991 | log::info!( 992 | target: "writeline", 993 | "Neovim was closed, not all input is shown" 994 | ); 995 | 996 | self.done() 997 | .await; 998 | }, 999 | 1000 | _ => return Err(e), 1001 | } 1002 | } 1003 | 1004 | self.pagerize_lines_displayed += 1; 1005 | 1006 | Ok(()) 1007 | } 1008 | 1009 | /// If there's more than -z value lines to read (default `90_000`) 1010 | /// then output will be pagerized through spawning `page -p` and 1011 | /// writing to it's PTY device 1012 | fn pagerize_output(&mut self) { 1013 | self.pagerize_lines_displayed = 0; 1014 | 1015 | log::trace!(target: "pagerize", "output is too large"); 1016 | 1017 | let mut page_args = std::env::args(); 1018 | page_args.next(); // skip `page` 1019 | 1020 | let nvim_addr = if let Some(addr) = &self.outp_ctx.opt.address { 1021 | addr.clone() 1022 | } else { 1023 | std::env::temp_dir() 1024 | .join("neovim-page") 1025 | .join(&format!("socket-{}", &self.outp_ctx.page_id)) 1026 | .to_string_lossy() 1027 | .to_string() 1028 | }; 1029 | 1030 | let page_pty = std::process::Command::new("page") 1031 | .stdin(std::process::Stdio::null()) 1032 | .stdout(std::process::Stdio::piped()) 1033 | .arg("--pagerize-hidden") 1034 | .arg(self.channel.to_string()) 1035 | .arg(self.outp_ctx.page_id.to_string()) 1036 | .arg("-a") 1037 | .arg(nvim_addr) 1038 | .args(page_args) 1039 | .arg("-p") 1040 | .spawn() 1041 | .expect("Cannot spawn `page`") 1042 | .wait_with_output() 1043 | .expect("Cannot get `page` stdout") 1044 | .stdout; 1045 | 1046 | let page_pty = String::from_utf8(page_pty) 1047 | .expect("Non UTF8 `page` output"); 1048 | 1049 | self.sink 1050 | .replace( 1051 | Box::new( 1052 | std::fs::OpenOptions::new() 1053 | .append(true) 1054 | .open(page_pty.trim()) 1055 | .expect("Cannot open pagerized PTY device") 1056 | ) 1057 | ); 1058 | } 1059 | 1060 | 1061 | /// If the whole queried part was sent waits 1062 | /// for notifications from neovim, processes some 1063 | /// or schedules further query in write loop 1064 | async fn exchange_query_messages(&mut self, s: &mut QueryState) { 1065 | if !s.is_whole_part_sent() { 1066 | return 1067 | } 1068 | 1069 | self.nvim_conn.nvim_actions 1070 | .notify_query_finished(s.how_many_lines_was_sent()) 1071 | .await; 1072 | 1073 | match self.nvim_conn.rx 1074 | .recv() 1075 | .await 1076 | { 1077 | Some(NotificationFromNeovim::FetchLines(n)) => 1078 | s.next_part(n), 1079 | 1080 | Some(NotificationFromNeovim::FetchPart) => 1081 | s.next_part(self.outp_ctx.query_lines_count), 1082 | 1083 | Some(NotificationFromNeovim::BufferClosed) => { 1084 | log::info!(target: "output-state", "Buffer closed"); 1085 | 1086 | self.done() 1087 | .await; 1088 | } 1089 | None => { 1090 | log::info!(target: "output-state", "Neovim closed"); 1091 | 1092 | self.done() 1093 | .await; 1094 | } 1095 | } 1096 | } 1097 | 1098 | 1099 | /// Executes `PageDisconnect` autocommand if -C flag was provided. 1100 | /// Some time might pass since page buffer was created and 1101 | /// output was started, so this function might temporarily refocus 1102 | /// on output buffer in order to run autocommand 1103 | pub async fn execute_disconnect_commands(&mut self) { 1104 | let BufferActions { 1105 | nvim_conn: NeovimConnection { nvim_actions, initial_win_and_buf, .. }, 1106 | buf, 1107 | outp_ctx, 1108 | .. 1109 | } = self; 1110 | 1111 | if !outp_ctx.opt.command_auto { 1112 | return 1113 | } 1114 | 1115 | let active_buf = nvim_actions 1116 | .get_current_buffer() 1117 | .await 1118 | .expect("Cannot get currently active buffer to execute PageDisconnect"); 1119 | 1120 | let switched = buf != &active_buf; 1121 | if switched { 1122 | nvim_actions 1123 | .switch_to_buffer(buf) 1124 | .await 1125 | .expect("Cannot switch back to page buffer"); 1126 | } 1127 | 1128 | nvim_actions 1129 | .execute_disconnect_autocmd_on_current_buffer() 1130 | .await; 1131 | 1132 | // Page buffer probably may be closed in autocommand 1133 | let still_loaded = active_buf.is_loaded() 1134 | .await 1135 | .expect("Cannot check if buffer loaded"); 1136 | 1137 | if switched && still_loaded { 1138 | nvim_actions 1139 | .switch_to_buffer(&active_buf) 1140 | .await 1141 | .expect("Cannot switch back to active buffer"); 1142 | 1143 | let same_buffer = initial_win_and_buf.1 == active_buf; 1144 | if same_buffer && outp_ctx.restore_initial_buf_focus.is_vi_mode_insert() { 1145 | nvim_actions 1146 | .set_current_buffer_insert_mode() 1147 | .await; 1148 | } 1149 | } 1150 | } 1151 | 1152 | /// Closes neovim connection then exits with 0 status code 1153 | pub async fn done(&mut self) { 1154 | log::trace!(target: "done", "now page can exit"); 1155 | 1156 | connection::close_and_exit(self.nvim_conn).await; 1157 | } 1158 | 1159 | /// Returns PTY device associated with output buffer. 1160 | /// This function ensures that PTY device is opened only once 1161 | fn get_sink(&mut self) -> &mut Box { 1162 | self.sink 1163 | .get_or_insert_with(|| { 1164 | Box::new( 1165 | std::fs::OpenOptions::new() 1166 | .append(true) 1167 | .open(&self.outp_ctx.buf_pty_path) 1168 | .expect("Cannot open PTY device") 1169 | ) 1170 | }) 1171 | } 1172 | 1173 | fn close_sink(&mut self) { 1174 | self.sink 1175 | .take(); 1176 | } 1177 | 1178 | } 1179 | 1180 | /// Encapsulates state of querying lines from neovim side 1181 | /// with :Page command. 1182 | /// Used only when -q argument is provided 1183 | #[derive(Default)] 1184 | struct QueryState { 1185 | expect: usize, 1186 | remain: usize, 1187 | } 1188 | 1189 | impl QueryState { 1190 | fn next_part(&mut self, lines_to_read: usize) { 1191 | self.expect = lines_to_read; 1192 | self.remain = lines_to_read; 1193 | } 1194 | 1195 | 1196 | fn line_has_been_sent(&mut self) { 1197 | self.remain -= 1; 1198 | } 1199 | 1200 | 1201 | fn is_whole_part_sent(&self) -> bool { 1202 | self.remain == 0 1203 | } 1204 | 1205 | 1206 | fn how_many_lines_was_sent(&self) -> usize { 1207 | self.expect - self.remain 1208 | } 1209 | } 1210 | } 1211 | -------------------------------------------------------------------------------- /src/pager/neovim.rs: -------------------------------------------------------------------------------- 1 | /// A module that extends neovim api with methods required in page 2 | use nvim_rs::{neovim::Neovim, error::CallError, Buffer, Window, Value}; 3 | use indoc::{indoc, formatdoc}; 4 | use connection::IoWrite; 5 | use std::{path::PathBuf, convert::TryFrom}; 6 | 7 | 8 | /// This struct wraps `nvim_rs::Neovim` and decorates it 9 | /// with methods required in page. Results returned from underlying 10 | /// Neovim methods are mostly unwrapped, since we anyway cannot provide 11 | /// any meaningful falback logic on call side 12 | pub struct Actions { 13 | nvim: Neovim, 14 | } 15 | 16 | impl From> for Actions { 17 | fn from(nvim: Neovim) -> Self { 18 | Actions { nvim } 19 | } 20 | } 21 | 22 | impl Actions { 23 | pub async fn get_current_buffer(&mut self) -> Result, Box> { 24 | self.nvim 25 | .get_current_buf() 26 | .await 27 | } 28 | 29 | 30 | pub async fn create_replacing_output_buffer(&mut self) -> OutputBuffer { 31 | let cmd = indoc! {" 32 | local buf = vim.api.nvim_get_current_buf() 33 | "}; 34 | 35 | self.create_buffer(cmd) 36 | .await 37 | .expect("Error when creating output buffer from current") 38 | } 39 | 40 | 41 | pub async fn create_switching_output_buffer(&mut self) -> OutputBuffer { 42 | let cmd = indoc! {" 43 | local buf = vim.api.nvim_create_buf(true, false) 44 | vim.api.nvim_set_current_buf(buf) 45 | "}; 46 | 47 | self.create_buffer(cmd) 48 | .await 49 | .expect("Error when creating output buffer") 50 | } 51 | 52 | 53 | pub async fn create_split_output_buffer( 54 | &mut self, 55 | opt: &crate::cli::SplitOptions 56 | ) -> OutputBuffer { 57 | 58 | let cmd = if opt.popup { 59 | 60 | let w_ratio = |s| format!("math.floor(((w / 2) * 3) / {})", s + 1); 61 | let h_ratio = |s| format!("math.floor(((h / 2) * 3) / {})", s + 1); 62 | 63 | let (w, h, o) = ("w".to_string(), "h".to_string(), "0".to_string()); 64 | 65 | let (width, height, row, col); 66 | 67 | if opt.split_right != 0 { 68 | (width = w_ratio(opt.split_right), height = h, row = &o, col = &w) 69 | 70 | } else if opt.split_left != 0 { 71 | (width = w_ratio(opt.split_left), height = h, row = &o, col = &o) 72 | 73 | } else if opt.split_below != 0 { 74 | (width = w, height = h_ratio(opt.split_below), row = &h, col = &o) 75 | 76 | } else if opt.split_above != 0 { 77 | (width = w, height = h_ratio(opt.split_above), row = &o, col = &o) 78 | 79 | } else if let Some(split_right_cols) = opt.split_right_cols.map(|x| x.to_string()) { 80 | (width = split_right_cols, height = h, row = &o, col = &w) 81 | 82 | } else if let Some(split_left_cols) = opt.split_left_cols.map(|x| x.to_string()) { 83 | (width = split_left_cols, height = h, row = &o, col = &o) 84 | 85 | } else if let Some(split_below_rows) = opt.split_below_rows.map(|x| x.to_string()) { 86 | (width = w, height = split_below_rows, row = &h, col = &o) 87 | 88 | } else if let Some(split_above_rows) = opt.split_above_rows.map(|x| x.to_string()) { 89 | (width = w, height = split_above_rows, row = &o, col = &o) 90 | 91 | } else { 92 | unreachable!() 93 | }; 94 | 95 | formatdoc! {" 96 | local w = vim.api.nvim_win_get_width(0) 97 | local h = vim.api.nvim_win_get_height(0) 98 | local buf = vim.api.nvim_create_buf(true, false) 99 | local win = vim.api.nvim_open_win(buf, true, {{ 100 | relative = 'editor', 101 | width = {width}, 102 | height = {height}, 103 | row = {row}, 104 | col = {col} 105 | }}) 106 | vim.api.nvim_set_current_win(win) 107 | local winblend = vim.g.page_popup_winblend or 25 108 | vim.api.nvim_win_set_option(win, 'winblend', winblend) 109 | "} 110 | } else { 111 | 112 | let w_ratio = |s| format!("' .. tostring(math.floor(((w / 2) * 3) / {})) .. '", s + 1); 113 | let h_ratio = |s| format!("' .. tostring(math.floor(((h / 2) * 3) / {})) .. '", s + 1); 114 | 115 | let (a, b) = ("aboveleft", "belowright"); 116 | let (w, h) = ("winfixwidth", "winfixheight"); 117 | let (v, z) = ("vsplit", "split"); 118 | 119 | let (direction, size, split, fix); 120 | 121 | if opt.split_right != 0 { 122 | (direction = b, size = w_ratio(opt.split_right), split = v, fix = w) 123 | 124 | } else if opt.split_left != 0 { 125 | (direction = a, size = w_ratio(opt.split_left), split = v, fix = w) 126 | 127 | } else if opt.split_below != 0 { 128 | (direction = b, size = h_ratio(opt.split_below), split = z, fix = h) 129 | 130 | } else if opt.split_above != 0 { 131 | (direction = a, size = h_ratio(opt.split_above), split = z, fix = h) 132 | 133 | } else if let Some(split_right_cols) = opt.split_right_cols.map(|x| x.to_string()) { 134 | (direction = b, size = split_right_cols, split = v, fix = w) 135 | 136 | } else if let Some(split_left_cols) = opt.split_left_cols.map(|x| x.to_string()) { 137 | (direction = a, size = split_left_cols, split = v, fix = w) 138 | 139 | } else if let Some(split_below_rows) = opt.split_below_rows.map(|x| x.to_string()) { 140 | (direction = b, size = split_below_rows, split = z, fix = h) 141 | 142 | } else if let Some(split_above_rows) = opt.split_above_rows.map(|x| x.to_string()) { 143 | (direction = a, size = split_above_rows, split = z, fix = h) 144 | 145 | } else { 146 | unreachable!() 147 | }; 148 | 149 | formatdoc! {" 150 | local prev_win = vim.api.nvim_get_current_win() 151 | local w = vim.api.nvim_win_get_width(prev_win) 152 | local h = vim.api.nvim_win_get_height(prev_win) 153 | vim.cmd('{direction} {size}{split}') 154 | local buf = vim.api.nvim_create_buf(true, false) 155 | vim.api.nvim_set_current_buf(buf) 156 | local win = vim.api.nvim_get_current_win() 157 | vim.api.nvim_win_set_option(win, '{fix}', true) 158 | "} 159 | }; 160 | 161 | self.create_buffer(&cmd) 162 | .await 163 | .expect("Error when creating split output buffer") 164 | } 165 | 166 | 167 | async fn create_buffer( 168 | &mut self, 169 | window_open_cmd: &str 170 | ) -> Result { 171 | // Shell will be temporarily replaced with /bin/sleep to halt 172 | // for i32::MAX seconds or 68 years 173 | let cmd = formatdoc! {" 174 | local shell, shellcmdflag = vim.o.shell, vim.o.shellcmdflag 175 | vim.o.shell, vim.o.shellcmdflag = 'sleep', '' 176 | {window_open_cmd} 177 | local chan = vim.api.nvim_call_function('termopen', {{ '2147483647' }}) 178 | vim.o.shell, vim.o.shellcmdflag = shell, shellcmdflag 179 | local pty = vim.api.nvim_get_chan_info(chan).pty 180 | if pty == nil or pty == '' then 181 | error 'No PTY on channel' 182 | end 183 | return {{ buf, pty }} 184 | "}; 185 | log::trace!(target: "create buffer", "{cmd}"); 186 | 187 | let v = self.nvim 188 | .exec_lua(&cmd, vec![]) 189 | .await 190 | .expect("Cannot create buffer"); 191 | 192 | OutputBuffer::try_from((v, &self.nvim)) 193 | } 194 | 195 | 196 | pub async fn mark_buffer_as_instance( 197 | &mut self, 198 | buf: &Buffer, 199 | inst_name: &str, 200 | inst_pty_path: &str 201 | ) { 202 | let bv = buf.get_value(); 203 | log::trace!(target: "new instance", "{:?}->{inst_name}->{inst_pty_path}", bv); 204 | 205 | let v = Value::from(vec![ 206 | Value::from(inst_name), 207 | Value::from(inst_pty_path) 208 | ]); 209 | 210 | if let Err(e) = buf 211 | .set_var("page_instance", v) 212 | .await 213 | { 214 | log::error!(target: "new instance", "Error when setting instance mark: {e}"); 215 | } 216 | } 217 | 218 | 219 | pub async fn find_instance_buffer( 220 | &mut self, 221 | inst_name: &str 222 | ) -> Option { 223 | log::trace!(target: "find instance", "{inst_name}"); 224 | 225 | let value = self 226 | .on_instance(inst_name, "return { buf, pty_path }") 227 | .await 228 | .expect("Cannot find instance buffer"); 229 | 230 | if value.is_nil() { 231 | return None 232 | } 233 | 234 | let buf = OutputBuffer::try_from((value, &self.nvim)); 235 | if let Err(e) = &buf { 236 | log::error!(target: "find instance", "Wrong response: {e}"); 237 | } 238 | 239 | buf.ok() 240 | } 241 | 242 | 243 | pub async fn close_instance_buffer(&mut self, inst_name: &str) { 244 | log::trace!(target: "close instance", "{inst_name}"); 245 | 246 | if let Err(e) = self 247 | .on_instance(inst_name, "vim.api.nvim_buf_delete(buf, {{ force = true }})") 248 | .await 249 | { 250 | log::error!( 251 | target: "close instance", 252 | "Error closing instance buffer: {inst_name}, {e}" 253 | ); 254 | } 255 | } 256 | 257 | 258 | pub async fn focus_instance_buffer(&mut self, inst_name: &str) { 259 | log::trace!(target: "focus instance", "{inst_name}"); 260 | 261 | let cmd = indoc! {" 262 | local active_buf = vim.api.nvim_get_current_buf() 263 | if active_buf == buf then 264 | return 265 | end 266 | for _, win in ipairs(vim.api.nvim_list_wins()) do 267 | local win_buf = vim.api.nvim_win_get_buf(win) 268 | if win_buf == buf then 269 | vim.api.nvim_set_current_win(win) 270 | return 271 | end 272 | end 273 | vim.api.nvim_set_current_buf(buf) 274 | "}; 275 | 276 | self.on_instance(inst_name, cmd) 277 | .await 278 | .expect("Cannot focus on instance buffer"); 279 | } 280 | 281 | 282 | async fn on_instance( 283 | &mut self, 284 | inst_name: &str, 285 | action: &str 286 | ) -> Result> { 287 | let cmd = formatdoc! {" 288 | for _, buf in ipairs(vim.api.nvim_list_bufs()) do 289 | local inst_name, pty_path 290 | local ok = pcall(function() 291 | local inst_val = vim.api.nvim_buf_get_var(buf, 'page_instance') 292 | inst_name, pty_path = unpack(inst_val) 293 | end) 294 | if ok and inst_name == '{inst_name}' then 295 | {action} 296 | end 297 | end 298 | "}; 299 | 300 | self.nvim 301 | .exec_lua(&cmd, vec![]) 302 | .await 303 | } 304 | 305 | 306 | pub async fn update_buffer_title( 307 | &mut self, 308 | buf: &Buffer, 309 | buf_title: &str 310 | ) { 311 | let bn = buf 312 | .get_number() 313 | .await; 314 | log::trace!(target: "update title", "{bn:?} => {buf_title}"); 315 | 316 | let numbered_names = (1..99) 317 | .map(|attempt_nr| format!("{buf_title}({attempt_nr})")); 318 | 319 | for name in std::iter::once(buf_title.to_string()) 320 | .chain(numbered_names) 321 | { 322 | if let Err(e) = buf 323 | .set_name(&name) 324 | .await 325 | { 326 | log::trace!( 327 | target: "update title", 328 | "{bn:?} => {buf_title}: {:?}", e.to_string() 329 | ); 330 | 331 | use CallError::NeovimError; 332 | match *e { 333 | NeovimError(_, m) if m == "Failed to rename buffer" => {} 334 | _ => { 335 | log::error!(target: "update title", "Cannot update title: {e}"); 336 | 337 | return 338 | } 339 | } 340 | } else { 341 | self.nvim 342 | .command("redraw!") // To update statusline 343 | .await 344 | .expect("Cannot redraw"); 345 | 346 | return 347 | } 348 | } 349 | 350 | log::error!(target: "update title", "Max attempts to rename buffer reached"); 351 | } 352 | 353 | 354 | pub async fn prepare_output_buffer( 355 | &mut self, 356 | initial_buf_nr: i64, 357 | cmds: OutputCommands 358 | ) { 359 | let OutputCommands { 360 | ft, 361 | edit, 362 | notify_closed, 363 | pre, 364 | cmd_provided_by_user, 365 | lua_provided_by_user, 366 | after 367 | } = cmds; 368 | 369 | let options = formatdoc! {r#" 370 | vim.b.page_alternate_bufnr = {initial_buf_nr} 371 | if vim.wo.scrolloff > 999 or vim.wo.scrolloff < 0 then 372 | vim.g.page_scrolloff_backup = 0 373 | else 374 | vim.g.page_scrolloff_backup = vim.wo.scrolloff 375 | end 376 | vim.bo.scrollback, vim.wo.scrolloff, vim.wo.signcolumn, vim.wo.number = 377 | 100000, 999, 'no', false 378 | {ft} 379 | {edit} 380 | vim.api.nvim_create_autocmd('BufEnter', {{ 381 | buffer = 0, 382 | callback = function() vim.wo.scrolloff = 999 end 383 | }}) 384 | vim.api.nvim_create_autocmd('BufLeave', {{ 385 | buffer = 0, 386 | callback = function() vim.wo.scrolloff = vim.g.page_scrolloff_backup end 387 | }}) 388 | {notify_closed} 389 | {pre} 390 | vim.api.nvim_exec_autocmds('User', {{ 391 | pattern = 'PageOpen' 392 | }}) 393 | vim.cmd 'redraw' 394 | {lua_provided_by_user} 395 | {cmd_provided_by_user} 396 | {after} 397 | "#}; 398 | log::trace!(target: "prepare output", "{options}"); 399 | 400 | if let Err(e) = self.nvim 401 | .exec_lua(&options, vec![]) 402 | .await 403 | { 404 | log::error!( 405 | target: "prepare output", 406 | "Unable to set page options, text might be displayed improperly: {e}" 407 | ); 408 | } 409 | } 410 | 411 | 412 | pub async fn execute_connect_autocmd_on_current_buffer(&mut self) { 413 | log::trace!(target: "au PageConnect", ""); 414 | 415 | let cmd = indoc! {" 416 | vim.api.nvim_exec_autocmds('User', { 417 | pattern = 'PageConnect', 418 | }) 419 | "}; 420 | if let Err(e) = self.nvim 421 | .exec_lua(cmd, vec![]) 422 | .await 423 | { 424 | log::error!(target: "au PageConnect", "Cannot execute PageConnect: {e}"); 425 | } 426 | } 427 | 428 | 429 | pub async fn execute_disconnect_autocmd_on_current_buffer(&mut self) { 430 | log::trace!(target: "au PageDisconnect", ""); 431 | 432 | let cmd = indoc! {" 433 | vim.api.nvim_exec_autocmds('User', { 434 | pattern = 'PageDisconnect', 435 | }) 436 | "}; 437 | if let Err(e) = self.nvim 438 | .exec_lua(cmd, vec![]) 439 | .await 440 | { 441 | log::error!(target: "au PageDisconnect", "Cannot execute PageDisconnect: {e}"); 442 | } 443 | } 444 | 445 | 446 | pub async fn execute_command_post(&mut self, cmd: &str) { 447 | log::trace!(target: "command post", "{cmd}"); 448 | 449 | if let Err(e) = self.nvim 450 | .command(cmd) 451 | .await 452 | { 453 | log::error!(target: "command post", "Cannot execute post command '{cmd}': {e}"); 454 | } 455 | } 456 | 457 | 458 | pub async fn execute_command_post_lua(&self, lua_expr: &str) { 459 | log::trace!(target: "command post lua", "{lua_expr}"); 460 | 461 | if let Err(e) = self.nvim 462 | .exec_lua(lua_expr, vec![]) 463 | .await 464 | { 465 | log::error!( 466 | target: "command post lua", 467 | "Cannot execute post lua command '{lua_expr}': {e}" 468 | ); 469 | } 470 | } 471 | 472 | 473 | pub async fn switch_to_window_and_buffer( 474 | &mut self, 475 | (win, buf): &(Window, Buffer) 476 | ) { 477 | let wn = win 478 | .get_number() 479 | .await; 480 | let bn = buf 481 | .get_number() 482 | .await; 483 | log::trace!(target: "set window and buffer", "Win:{wn:?} Buf:{bn:?}"); 484 | 485 | if let Err(e) = self.nvim 486 | .set_current_win(win) 487 | .await 488 | { 489 | log::error!(target: "set window and buffer", "Cannot switch to window: {e}"); 490 | } 491 | 492 | if let Err(e) = self.nvim 493 | .set_current_buf(buf) 494 | .await 495 | { 496 | log::error!(target: "set window and buffer", "Cannot switch to buffer: {e}"); 497 | } 498 | } 499 | 500 | 501 | pub async fn switch_to_buffer( 502 | &mut self, 503 | buf: &Buffer 504 | ) -> Result<(), Box> { 505 | log::trace!(target: "set buffer", "{:?}", buf.get_value()); 506 | 507 | self.nvim 508 | .set_current_buf(buf) 509 | .await 510 | } 511 | 512 | 513 | pub async fn set_current_buffer_insert_mode(&mut self) { 514 | log::trace!(target: "set INSERT", ""); 515 | 516 | // feedkeys fixes "can't enter normal mode from..." 517 | let cmd = indoc! {r#" 518 | local keys = vim.api 519 | .nvim_replace_termcodes('A', true, false, true) 520 | vim.api.nvim_feedkeys(keys, 'n', false) 521 | "#}; 522 | if let Err(e) = self.nvim 523 | .exec_lua(cmd, vec![]) 524 | .await 525 | { 526 | log::error!(target: "set INSERT", "Error when setting mode: {e}"); 527 | } 528 | } 529 | 530 | 531 | pub async fn set_current_buffer_follow_output_mode(&mut self) { 532 | log::trace!(target: "set FOLLOW", ""); 533 | 534 | let cmd = indoc! {r#" 535 | local keys = vim.api 536 | .nvim_replace_termcodes('G', true, false, true) 537 | vim.api.nvim_feedkeys(keys, 'n', false) 538 | "#}; 539 | if let Err(e) = self.nvim 540 | .exec_lua(cmd, vec![]) 541 | .await 542 | { 543 | log::error!(target: "set FOLLOW", "Error when setting mode: {e}"); 544 | } 545 | } 546 | 547 | 548 | pub async fn set_current_buffer_scroll_mode(&mut self) { 549 | log::trace!(target: "set SCROLL", ""); 550 | 551 | let cmd = indoc! {r#" 552 | local keys = vim.api 553 | .nvim_replace_termcodes('ggM', true, false, true) 554 | vim.api.nvim_feedkeys(keys, 'n', false) 555 | "#}; 556 | if let Err(e) = self.nvim 557 | .exec_lua(cmd, vec![]) 558 | .await 559 | { 560 | log::error!(target: "set SCROLL", "Error when setting mode: {e}"); 561 | } 562 | } 563 | 564 | 565 | pub async fn open_file_buffer( 566 | &mut self, 567 | file_opt: &str, 568 | ) -> Result<(), Box> { 569 | log::trace!(target: "open file", "{file_opt:?}"); 570 | 571 | self.nvim 572 | .command(&format!("e {}", file_opt)) 573 | .await?; 574 | 575 | Ok(()) 576 | } 577 | 578 | 579 | pub async fn notify_query_finished(&mut self, lines_read_count: usize) { 580 | log::trace!(target: "query finished", "Read {lines_read_count} lines"); 581 | 582 | let cmd = formatdoc! {" 583 | vim.cmd 'redraw' 584 | local msg = '-- [PAGE] {lines_read_count} lines read; has more --' 585 | vim.api.nvim_echo({{ {{ msg, 'Comment', }}, }}, false, {{}}) 586 | "}; 587 | 588 | self.nvim 589 | .exec_lua(&cmd, vec![]) 590 | .await 591 | .expect("Cannot notify query finished"); 592 | } 593 | 594 | 595 | pub async fn notify_end_of_input(&mut self) { 596 | log::trace!(target: "end input", ""); 597 | 598 | let cmd = indoc! {" 599 | vim.cmd 'redraw' 600 | local msg = '-- [PAGE] end of input --' 601 | vim.api.nvim_echo({{ msg, 'Comment' }, }, false, {}) 602 | "}; 603 | 604 | self.nvim 605 | .exec_lua(cmd, vec![]) 606 | .await 607 | .expect("Cannot notify end of input"); 608 | } 609 | 610 | 611 | pub async fn get_var_or( 612 | &mut self, 613 | key: &str, 614 | default: &str 615 | ) -> String { 616 | let val = self.nvim 617 | .get_var(key) 618 | .await 619 | .map(|v| v.to_string()); 620 | 621 | log::trace!(target: "get var", "Key '{key}': '{val:?}'"); 622 | 623 | val.unwrap_or_else(|e| { 624 | use CallError::NeovimError; 625 | match *e { 626 | NeovimError(_, m) if m == format!("Key not found: {key}") => {}, 627 | 628 | _ => { 629 | log::error!( 630 | target: "get var", 631 | "Error getting var: {key}, {e}" 632 | ); 633 | } 634 | } 635 | 636 | String::from(default) 637 | }) 638 | } 639 | } 640 | 641 | 642 | /// This struct holds output buffer together with path to its PTY 643 | pub struct OutputBuffer { 644 | pub buf: Buffer, 645 | pub pty_path: PathBuf, 646 | } 647 | 648 | impl TryFrom<(Value, &Neovim)> for OutputBuffer { 649 | type Error = String; 650 | 651 | fn try_from( 652 | (val, nvim): (Value, &Neovim) 653 | ) -> Result { 654 | let tup = val 655 | .as_array() 656 | .ok_or("Response is not an array")?; 657 | let buf_val = tup 658 | .get(0) 659 | .ok_or("No buf handle")?; 660 | let pty_val = tup 661 | .get(1) 662 | .ok_or("No pty handle")? 663 | .as_str() 664 | .ok_or("PTY not a string")?; 665 | 666 | let buf = Buffer::new(buf_val.clone(), nvim.clone()); 667 | let pty_path = PathBuf::from(pty_val); 668 | 669 | Ok(OutputBuffer { buf, pty_path }) 670 | } 671 | } 672 | 673 | 674 | /// This struct provides commands that 675 | /// would be run on output buffer after creation 676 | pub struct OutputCommands { 677 | edit: String, 678 | ft: String, 679 | notify_closed: String, 680 | pre: String, 681 | cmd_provided_by_user: String, 682 | lua_provided_by_user: String, 683 | after: String, 684 | } 685 | 686 | impl OutputCommands { 687 | fn create_with( 688 | cmd_provided_by_user: &str, 689 | lua_provided_by_user: &str, 690 | writeable: bool, 691 | ) -> OutputCommands { 692 | let mut cmd_provided_by_user = String::from(cmd_provided_by_user); 693 | if !cmd_provided_by_user.is_empty() { 694 | cmd_provided_by_user = format!("vim.cmd [====[{cmd_provided_by_user}]====]"); 695 | } 696 | 697 | let lua_provided_by_user = String::from(lua_provided_by_user); 698 | 699 | let mut edit = String::new(); 700 | if !writeable { 701 | let cmd = indoc! {r#" 702 | vim.bo.modifiable = false 703 | 704 | local function page_echo_notification(message) 705 | vim.defer_fn(function() 706 | local msg = '-- [PAGE] ' .. message .. ' --' 707 | vim.api.nvim_echo({{ msg, 'Comment' }, }, false, {}) 708 | vim.api.nvim_create_autocmd('CursorMoved', { 709 | buffer = 0, 710 | once = true, 711 | command = 'echo' 712 | }) 713 | end, 64) 714 | end 715 | 716 | local function page_scroll_text_bound(top, message, movement) 717 | local row, col, search 718 | if top then 719 | row, col, search = 1, 1, { '\\S', 'c' } 720 | else 721 | row, col, search = 9999999999, 9999999999, { '\\S', 'bc' } 722 | end 723 | vim.api.nvim_call_function('cursor', { row, col }) 724 | vim.api.nvim_call_function('search', search) 725 | if movement ~= nil then 726 | movement() 727 | end 728 | page_echo_notification(message) 729 | end 730 | 731 | local function page_scroll(top, message) 732 | vim.wo.scrolloff = 0 733 | local movement 734 | if top then 735 | local key = vim.api 736 | .nvim_replace_termcodes('zM', true, false, true) 737 | movement = function() 738 | vim.api.nvim_feedkeys(key, 'nx', false) 739 | end 740 | else 741 | movement = function() 742 | vim.api.nvim_feedkeys('z-M', 'nx', false) 743 | end 744 | end 745 | page_scroll_text_bound(top, message, movement) 746 | vim.wo.scrolloff = 999 747 | end 748 | 749 | local function page_close() 750 | local buf = vim.api.nvim_get_current_buf() 751 | if buf ~= vim.b.page_alternate_bufnr and 752 | vim.api.nvim_buf_is_loaded(vim.b.page_alternate_bufnr) 753 | then 754 | vim.api.nvim_set_current_buf(vim.b.page_alternate_bufnr) 755 | end 756 | vim.api.nvim_buf_delete(buf, { force = true }) 757 | local exit = true 758 | for _, b in ipairs(vim.api.nvim_list_bufs()) do 759 | local bt = vim.api.nvim_buf_get_option(b, 'buftype') 760 | if bt == '' or 761 | bt == 'acwrite' or 762 | bt == 'terminal' or 763 | bt == 'prompt' 764 | then 765 | local bm = vim.api.nvim_buf_get_option(b, 'modified') 766 | if bm then 767 | exit = false 768 | break 769 | end 770 | local bl = vim.api.nvim_buf_get_lines(b, 0, -1, false) 771 | if #bl ~= 0 and bl[1] ~= '' and #bl > 1 then 772 | exit = false 773 | break 774 | end 775 | end 776 | end 777 | if exit then 778 | vim.cmd 'qa!' 779 | end 780 | end 781 | 782 | local function page_map(key, rhs) 783 | vim.keymap.set('n', key, rhs, { nowait = true, buffer = 0 }) 784 | end 785 | 786 | page_map('I', function() 787 | page_scroll(true, 'in the beginning of scroll') 788 | end) 789 | page_map('A', function() 790 | page_scroll(false, 'at the end of scroll') 791 | end) 792 | page_map('i', function() 793 | page_scroll_text_bound(true, 'in the beginning') 794 | end) 795 | page_map('a', function() 796 | page_scroll_text_bound(false, 'at the end') 797 | end) 798 | page_map('q', page_close) 799 | page_map('u', '') 800 | page_map('d', '') 801 | page_map('x', 'G') 802 | "#}; 803 | 804 | edit += cmd; 805 | } 806 | 807 | OutputCommands { 808 | ft: String::new(), 809 | pre: String::new(), 810 | after: String::new(), 811 | notify_closed: String::new(), 812 | edit, 813 | cmd_provided_by_user, 814 | lua_provided_by_user, 815 | } 816 | } 817 | 818 | 819 | pub fn for_file_buffer( 820 | cmd_provided_by_user: &str, 821 | lua_provided_by_user: &str, 822 | writeable: bool 823 | ) -> OutputCommands { 824 | let mut cmds = Self::create_with( 825 | cmd_provided_by_user, 826 | lua_provided_by_user, 827 | writeable 828 | ); 829 | 830 | let cmd = indoc! {" 831 | vim.api.nvim_exec_autocmds('User', { 832 | pattern = 'PageOpenFile', 833 | }) 834 | "}; 835 | 836 | cmds.after += cmd; 837 | cmds 838 | } 839 | 840 | 841 | pub fn for_output_buffer( 842 | page_id: u128, 843 | channel: u128, 844 | query_lines_count: usize, 845 | opt: &crate::cli::OutputOptions 846 | ) -> OutputCommands { 847 | let cmd_provided_by_user = opt.command 848 | .as_deref() 849 | .unwrap_or_default(); 850 | let lua_provided_by_user = opt.lua 851 | .as_deref() 852 | .unwrap_or_default(); 853 | 854 | let mut cmds = Self::create_with( 855 | cmd_provided_by_user, 856 | lua_provided_by_user, 857 | opt.writable 858 | ); 859 | 860 | let ft = &opt.filetype; 861 | cmds.ft = format!("vim.bo.filetype = '{ft}'"); 862 | 863 | cmds.notify_closed = formatdoc! {r#" 864 | vim.api.nvim_create_autocmd('BufDelete', {{ 865 | buffer = 0, 866 | callback = function() 867 | pcall(function() 868 | vim.rpcnotify({channel}, 'page_buffer_closed', '{page_id}') 869 | end) 870 | end 871 | }}) 872 | "#}; 873 | 874 | if query_lines_count != 0 { 875 | 876 | let prefix = cmds.pre; 877 | cmds.pre = formatdoc! {r#" 878 | {prefix} 879 | vim.b.page_query_size = {query_lines_count} 880 | local function fetch_lines(opt) 881 | local ok = pcall(function() 882 | vim.rpcnotify({channel}, 'page_fetch_lines', '{page_id}', opt.args) 883 | end) 884 | if not ok then 885 | page_echo_notification 'closed' 886 | end 887 | end 888 | local function define_query_cmd() 889 | local cmd_opts = {{ force = true, nargs = '?' }} 890 | vim.api.nvim_create_user_command('Page', fetch_lines, cmd_opts) 891 | end 892 | define_query_cmd() 893 | vim.api.nvim_create_autocmd('BufEnter', {{ 894 | buffer = 0, 895 | callback = define_query_cmd, 896 | }}) 897 | "#}; 898 | 899 | if !opt.writable { 900 | 901 | let prefix = cmds.pre; 902 | cmds.pre = formatdoc! {r#" 903 | {prefix} 904 | page_map('r', function() 905 | fetch_lines {{ args = vim.b.page_query_size * vim.v.count1 }} 906 | end) 907 | page_map('R', function() 908 | fetch_lines {{ args = 9999 }} 909 | end) 910 | "#}; 911 | } 912 | } 913 | 914 | if opt.pwd { 915 | let pwd = std::env::var("PWD") 916 | .unwrap(); 917 | 918 | let prefix = cmds.pre; 919 | cmds.pre = formatdoc! {r#" 920 | {prefix} 921 | vim.b.page_lcd_backup = getcwd() 922 | vim.cmd 'lcd {pwd}' 923 | vim.api.nvim_create_autocmd('BufEnter', {{ 924 | buffer = 0, 925 | command = 'lcd {pwd}' 926 | }}) 927 | vim.api.nvim_create_autocmd('BufLeave', {{ 928 | buffer = 0, 929 | command = 'exe "lcd" . b:page_lcd_backup' 930 | }}) 931 | "#}; 932 | } 933 | 934 | cmds 935 | } 936 | } 937 | -------------------------------------------------------------------------------- /src/picker/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{ 2 | Parser, 3 | ArgGroup, 4 | ArgAction, 5 | ValueHint, 6 | }; 7 | 8 | /// File picker for neovim inspired by neovim-remote 9 | #[derive(Parser, Debug)] 10 | #[clap( 11 | author, 12 | disable_help_subcommand = true, 13 | allow_negative_numbers = true, 14 | args_override_self = true, 15 | group = splits_arg_group(), 16 | group = back_arg_group(), 17 | group = follow_arg_group(), 18 | group = command_only_arg_group(), 19 | )] 20 | pub struct Options { 21 | /// Open provided files as editable 22 | /// [if none provided nv opens last modified file in currend directory] 23 | #[clap(name="FILE", value_hint=ValueHint::FilePath)] 24 | pub files: Vec, 25 | 26 | /// Open non-text files including directories, binaries, images etc 27 | #[clap(display_order=1, short='o')] 28 | pub open_non_text: bool, 29 | 30 | /// Ignoring [FILE] open all text files in the current directory 31 | /// and recursively open all text files in its subdirectories 32 | /// [0: disabled and default; 33 | /// empty: defaults to 1 and implied if no provided; 34 | /// : also opens in subdirectories at this level of depth] 35 | #[clap(short='O')] 36 | pub recurse_depth: Option>, 37 | 38 | /// Open in `page` instead (just postfix shortcut) {n} 39 | /// ~ ~ ~ 40 | #[clap(short='v')] 41 | pub view_only: bool, 42 | 43 | /// Open each [FILE] at last line 44 | #[clap(short='f')] 45 | pub follow: bool, 46 | 47 | /// Open and search for a specified 48 | #[clap(short='p')] 49 | pub pattern: Option, 50 | 51 | /// Open and search backwars for a specified 52 | #[clap(short='P')] 53 | pub pattern_backwards: Option, 54 | 55 | /// Return back to current buffer 56 | #[clap(short='b')] 57 | pub back: bool, 58 | 59 | /// Return back to current buffer and enter into INSERT/TERMINAL mode 60 | #[clap(short='B')] 61 | pub back_restore: bool, 62 | 63 | /// Keep `nv` process until buffer is closed 64 | /// (for editing git commit message) 65 | #[clap(short='k')] 66 | pub keep: bool, 67 | 68 | /// Keep `nv` process until first write occur, 69 | /// then close buffer and neovim if it was spawned by `nv` {n} 70 | /// ~ ~ ~ 71 | #[clap(short='K')] 72 | pub keep_until_write: bool, 73 | 74 | /// TCP/IP socket address or path to named pipe listened 75 | /// by running host neovim process 76 | #[clap(short='a', env="NVIM")] 77 | pub address: Option, 78 | 79 | /// Arguments that will be passed to child neovim process 80 | /// spawned when
is missing 81 | #[clap(short='A', env="NVIM_PAGE_PICKER_ARGS")] 82 | pub arguments: Option, 83 | 84 | /// Config that will be used by child neovim process spawned 85 | /// when
is missing [file: $XDG_CONFIG_HOME/page/init.vim] 86 | #[clap(short='c', value_hint=ValueHint::AnyPath)] 87 | pub config: Option, 88 | 89 | /// Override filetype on each [FILE] buffer 90 | /// (to enable custom syntax highlighting) [text: default] {n} 91 | /// ~ ~ ~ 92 | #[clap(short='t')] 93 | pub filetype: Option, 94 | 95 | /// Run command on each [FILE] buffer after it was created 96 | #[clap(short='e')] 97 | pub command: Option, 98 | 99 | /// Run lua expr on each [FILE] buffer after it was created 100 | #[clap(long="e")] 101 | pub lua: Option, 102 | 103 | /// Just run command with ignoring all other options 104 | #[clap(short='x')] 105 | pub command_only: Option, 106 | 107 | /// Just run lua expr with ignoring all other options {n} 108 | /// ~ ~ ~ 109 | #[clap(long="x")] 110 | pub lua_only: Option, 111 | 112 | #[clap(flatten)] 113 | pub split: SplitOptions, 114 | } 115 | 116 | impl Options { 117 | pub fn is_split_implied(&self) -> bool { 118 | self.split.split_left_cols.is_some() || 119 | self.split.split_right_cols.is_some() || 120 | self.split.split_above_rows.is_some() || 121 | self.split.split_below_rows.is_some() || 122 | self.split.split_left > 0u8 || 123 | self.split.split_right > 0u8 || 124 | self.split.split_above > 0u8 || 125 | self.split.split_below > 0u8 126 | } 127 | } 128 | 129 | 130 | // Options for split 131 | #[derive(Parser, Debug)] 132 | pub struct SplitOptions { 133 | /// Split left with ratio: window_width * 3 / ( + 1) 134 | #[clap(display_order=900, short='l', action=ArgAction::Count)] 135 | pub split_left: u8, 136 | 137 | /// Split right with ratio: window_width * 3 / ( + 1) 138 | #[clap(display_order=901, short='r', action=ArgAction::Count)] 139 | pub split_right: u8, 140 | 141 | /// Split above with ratio: window_height * 3 / ( + 1) 142 | #[clap(display_order=902, short='u', action=ArgAction::Count)] 143 | pub split_above: u8, 144 | 145 | /// Split below with ratio: window_height * 3 / ( + 1) 146 | #[clap(display_order=903, short='d', action=ArgAction::Count)] 147 | pub split_below: u8, 148 | 149 | /// Split left and resize to columns 150 | #[clap(display_order=904, short='L')] 151 | pub split_left_cols: Option, 152 | 153 | /// Split right and resize to columns 154 | #[clap(display_order=905, short='R')] 155 | pub split_right_cols: Option, 156 | 157 | /// Split above and resize to rows 158 | #[clap(display_order=906, short='U')] 159 | pub split_above_rows: Option, 160 | 161 | /// Split below and resize to rows {n} 162 | /// ^ 163 | #[clap(display_order=907, short='D')] 164 | pub split_below_rows: Option, 165 | 166 | /// With any of -r -l -u -d -R -L -U -D open floating window instead of split 167 | /// [to not overwrite data in the current terminal] {n} 168 | /// ~ ~ ~ 169 | #[clap(display_order=908, short='+')] 170 | pub popup: bool, 171 | } 172 | 173 | 174 | 175 | fn back_arg_group() -> ArgGroup { 176 | ArgGroup::new("focusing") 177 | .args(["back", "back_restore"]) 178 | .multiple(false) 179 | } 180 | 181 | fn command_only_arg_group() -> ArgGroup { 182 | ArgGroup::new("commands") 183 | .args(["command_only", "lua_only"]) 184 | .multiple(false) 185 | } 186 | 187 | fn follow_arg_group() -> ArgGroup { 188 | ArgGroup::new("movement") 189 | .args(["follow", "pattern", "pattern_backwards"]) 190 | .multiple(false) 191 | } 192 | 193 | fn splits_arg_group() -> ArgGroup { 194 | ArgGroup::new("splits") 195 | .args([ 196 | "split_left", 197 | "split_right", 198 | "split_above", 199 | "split_below" 200 | ]) 201 | .args([ 202 | "split_left_cols", 203 | "split_right_cols", 204 | "split_above_rows", 205 | "split_below_rows" 206 | ]) 207 | .multiple(false) 208 | } 209 | 210 | 211 | pub fn get_options() -> Options { 212 | Options::parse() 213 | } 214 | 215 | 216 | #[derive(Debug, Clone)] 217 | pub enum FileOption { 218 | Uri(String), 219 | Path(String), 220 | } 221 | 222 | impl From<&std::ffi::OsStr> for FileOption { 223 | fn from(value: &std::ffi::OsStr) -> Self { 224 | let s = value.to_string_lossy(); 225 | let mut chars = s.chars(); 226 | 227 | loop { 228 | match chars.next() { 229 | Some('+' | '-' | '.') => continue, 230 | 231 | Some(c) if c.is_alphanumeric() => continue, 232 | 233 | Some(c) if c == ':' && 234 | matches!(chars.next(), Some('/')) && 235 | matches!(chars.next(), Some('/')) => 236 | 237 | return FileOption::Uri(String::from(s)), 238 | 239 | _ => {} 240 | } 241 | 242 | return FileOption::Path(String::from(s)) 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/picker/context.rs: -------------------------------------------------------------------------------- 1 | pub use env_context::Env; 2 | 3 | pub mod env_context { 4 | 5 | #[derive(Debug)] 6 | pub struct Env { 7 | pub opt: crate::cli::Options, 8 | pub files_usage: FilesUsage, 9 | pub tmp_dir: std::path::PathBuf, 10 | pub page_id: u128, 11 | pub read_stdin_usage: ReadStdinUsage, 12 | pub split_usage: SplitUsage 13 | } 14 | 15 | pub fn enter() -> Env { 16 | let mut opt = crate::cli::get_options(); 17 | 18 | // Fallback for neovim < 8.0 which don't uses $NVIM 19 | if opt.address.is_none() { 20 | if let Ok(address) = std::env::var("NVIM_LISTEN_ADDRESS") { 21 | opt.address.replace(address); 22 | } 23 | } 24 | 25 | // Treat empty -a value as if it wasn't provided 26 | if opt.address.as_deref().map_or(false, str::is_empty) { 27 | opt.address = None; 28 | } 29 | 30 | let input_from_pipe = !atty::is(atty::Stream::Stdin); 31 | 32 | let mut files_usage = FilesUsage::FilesProvided; 33 | if opt.files.is_empty() && !input_from_pipe { 34 | files_usage = FilesUsage::LastModifiedFile; 35 | } 36 | let recurse_depth = match opt.recurse_depth { 37 | Some(Some(n)) => n, 38 | Some(None) => 1, 39 | None => 0, 40 | }; 41 | if recurse_depth > 0 { 42 | files_usage = FilesUsage::RecursiveCurrentDir { recurse_depth } 43 | } 44 | 45 | let tmp_dir = { 46 | let d = std::env::temp_dir() 47 | .join("neovim-page"); 48 | std::fs::create_dir_all(&d) 49 | .expect("Cannot create temporary directory for page"); 50 | d 51 | }; 52 | 53 | let pipe_path = { 54 | // This should provide enough entropy for current use case 55 | std::time::UNIX_EPOCH 56 | .elapsed() 57 | .unwrap() 58 | .as_nanos() 59 | }; 60 | 61 | let mut split_usage = SplitUsage::Disabled; 62 | if opt.address.is_some() && opt.is_split_implied() { 63 | split_usage = SplitUsage::Enabled; 64 | } 65 | 66 | let mut pipe_buf_usage = ReadStdinUsage::Disabled; 67 | if input_from_pipe { 68 | pipe_buf_usage = ReadStdinUsage::Enabled; 69 | } 70 | 71 | Env { 72 | opt, 73 | files_usage, 74 | tmp_dir, 75 | page_id: pipe_path, 76 | read_stdin_usage: pipe_buf_usage, 77 | split_usage, 78 | } 79 | } 80 | 81 | #[derive(Debug)] 82 | pub enum FilesUsage { 83 | RecursiveCurrentDir { 84 | recurse_depth: usize, 85 | }, 86 | LastModifiedFile, 87 | FilesProvided, 88 | } 89 | 90 | #[derive(Debug)] 91 | pub enum ReadStdinUsage { 92 | Enabled, 93 | Disabled 94 | } 95 | 96 | #[derive(Debug)] 97 | pub enum SplitUsage { 98 | Enabled, 99 | Disabled 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/picker/main.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod cli; 2 | pub(crate) mod context; 3 | 4 | pub type NeovimConnection = connection::NeovimConnection>; 5 | pub type NeovimBuffer = connection::Buffer; 6 | 7 | 8 | #[tokio::main(worker_threads=2)] 9 | async fn main() { 10 | connection::init_logger(); 11 | 12 | let env_ctx = context::env_context::enter(); 13 | 14 | main::warn_if_incompatible_options(&env_ctx.opt); 15 | 16 | redirect_to_page(env_ctx).await; 17 | } 18 | 19 | mod main { 20 | // Some options takes effect only when page would be 21 | // spawned from neovim's terminal 22 | pub fn warn_if_incompatible_options(opt: &crate::cli::Options) { 23 | if opt.address.is_some() { 24 | return 25 | } 26 | 27 | if opt.is_split_implied() { 28 | log::warn!( 29 | target: "usage", 30 | "Split (-r -l -u -d -R -L -U -D) is ignored \ 31 | if address (-a or $NVIM) isn't set" 32 | ); 33 | } 34 | if opt.back || opt.back_restore { 35 | log::warn!( 36 | target: "usage", 37 | "Switch back (-b -B) is ignored \ 38 | if address (-a or $NVIM) isn't set" 39 | ); 40 | } 41 | } 42 | } 43 | 44 | 45 | async fn redirect_to_page(env_ctx: context::Env) { 46 | if env_ctx.opt.view_only { 47 | let mut page_args = std::env::args(); 48 | page_args.next(); // skip `nv` 49 | let page_args = page_args 50 | .filter(|arg| arg != "-v"); 51 | 52 | let exit_code = std::process::Command::new("page") 53 | .args(page_args) 54 | .spawn() 55 | .expect("Cannot spawn `page`") 56 | .wait() 57 | .expect("`page` died unexpectedly") 58 | .code() 59 | .unwrap_or(0); 60 | 61 | std::process::exit(exit_code) 62 | } 63 | 64 | connect_neovim(env_ctx).await; 65 | } 66 | 67 | 68 | async fn connect_neovim(env_ctx: context::Env) { 69 | log::info!(target: "context", "{env_ctx:#?}"); 70 | 71 | connection::init_panic_hook(); 72 | 73 | let mut nvim_conn: NeovimConnection = connection::open( 74 | &env_ctx.tmp_dir, 75 | env_ctx.page_id, 76 | &env_ctx.opt.address, 77 | &env_ctx.opt.config, 78 | &env_ctx.opt.config, 79 | false 80 | ).await; 81 | 82 | if let Some(cmd) = &env_ctx.opt.command_only { 83 | nvim_conn.nvim_actions 84 | .command(cmd) 85 | .await 86 | .expect("Cannot spawn cmd only"); 87 | 88 | connection::close_and_exit(&mut nvim_conn).await; 89 | 90 | } else if let Some(lua) = &env_ctx.opt.lua_only { 91 | nvim_conn.nvim_actions 92 | .exec_lua(lua, vec![]) 93 | .await 94 | .expect("Cannot spawn lua only"); 95 | 96 | connection::close_and_exit(&mut nvim_conn).await; 97 | }; 98 | 99 | split_current_buffer(env_ctx, nvim_conn).await; 100 | } 101 | 102 | 103 | async fn split_current_buffer(env_ctx: context::Env, conn: NeovimConnection) { 104 | use context::env_context::SplitUsage; 105 | if let SplitUsage::Enabled = env_ctx.split_usage { 106 | let cmd = split_current_buffer::create_split_command(&env_ctx.opt.split); 107 | 108 | log::info!(target: "split_current_buffer", "{cmd}"); 109 | 110 | conn.nvim_actions 111 | .exec_lua(&cmd, vec![]) 112 | .await 113 | .expect("Cannot create split window"); 114 | } 115 | 116 | read_stdin(env_ctx, conn).await; 117 | } 118 | 119 | mod split_current_buffer { 120 | /// This is almost copy-paste from pager/neovim.rs 121 | pub fn create_split_command( 122 | opt: &crate::cli::SplitOptions 123 | ) -> String { 124 | if opt.popup { 125 | 126 | let w_ratio = |s| format!("math.floor(((w / 2) * 3) / {})", s + 1); 127 | let h_ratio = |s| format!("math.floor(((h / 2) * 3) / {})", s + 1); 128 | 129 | let (w, h, o) = ("w".to_string(), "h".to_string(), "0".to_string()); 130 | 131 | let (width, height, row, col); 132 | 133 | if opt.split_right != 0 { 134 | (width = w_ratio(opt.split_right), height = h, row = &o, col = &w) 135 | 136 | } else if opt.split_left != 0 { 137 | (width = w_ratio(opt.split_left), height = h, row = &o, col = &o) 138 | 139 | } else if opt.split_below != 0 { 140 | (width = w, height = h_ratio(opt.split_below), row = &h, col = &o) 141 | 142 | } else if opt.split_above != 0 { 143 | (width = w, height = h_ratio(opt.split_above), row = &o, col = &o) 144 | 145 | } else if let Some(split_right_cols) = opt.split_right_cols.map(|x| x.to_string()) { 146 | (width = split_right_cols, height = h, row = &o, col = &w) 147 | 148 | } else if let Some(split_left_cols) = opt.split_left_cols.map(|x| x.to_string()) { 149 | (width = split_left_cols, height = h, row = &o, col = &o) 150 | 151 | } else if let Some(split_below_rows) = opt.split_below_rows.map(|x| x.to_string()) { 152 | (width = w, height = split_below_rows, row = &h, col = &o) 153 | 154 | } else if let Some(split_above_rows) = opt.split_above_rows.map(|x| x.to_string()) { 155 | (width = w, height = split_above_rows, row = &o, col = &o) 156 | 157 | } else { 158 | unreachable!() 159 | }; 160 | 161 | indoc::formatdoc! {" 162 | local w = vim.api.nvim_win_get_width(0) 163 | local h = vim.api.nvim_win_get_height(0) 164 | local buf = vim.api.nvim_create_buf(true, false) 165 | local win = vim.api.nvim_open_win(buf, true, {{ 166 | relative = 'editor', 167 | width = {width}, 168 | height = {height}, 169 | row = {row}, 170 | col = {col} 171 | }}) 172 | vim.api.nvim_set_current_win(win) 173 | local winblend = vim.g.page_popup_winblend or 25 174 | vim.api.nvim_win_set_option(win, 'winblend', winblend) 175 | "} 176 | } else { 177 | 178 | let w_ratio = |s| format!("' .. tostring(math.floor(((w / 2) * 3) / {})) .. '", s + 1); 179 | let h_ratio = |s| format!("' .. tostring(math.floor(((h / 2) * 3) / {})) .. '", s + 1); 180 | 181 | let (a, b) = ("aboveleft", "belowright"); 182 | let (w, h) = ("winfixwidth", "winfixheight"); 183 | let (v, z) = ("vsplit", "split"); 184 | 185 | let (direction, size, split, fix); 186 | 187 | if opt.split_right != 0 { 188 | (direction = b, size = w_ratio(opt.split_right), split = v, fix = w) 189 | 190 | } else if opt.split_left != 0 { 191 | (direction = a, size = w_ratio(opt.split_left), split = v, fix = w) 192 | 193 | } else if opt.split_below != 0 { 194 | (direction = b, size = h_ratio(opt.split_below), split = z, fix = h) 195 | 196 | } else if opt.split_above != 0 { 197 | (direction = a, size = h_ratio(opt.split_above), split = z, fix = h) 198 | 199 | } else if let Some(split_right_cols) = opt.split_right_cols.map(|x| x.to_string()) { 200 | (direction = b, size = split_right_cols, split = v, fix = w) 201 | 202 | } else if let Some(split_left_cols) = opt.split_left_cols.map(|x| x.to_string()) { 203 | (direction = a, size = split_left_cols, split = v, fix = w) 204 | 205 | } else if let Some(split_below_rows) = opt.split_below_rows.map(|x| x.to_string()) { 206 | (direction = b, size = split_below_rows, split = z, fix = h) 207 | 208 | } else if let Some(split_above_rows) = opt.split_above_rows.map(|x| x.to_string()) { 209 | (direction = a, size = split_above_rows, split = z, fix = h) 210 | 211 | } else { 212 | unreachable!() 213 | }; 214 | 215 | indoc::formatdoc! {" 216 | local prev_win = vim.api.nvim_get_current_win() 217 | local w = vim.api.nvim_win_get_width(prev_win) 218 | local h = vim.api.nvim_win_get_height(prev_win) 219 | vim.cmd('{direction} {size}{split}') 220 | local buf = vim.api.nvim_create_buf(true, false) 221 | vim.api.nvim_set_current_buf(buf) 222 | local win = vim.api.nvim_get_current_win() 223 | vim.api.nvim_win_set_option(win, '{fix}', true) 224 | "} 225 | } 226 | } 227 | } 228 | 229 | 230 | async fn read_stdin(env_ctx: context::Env, conn: NeovimConnection) { 231 | use context::env_context::ReadStdinUsage; 232 | if let ReadStdinUsage::Enabled = &env_ctx.read_stdin_usage { 233 | log::info!(target: "read_stdin", ""); 234 | 235 | let buf = conn.nvim_actions 236 | .create_buf(true, true) 237 | .await 238 | .expect("Cannot create STDIN buffer"); 239 | 240 | conn.nvim_actions 241 | .set_current_buf(&buf) 242 | .await 243 | .expect("Cannot set current STDIN buffer"); 244 | 245 | let mut ln = Vec::with_capacity(512); 246 | let mut i = 0; 247 | 248 | for b in std::io::Read::bytes(std::io::stdin()) { 249 | match b { 250 | Err(e) => { 251 | panic!("Failed to prefetch line from stdin: {e:#?}") 252 | } 253 | Ok(_eol @ b'\n') => { 254 | ln.shrink_to_fit(); 255 | 256 | let ln_str = String::from_utf8(ln) 257 | .expect("Cannot read UTF8 string"); 258 | ln = Vec::with_capacity(512); 259 | 260 | buf.set_lines(i, i, false, vec![ln_str]) 261 | .await 262 | .expect("Cannot set line"); 263 | 264 | i += 1; 265 | } 266 | Ok(b) => { 267 | ln.push(b); 268 | } 269 | } 270 | } 271 | } 272 | 273 | open_files(env_ctx, conn).await; 274 | } 275 | 276 | 277 | async fn open_files(env_ctx: context::Env, mut conn: NeovimConnection) { 278 | use context::env_context::FilesUsage; 279 | match env_ctx.files_usage { 280 | 281 | FilesUsage::RecursiveCurrentDir { 282 | recurse_depth 283 | } => { 284 | log::info!(target: "recursive", ""); 285 | 286 | let read_dir = walkdir::WalkDir::new("./") 287 | .contents_first(true) 288 | .follow_links(false) 289 | .max_depth(recurse_depth); 290 | 291 | for f in read_dir { 292 | let f = match f { 293 | Err(e) => { 294 | log::error!("Error reading file: {e:#?}"); 295 | continue 296 | } 297 | Ok(f) => f 298 | }; 299 | 300 | if let Some(f) = open_files::FileToOpen::new_existed_file(f.path()) { 301 | if !f.is_text && !env_ctx.opt.open_non_text { 302 | continue 303 | } 304 | 305 | open_files::open_file(&mut conn, &env_ctx, &f.path_string).await; 306 | } 307 | } 308 | }, 309 | 310 | FilesUsage::LastModifiedFile => { 311 | log::info!(target: "last_modified", ""); 312 | 313 | let mut last_modified = None; 314 | 315 | let read_dir = std::fs::read_dir("./") 316 | .expect("Cannot read current directory"); 317 | 318 | for f in read_dir { 319 | let f = match f { 320 | Err(e) => { 321 | log::error!("Error reading last file: {e:#?}"); 322 | continue 323 | } 324 | Ok(f) => f 325 | }; 326 | 327 | if let Some(f) = open_files::FileToOpen::new_existed_file(f.path()) { 328 | if !f.is_text && !env_ctx.opt.open_non_text { 329 | continue; 330 | } 331 | 332 | let Ok(f_modified_time) = f.get_modified_time() else { 333 | log::error!( 334 | target: "last_modified", 335 | "Cannot read metadata: {}", 336 | f.path_string 337 | ); 338 | 339 | continue; 340 | }; 341 | 342 | if let Some((l_modified_time, l_modified)) = last_modified.as_mut() { 343 | if *l_modified_time < f_modified_time { 344 | (*l_modified_time, *l_modified) = (f_modified_time, f); 345 | } 346 | } else { 347 | last_modified.replace((f_modified_time, f)); 348 | } 349 | } 350 | } 351 | 352 | if let Some((_, f)) = last_modified { 353 | log::trace!(target: "last_modified", "{}", f.path_string); 354 | 355 | open_files::open_file(&mut conn, &env_ctx, &f.path_string).await; 356 | } 357 | }, 358 | 359 | FilesUsage::FilesProvided => { 360 | log::info!(target: "files_provided", "{}", env_ctx.opt.files.len()); 361 | 362 | for f in &env_ctx.opt.files { 363 | if let Some(f) = open_files::FileToOpen::new_maybe_uri(f) { 364 | if !f.is_text && !env_ctx.opt.open_non_text { 365 | continue 366 | } 367 | 368 | open_files::open_file(&mut conn, &env_ctx, &f.path_string).await; 369 | } 370 | } 371 | } 372 | } 373 | 374 | exit_from_neovim(env_ctx, conn).await; 375 | } 376 | 377 | mod open_files { 378 | use std::{path::{PathBuf, Path}, time::SystemTime}; 379 | use crate::{ 380 | cli::FileOption, 381 | context::Env, 382 | }; 383 | 384 | use once_cell::sync::Lazy; 385 | static PWD: Lazy = Lazy::new(|| { 386 | PathBuf::from( 387 | std::env::var("PWD") 388 | .expect("Cannot read $PWD value") 389 | ) 390 | }); 391 | 392 | pub struct FileToOpen { 393 | pub path: PathBuf, 394 | pub path_string: String, 395 | pub is_text: bool, 396 | } 397 | 398 | impl FileToOpen { 399 | pub fn new_existed_file + std::fmt::Debug>(path: P) -> Option { 400 | let path = match std::fs::canonicalize(&path) { 401 | Ok(canonical) => canonical, 402 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => { 403 | PWD.join(path.as_ref()) 404 | } 405 | Err(e) => { 406 | log::error!( 407 | target: "open file", 408 | "cannot open {path:?}: {e}" 409 | ); 410 | return None; 411 | } 412 | }; 413 | let path_string = path 414 | .to_string_lossy() 415 | .to_string(); 416 | let is_text = is_text_file(&path_string); 417 | let f = FileToOpen { 418 | path, 419 | path_string, 420 | is_text 421 | }; 422 | Some(f) 423 | } 424 | 425 | pub fn new_maybe_uri(path: &FileOption) -> Option { 426 | let f = match path { 427 | FileOption::Uri(u) => Self { 428 | path: PathBuf::new(), 429 | path_string: u.clone(), 430 | is_text: true, 431 | }, 432 | FileOption::Path(p) => Self::new_existed_file(p)?, 433 | }; 434 | Some(f) 435 | } 436 | 437 | pub fn get_modified_time(&self) -> std::io::Result { 438 | let f_meta = self.path 439 | .metadata()?; 440 | let modified = f_meta 441 | .modified()?; 442 | Ok(modified) 443 | } 444 | } 445 | 446 | pub fn is_text_file>(f: F) -> bool { 447 | let file_cmd = std::process::Command::new("file") 448 | .arg("--mime") 449 | .arg(f.as_ref()) 450 | .output() 451 | .expect("Cannot get `file` output"); 452 | let file_cmd_output = String::from_utf8(file_cmd.stdout) 453 | .expect("Non UTF8 `file` output"); 454 | 455 | if file_cmd_output.contains("text/") || 456 | file_cmd_output.contains("inode/symlink") || 457 | file_cmd_output.contains("inode/x-empty") || 458 | file_cmd_output.contains(": cannot open") 459 | { 460 | return true 461 | } 462 | 463 | if file_cmd_output.contains("symbolic link") { 464 | let pointee = std::fs::read_link(f.as_ref()) 465 | .expect("Cannot read link"); 466 | 467 | return is_text_file(pointee) 468 | } 469 | 470 | false 471 | } 472 | 473 | 474 | pub async fn open_file( 475 | conn: &mut super::NeovimConnection, 476 | env_ctx: &Env, 477 | f: &str 478 | ) { 479 | log::info!(target: "open_file", "{f}"); 480 | 481 | if let Err(e) = conn.nvim_actions 482 | .command(&format!("e {}", f)) 483 | .await 484 | { 485 | log::error!(target: "open", "Cannot open file buffer: {e:#?}"); 486 | return 487 | } 488 | 489 | if let Some(ft) = &env_ctx.opt.filetype { 490 | if let Err(e) = conn.nvim_actions 491 | .command(&format!("set filetype={ft}")) 492 | .await 493 | { 494 | log::error!(target: "ft", "Cannot set filetype: {e:#?}"); 495 | } 496 | } 497 | 498 | if env_ctx.opt.follow { 499 | if let Err(e) = conn.nvim_actions 500 | .command("norm! G") 501 | .await 502 | { 503 | log::error!(target: "G", "Cannot execute follow command: {e:#?}"); 504 | } 505 | 506 | } else if let Some(pattern) = &env_ctx.opt.pattern { 507 | if let Err(e) = conn.nvim_actions 508 | .command(&(format!("norm! /{pattern}"))) 509 | .await 510 | { 511 | log::error!(target: "pattern", "Cannot execute follow command: {e:#?}"); 512 | } 513 | 514 | } else if let Some(pattern_backwards) = &env_ctx.opt.pattern_backwards { 515 | if let Err(e) = conn.nvim_actions 516 | .command(&(format!("norm! ?{pattern_backwards}"))) 517 | .await 518 | { 519 | log::error!( 520 | target: "pattern_backwards", 521 | "Cannot execute follow backwards command: {e:#?}" 522 | ); 523 | } 524 | } 525 | 526 | if env_ctx.opt.keep || env_ctx.opt.keep_until_write { 527 | let (channel, page_id) = (conn.channel, &env_ctx.page_id); 528 | 529 | let mut keep_until_write_cmd = ""; 530 | if env_ctx.opt.keep_until_write { 531 | keep_until_write_cmd = indoc::indoc! {r#" 532 | vim.api.nvim_create_autocmd('BufWritePost', { 533 | buffer = buf, 534 | callback = function() 535 | pcall(function() 536 | on_delete() 537 | vim.api.nvim_buf_delete(buf, { force = true }) 538 | end) 539 | end 540 | }) 541 | "#}; 542 | } 543 | 544 | let cmd = indoc::formatdoc! {r#" 545 | local buf = vim.api.nvim_get_current_buf() 546 | local function on_delete() 547 | pcall(function() 548 | vim.rpcnotify({channel}, 'page_buffer_closed', '{page_id}') 549 | end) 550 | end 551 | {keep_until_write_cmd} 552 | vim.api.nvim_create_autocmd({{ 'BufDelete', 'BufWinLeave' }}, {{ 553 | buffer = buf, 554 | callback = on_delete 555 | }}) 556 | "#}; 557 | conn.nvim_actions 558 | .exec_lua(&cmd, vec![]) 559 | .await 560 | .expect("Cannot execute keep command"); 561 | } 562 | 563 | if let Some(lua) = &env_ctx.opt.lua { 564 | if let Err(e) = conn.nvim_actions 565 | .exec_lua(lua, vec![]) 566 | .await 567 | { 568 | log::error!(target: "lua", "Cannot execute lua command: {e:#?}"); 569 | } 570 | } 571 | 572 | if let Some(command) = &env_ctx.opt.command { 573 | if let Err(e) = conn.nvim_actions 574 | .command(command) 575 | .await 576 | { 577 | log::error!(target: "cmd", "Cannot execute command: {e:#?}"); 578 | } 579 | } 580 | 581 | if env_ctx.opt.keep || env_ctx.opt.keep_until_write { 582 | match conn.rx.recv().await { 583 | Some(connection::NotificationFromNeovim::BufferClosed) | None => {}, 584 | n => { 585 | log::error!("Unhandled notification: {n:?}"); 586 | } 587 | } 588 | } 589 | } 590 | } 591 | 592 | 593 | async fn exit_from_neovim(env_ctx: context::Env, mut conn: NeovimConnection) { 594 | log::info!(target: "exit_from_neovim", ""); 595 | 596 | if !env_ctx.opt.back && !env_ctx.opt.back_restore { 597 | connection::close_and_exit(&mut conn).await; 598 | } 599 | 600 | let (win, buf) = &conn.initial_win_and_buf; 601 | 602 | if let Err(e) = conn.nvim_actions 603 | .set_current_win(win) 604 | .await 605 | { 606 | log::error!("Cannot return to initial window: {e:#?}"); 607 | connection::close_and_exit(&mut conn).await; 608 | } 609 | 610 | if let Err(e) = conn.nvim_actions 611 | .set_current_buf(buf) 612 | .await 613 | { 614 | log::error!("Cannot return to initial buffer: {e:#?}"); 615 | connection::close_and_exit(&mut conn).await; 616 | } 617 | 618 | if env_ctx.opt.back_restore { 619 | if let Err(e) = conn.nvim_actions 620 | .command("norm! A") 621 | .await 622 | { 623 | log::error!("Cannot return to insert mode: {e:#?}"); 624 | } 625 | } 626 | 627 | connection::close_and_exit(&mut conn).await; 628 | } 629 | --------------------------------------------------------------------------------