Compare commits

...

77 Commits

Author SHA1 Message Date
Johannes Altmanninger
65556ac2ae Increase grace period for decoding escape sequences
Historically, fish has treated input bytes [0x1b, 'b'] as alt-b (rather than
"escape,b") if the second byte arrives within 30ms of the first.

Since we made builtin bind to match key events instead of raw byte sequences
key events (as per the kitty keyboard protocol), we have another place where
we do similar disambiguation: when we read keys such as alt-left ("\e[1;3D"),
we only consider bytes to be part of this sequence if stdin is immediately
readable (actually after a 1ms timeout since e1be842 (Work around
torn byte sequences in qemu kbd input with 1ms timeout, 2025-03-04)).

This is technically wrong but has worked in practice (for Kakoune etc.).

Issue #11668 reports two issues on some Windows terminals with a remote fish shell:
- the "bracketed paste finished" sequence may be split into multiple packets,
  which causes a delay of > 1ms between individual bytes being readable.
- AutoHotKey scripts simulating seven "left" keys result in sequence tearing
  as well.

Try to fix the paste case by increasing the timeout when parsing escape
sequences.

Also increase the timeout for terminals that support the kitty keyboard
protocol.  The user should only notice the delay caused by the timeout after
presses one of escape,O, escape,P, escape,[, or escape,] while the kitty
keyboard protocol is disabled (e.g. while an external command is running).
This case also virtually increases fish_escape_delay_ms; hopefully this edge
case is not ever relevant.

Part of #11668
2025-07-25 11:47:18 +02:00
Johannes Altmanninger
25e5cc23c1 Block interrupts and uvar events while decoding key
readb() has only one caller that passes blocking=false: try_readb().
This function is used while decoding keys; anything but a successful read
is treated as "end of input sequence".

This means that key input sequences such as \e[1;3D
can be torn apart by
- signals (EINTR) which is more likely since e1be842 (Work around torn byte
  sequences in qemu kbd input with 1ms timeout, 2025-03-04).
- universal variable notifications (from other fish processes)

Fix this by blocking signals and not listening on the uvar fd.  We do
something similar at the next higher level -- key sequence matching --
so extract a function to reuse for key decoding.

Ref: https://github.com/fish-shell/fish-shell/issues/11668#issuecomment-3101341081
2025-07-25 11:47:18 +02:00
Johannes Altmanninger
4012345ba9 Reduce MaybeUninit lifetime 2025-07-25 11:47:18 +02:00
Johannes Altmanninger
43d583d991 Fix regression causing \e[ to be interpreted as ctrl-[
Fixes 3201cb9f01 (Stop parsing invalid CSI/SS3 sequences as alt-[/alt-o,
2024-12-30).
2025-07-25 11:47:18 +02:00
Johannes Altmanninger
d69886efe0 completions/protontricks: coding style, translations 2025-07-25 11:15:05 +02:00
Johannes Altmanninger
bd8cc6d317 Merge pull request #11375 2025-07-25 11:10:38 +02:00
Johannes Altmanninger
9c5b3f3d57 Merge pull request #11674 2025-07-25 11:03:53 +02:00
Johannes Altmanninger
5970f34a60 Retry some writes on EINTR again
I guess?
Fixes f0e007c439 (Relocate tty metadata and protocols and clean it up,
2025-06-19).
2025-07-25 10:23:48 +02:00
Johannes Altmanninger
eaa837effa Refresh TTY timestamps again in most cases
See commit 081c3282b7 (Refresh TTY timestamps also in some rare cases,
2025-01-15) and others.
Fixes d27f5a5293 (Adopt TtyHandoff in remaining places, 2025-06-21)
Fixes #11671
2025-07-25 10:23:48 +02:00
Johannes Altmanninger
e52cf2f6a7 Try to restore TTY protocols more reliably after SIGTERM
We might
1. set TTY_PROTOCOLS_ACTIVE to false
2. receive `SIGTERM`
3. due to 1 fail to disable TTY protocols

Fix this by making sure that the disabling of protocols happens-before we
set TTY_PROTOCOLS_ACTIVE to false.

See 37c04745e6 (Avoid potential contention on SIGTERM while enabling terminal
protocols, 2024-10-08).
Fixes d27f5a5293 (Adopt TtyHandoff in remaining places, 2025-06-21)
2025-07-25 10:23:48 +02:00
Johannes Altmanninger
8c7568c0cb Reapply "Disable focus reporting on non-tmux again for now" 2024-04-18
Reapply bdd478bbd0. Amendment to f0e007c439 (Relocate tty metadata and
protocols and clean it up, 2025-06-19).
2025-07-25 10:23:48 +02:00
Johannes Altmanninger
07979782a6 Fix iTerm2 detection on non-iTerm2 terminals
Fixes f0e007c439 (Relocate tty metadata and protocols and clean it up,
2025-06-19).
2025-07-25 10:23:48 +02:00
Johannes Altmanninger
59b43986e9 build_tools/style.fish: fail if formatters are not available
build_tools/check.sh is supposed to fail on formatting violations.  I don't
think we have a good reason for running build_tools/style.fish outside
check.sh.

black is the only formatter not versioned in CI -- but we can probably
satisfy all realistic versions.

Ref: https://github.com/fish-shell/fish-shell/pull/11608#discussion_r2173176621
2025-07-25 10:23:48 +02:00
phisonate
51f3722e02 Fix funced to not expand or execute function name when interactive
Due to unnecessary quotes in the prompt command given to `read` by `funced` when
editing a function interactively (using `-i`), the name of the function to edit
would be evaluated, expanded and even executed (when using command substitution
for example), which is at least annoying when using unusual but valid and
allowed function names like '*' or 'head (cat)'. This commit delays the function
name expansion so that this should no longer happen.
2025-07-24 01:03:02 +02:00
Peter Ammon
db0f9c1d53 Minor refactoring of make_wait_handle 2025-07-20 13:40:22 -07:00
Peter Ammon
c9901398ed Switch DISOWNED_PIDS from MainThread to Mutex
Preparation for concurrent execution. These can be reaped on any thread.
2025-07-19 17:56:58 -07:00
Peter Ammon
6181ba3b56 Fix an 1.70 clippy 2025-07-19 16:49:51 -07:00
Peter Ammon
d27f5a5293 Adopt TtyHandoff in remaining places
This adopts the tty handoff in remaining places. The idea is to rationalize
when we enable and disable tty protocols (such as CSI-U).

In particular this removes the tty protocol disabling in Parser::eval_node
- that is intended to execute pure fish script and should not be talking to
the tty.
2025-07-19 16:04:13 -07:00
Peter Ammon
c1d165de9d Adopt TtyHandoff in fish_key_reader
Prepare to remove terminal_protocols_enable/disable_ifn
2025-07-19 16:04:13 -07:00
Peter Ammon
f0e007c439 Relocate tty metadata and protocols and clean it up
fish-shell attempts to set up certain terminal protocols (bracketed paste,
CSI-U) while it is in control of the tty, and disable these when passing
off the tty to other processes. These terminal protocols are enabled or
disabled by emitting certain control sequences to the tty.

Today fish-shell does this in a somewhat haphazard way, tracking whether
the protocols are enabled or disabled. Functions like `Parser::exec_node`
then just toggle these, causing data to be written to the terminal in
unexpected places. In particular this is very bad for concurrent execution:
we don't want random threads talking to the tty.

Fortunately we have a controlled place where we can muck with the tty:
`TtyTransfer` which controls handoff of ownership to child processes (via
`tcsetpgrp`). Let's centralize logic around enabling and disabling terminal
protocols there. Put it in a new module and rename it to `TtyHandoff` which is a
little nicer.

This commit moves code around and does some cleanup; it doesn't actually
pull the trigger on centralizing the logic though. Next commit will do that.
2025-07-19 16:04:13 -07:00
Peter Ammon
65a4cb5245 Revert "Restore terminal state on SIGTERM again"
This reverts commit 1d6fa258f6.

This reintroduces commit 941701da3d, which was then reverted in
941701da3d8; this commit reverts the revert to reintroduce 941701da3d.

The reason is that the existing logic in terminal_protocols_disable_ifn does a
bunch of stuff for which nobody has thought about its signal safety, such as
accessing the reader stack (clearly not async signal safe).
Even functions which happen to be safe now may become unsafe in the future.

This is just the nature of signal handling code. We must ensure that only
async-signal safe syscalls are run, and only functions which are themselves
async-signal safe, which we (try) to designate with the "safe_" prefix.
2025-07-19 15:37:00 -07:00
rosavi
c7262d6c05 Add completions for cpan and t-rec
Closes #11647
2025-07-19 22:15:24 +02:00
Johannes Altmanninger
f3c264722d Merge pull request #11666 2025-07-19 22:15:24 +02:00
Johannes Altmanninger
39742cafa0 Merge pull request #11663 2025-07-19 22:15:24 +02:00
Johannes Altmanninger
295d2bd218 update translations for completions/fish_indent 2025-07-19 22:15:24 +02:00
Johannes Altmanninger
3588b41744 Merge pull request #11662 2025-07-19 22:15:24 +02:00
Johannes Altmanninger
82e3311756 Merge pull request #11659 2025-07-19 22:15:24 +02:00
Johannes Altmanninger
b611c96cdd Merge pull request #11654 2025-07-19 22:15:24 +02:00
Johannes Altmanninger
1d6fa258f6 Restore terminal state on SIGTERM again
Commit 941701da3d (Restore some async-signal discipline to SIGTERM,
2025-06-15) made two changes
1. removed a mutex lock in signal handler (regression from 55fd43d86c
   (Port reader, 2023-12-22))
2. removed some SIGTERM cleanup

I'm not sure what's the reason for 2, so let's revert it I guess.  This code
path already uses FLOG_SAFE for async-signal safety.

There is an avoidable panic when `Outputter::stdoutput()` is already
borrowed. Fix that.

Closes #11597
2025-07-19 22:15:24 +02:00
Johannes Altmanninger
6312b1dbd8 Format test_driver.py 2025-07-19 22:15:24 +02:00
Peter Ammon
5fa2f62536 test_driver: increase open file limit
Prevent failures due to file handle exhaustion.
2025-07-19 11:14:37 -07:00
Peter Ammon
bbf7568ebd test_driver.py: Properly report exceptions
If a test fails by throwing an exception (in this case, "Too many open files")
then that exception would propagate, be uncaught, and then the remaining tests
would not be await'ed, leading to a hang.

Fix this by properly catching and reporting exceptions.
2025-07-19 11:01:48 -07:00
Rhidian De Wit
72347517b2 Fix missing bool to string cast causing errors 2025-07-17 20:17:52 +02:00
may
95475c35ff update translation files 2025-07-16 16:36:09 +02:00
may
560d21cd86 complete git rebase --keep-base 2025-07-16 15:49:38 +02:00
nick
bef453f69b fish_indent -c/--check completions
Manpage `fish_indent(1)` documents the `-c/--check` option, which checks
if a file is already indented as `fish_indent` would. This option is now
included in the completions for `fish_indent`.
2025-07-15 15:44:36 -05:00
vcalv
fa832ead65 add suspend-then-hibernate to systemctl.fish
suspend-then-hibernate was missing
2025-07-14 18:09:32 -04:00
Azamat Dinaev
eb7afd2a9c Update __fish_print_hostnames.fish
There was an issue in autocomplete of ssh. 

When you put in ~/.ssh/config line like this:

"Include Include ${HRL_SSH}/onprem_config"

and then trying to use fish complete for ssh, for example:

"ssh -J" and press key <Tab> it throughs an error that fish cannot understand ${HRL_SSH} with brackets.
2025-07-12 14:18:43 +03:00
Johannes Altmanninger
f4ddcfa694 Merge pull request #11652 2025-07-11 12:11:17 +02:00
Johannes Altmanninger
1605d8d6ce Merge pull request #11644 2025-07-11 12:10:49 +02:00
Johannes Altmanninger
a7559a62c4 completions/cjpm: format with fish_indent 2025-07-11 12:09:58 +02:00
Johannes Altmanninger
e9327d234d Merge pull request #11641 2025-07-11 12:08:37 +02:00
Daniel Rainer
a3d03fc0fb Avoid duplicate flag values
Both `SKIP_CMDSUBST` and `NO_SPACE_FOR_UNCLOSED_BRACE` used `1 << 14` as their
value accidentally, resulting from `SKIP_CMDSUBST` not being sorted correctly.
Resolve this by using the next (and last in u16) unused bit for `SKIP_CMDSUBST`
and moving it to the end.

Fixes #11651.
2025-07-10 18:52:10 +02:00
Daniel Rainer
1e981a9827 Support upper/lower casing selection
Fixes #11639.
2025-07-10 16:57:17 +02:00
Daniel Rainer
770f4ce6d1 Add docs for casing shortcuts in normal mode 2025-07-06 20:15:25 +02:00
Daniel Rainer
aa782bdad7 Fix vi mode docs for moving to beginning of line 2025-07-06 20:10:21 +02:00
Jiangqiu Shen
e4c55131c7 Update translation 2025-07-04 18:31:39 -04:00
Jiangqiu Shen
e6ad78cda7 update 2025-07-04 00:30:28 -04:00
Jiangqiu Shen
578e162f35 Add completion for Cangjie programing language
add completion of cjpm
2025-07-04 00:29:11 -04:00
Johannes Altmanninger
e9bb150a41 Merge pull request #11633 2025-07-03 15:19:51 +02:00
Johannes Altmanninger
b5eccdf9f6 Merge pull request #11632 2025-07-03 15:19:51 +02:00
Johannes Altmanninger
75716bd6b0 Merge pull request #11631 2025-07-03 15:19:51 +02:00
Johannes Altmanninger
9e628995da Merge pull request #11629 ("Add completions for tmuxp") 2025-07-03 15:19:51 +02:00
Daniel Müller
5b39efc96d Support incrementing/decrementing the number below the cursor
Vim supports incrementing & decrementing the number below the cursor (or
after it) via Ctrl-a and Ctrl-x, respectively. Given fish's Vi mode
support, it makes sense to provide similar functionality when working on
the command line, to provide a more natural environment for Vim users.
With this change we add the necessary functionality.

Closes: #8320
Closes #11570
2025-07-03 14:38:42 +02:00
Johannes Altmanninger
b5bb50d742 builtin commandline: apply commandline+cursor to first top-level reader
Historically, `fish -C "commandline echo"` was silently ignored.  Make it do
the expected thing.  This won't affect subsequent readers because we only do
it for top-level ones, and reader_pop() will clear the commandline state again.

This improves consistency with the parent commit.  We probably don't want to
support arbitrary readline commands before the first reader is initialized,
but setting the initial commandline seems useful: first, it would have helped
me in the past for debugging fish.  Second, it would allow one to rewrite
an application launcher:

	 foot --app-id my-foot-launcher -e fish -C '
	 	set fish_history launcher
	 	bind escape exit
	 	bind ctrl-\[ exit
	-	function fish_should_add_to_history
	-		false
	-	end
	-	for enter in enter ctrl-j
	-		bind $enter '\''
	-			history append -- "$(commandline)"
	-			commandline "setsid $(commandline) </dev/null >/dev/null 2>&1 & disown && exit"
	-			commandline -f execute
	-		'\''
	-	end
	+	commandline "setsid  </dev/null >/dev/null 2>&1 & disown && exit"
	+	commandline --cursor $(string length "setsid ")
	 '

which is probably not desirable today because it will disable autosuggestions.
Though that could be fixed eventually by making autosuggestions smarter.

If we find a generally-useful use case, we should mention this in the changelog.

Ref: https://github.com/fish-shell/fish-shell/pull/11570#discussion_r2144544053
2025-07-03 14:38:42 +02:00
Daniel Müller
32c36aa5f8 builtins commandline/complete: allow handling commandline before reader initialization
Commands like "commandline foo" silently fail, and "complete -C" fails with
a weird "option requires an argument" error.

I think at least the first one can be useful in edge cases, e.g. to test
code that does not separate the `commandline` input and output (#11570),
and to set fish's initial commandline, see the next commit.

I don't think there are super strong reasons to allow these, but if the
existing state is merely due to "no one has ever thought of doing this",
then we should try changing it.

For consistency, also allow "complete -C". I guess an argument for that is
that it's weird to make a command behave differently in non-interactive shells.

For now, keep the historical behavior of disabling access to the command
line in non-interactive shells. If we find a good reason for allowing that
(which seems unlikely), we can.

Ref: https://github.com/fish-shell/fish-shell/pull/11570#discussion_r2144544053

Co-authored-by: Johannes Altmanninger <aclopte@gmail.com>
2025-07-03 14:16:18 +02:00
merceyz
fc37d8d5a8 feat: add completions for volta 2025-06-29 16:23:49 +02:00
merceyz
8d361b4290 feat: add completions for k9s 2025-06-29 16:18:41 +02:00
merceyz
9789e6b731 feat: add completions for cilium and hubble 2025-06-29 16:09:27 +02:00
Johannes Altmanninger
4d67ca7c58 Add ctrl-alt-h compatibility binding
Historically, ctrl-i sends the same code as tab, ctrl-h sends backspace and
ctrl-j and ctrl-m behave like enter.

Even for terminals that send unambiguous encodings (via the kitty keyboard
protocol), we have kept bindings like ctrl-h, to support existing habits.

We forgot that pressing alt-ctrl-h would behave like alt-backspace (and can
be easier to reach) so maybe we should add that as well.

Don't add ctrl-shift-i because at least on Linux, that's usually intercepted
by the terminal emulator.

Technically there are some more such as "ctrl-2" (which used to do the same as
"ctrl-space") but I don't think anyone uses that over "ctrl-space".

Closes #https://github.com/fish-shell/fish-shell/discussions/11548
2025-06-28 14:19:10 +02:00
Patryk Bratkowski
fbe5a53dc9 Add completions for tmuxp 2025-06-28 14:58:37 +03:00
Johannes Altmanninger
dcd93e4c52 Fix compatibility with Python 3.6 for OpenSUSE Leap 15.6
Nightlies for opensuse/leap:15.6 are failing because their /bin/python3
is Python 3.6 (the "python311" package creates only /bin/python311).
Python3.6 has been EOL for 3.5 years but OpenSuse leap is not even EOL.
Given that we don't write a lot of Python, let's support this for now.
2025-06-28 13:36:31 +02:00
Johannes Altmanninger
7acd20dc7e Fix opensuse docker file file extension 2025-06-28 13:27:30 +02:00
Johannes Altmanninger
1d893b77d3 Also flag MSRV rustc/clippy warnings in CI 2025-06-28 11:04:29 +02:00
Johannes Altmanninger
b451650faa Flag stable rustc/clippy warnings in CI
Today, when a change introduces warnings, the change author might not see
them. Fix that by making clippy fail on warnings.

AFAICT, "clippy --deny-warnings" will also fail on rustc warnings.
I'd imagine this is what most respectable Rust projects do.

Pin stable rust so we won't get unrelated failures. Alternatively, we could
keep using "dtolnay/rust-toolchain@stable", that should be fine too (as long
as we have the capacity to quickly silence/iron out clippy failures).

While at it, remove some unneeded dependencies. Keep gettext because that
one might enable some cfg-directives (?).
Other cfgs like feature="benchmark" and target_os != "linux" are not yet checked in CI.

See #11584
2025-06-28 11:02:19 +02:00
Johannes Altmanninger
3e0a53ae4f Resolve an uninlined_format_args warning 2025-06-28 11:02:19 +02:00
Johannes Altmanninger
e01aafab1c Extract github actions for interesting rust toolchain versions
Extract a github action to reduce the number of references to our MSRV and
stable (to be pinned in the next commit).

While at it, use the MSRV for macOS builds; this means that we'll be less
like accidentally to break the macOS build when bumping the MSRV.  I don't
think there is a reason for using 1.73 specifically, other than "it's the
highest we can use on old macOS", so using an even older one should be fine.
2025-06-28 09:50:18 +02:00
Johannes Altmanninger
fd0fba83b9 Fix inconsistency in docker file lint override 2025-06-28 09:50:18 +02:00
Johannes Altmanninger
6644cc9b0e Use statvfs on NetBSD again to fix build
From commit ba00d721f4 (Correct statvfs call to statfs, 2025-06-19):

> This was missed in the Rust port

To elaborate:

- ec176dc07e (Port path.h, 2023-04-09) didn't change this (as before,
 `statvfs` used `ST_LOCAL` and `statfs` used `MNT_LOCAL`)
- 6877773fdd (Fix build on NetBSD (#10270), 2024-01-28) changed the `statvfs`
  call to `statfs`, presumably due to the libc-wrapper for
  `statvfs` being missing on NetBSD.  This change happens
  to work fine on NetBSD because they do [`#define ST_LOCAL
  MNT_LOCAL`](https://github.com/fish-shell/fish-shell/pull/11486#discussion_r2092408952)
  But it was wrong on others like macOS and FreeBSD, which was fixed by
  ba00d721f4 (but that broke the build on NetBSD).
- 7228cb15bf (Include sys/statvfs.h for the definition of ST_LOCAL (Rust
  port regression), 2025-05-16)
  fixed a code clone left behind by the above commit (incorrectly assuming
  that the clone had always existed.)

Fix the NetBSD build specifically by using statfs on that platform.

Note that this still doesn't make the behavior equivalent to commit LastC++11.
That one used ST_LOCAL if defined, and otherwise MNT_LOCAL if defined.

If we want perfect equivalence, we could detect both flags in `src/build.rs`.
Then we would also build on operating systems that define neither. Not sure.

Closes #11596
2025-06-28 09:50:18 +02:00
adamanteye
f5370e6f22 Add colorful disassembler output completion for objdump
As desribed in objdump(1), --disassembler-color can be applied to
enable or disable the use of syntax highlighting in disassembly
output.

The options are:

--disassembler-color=off
--disassembler-color=terminal
--disassembler-color=on|color|colour
--disassembler-color=extened|extended-color|extened-colour

Signed-off-by: adamanteye <ada@adamanteye.cc>

Closes #11615
2025-06-28 09:50:18 +02:00
Patryk Bratkowski
d62fb9cc74 Add 'ollama stop' completions.
- Added the '__fish_ollama_ps' function to list running models.
- Added the 'stop' subcommand to ollama completions.
- Added running models as arguments to 'stop'.
2025-06-28 09:50:18 +02:00
Johannes Altmanninger
88ab024d7d Merge pull request #11627 2025-06-28 09:29:11 +02:00
Johannes Altmanninger
1cc900ab7f Merge pull request #11625 2025-06-28 09:28:58 +02:00
Dezhi Wu
144725e947 fix(echo): handle overflow in octal/hex escape parsing
Use wrapping arithmetic when parsing octal and hex escapes in echo to
prevent panics on overflow and ensure consistent behavior with other
shells. This change allows echo to process escape sequences like \5555
without crashing, keeping the same behavior as 3.7.1.

```
$ ./fish --version
fish, version 3.7.1
$ ./fish -c 'echo -e "\5555"'
m5
```
2025-06-28 11:15:42 +08:00
Dezhi Wu
d969577f0b fix(fmt): Resolve Rust 1.88 clippy warnings
Update formatting macros to use the new inline variable syntax as
recommended by Rust 1.88 clippy.
2025-06-28 09:10:27 +08:00
Lucas Melo
3cbb5e384b Shorten and format completions for protontricks, protontricks-launch 2025-05-12 08:36:07 -03:00
Lucas Melo
90b35335ee Add completions for protontricks 2025-05-12 08:24:26 -03:00
76 changed files with 4998 additions and 673 deletions

View File

@@ -0,0 +1,22 @@
name: Oldest Supported Rust Toolchain
on:
workflow_call:
inputs:
targets:
description: Comma-separated list of target triples to install for this toolchain
required: false
components:
description: Comma-separated list of components to be additionally installed
required: false
permissions:
contents: read
runs:
using: "composite"
steps:
- uses: dtolnay/rust-toolchain@1.70
with:
targets: ${{ inputs.targets }}
components: ${{ inputs.components}}

View File

@@ -0,0 +1,22 @@
name: Stable Rust Toolchain
on:
workflow_call:
inputs:
targets:
description: Comma-separated list of target triples to install for this toolchain
required: false
components:
description: Comma-separated list of components to be additionally installed
required: false
permissions:
contents: read
runs:
using: "composite"
steps:
- uses: dtolnay/rust-toolchain@1.88
with:
targets: ${{ inputs.targets }}
components: ${{ inputs.components }}

View File

@@ -9,12 +9,12 @@ jobs:
environment: macos-codesign
steps:
- uses: actions/checkout@v4
- name: Install Rust 1.73.0
uses: dtolnay/rust-toolchain@1.73.0
- name: Install Rust
uses: ./.github/actions/rust-toolchain@oldest-supported
with:
targets: x86_64-apple-darwin
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
uses: ./.github/actions/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: build-and-codesign

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.70
- uses: ./.github/actions/rust-toolchain@oldest-supported
- name: Install deps
run: |
sudo apt install gettext libpcre2-dev python3-pexpect python3-sphinx tmux
@@ -47,7 +47,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.70
- uses: ./.github/actions/rust-toolchain@oldest-supported
with:
targets: "i586-unknown-linux-gnu" # rust-toolchain wants this comma-separated
- name: Install deps
@@ -128,7 +128,7 @@ jobs:
#
# steps:
# - uses: actions/checkout@v4
# - uses: dtolnay/rust-toolchain@1.70
# - uses: ./.github/actions/rust-toolchain@oldest-supported
# - name: Install deps
# run: |
# sudo apt install gettext libpcre2-dev python3-pexpect tmux
@@ -156,7 +156,7 @@ jobs:
CARGO_NET_GIT_FETCH_WITH_CLI: true
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.70
- uses: ./.github/actions/rust-toolchain@oldest-supported
- name: Install deps
run: |
# --break-system-packages because homebrew has now declared itself "externally managed".

View File

@@ -8,47 +8,48 @@ permissions:
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: ./.github/actions/rust-toolchain@stable
with:
components: rustfmt
- name: cargo fmt
run: cargo fmt --check
clippy:
clippy-stable:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: ./.github/actions/rust-toolchain@stable
with:
components: clippy
- name: Install deps
run: |
sudo apt install gettext libpcre2-dev
- name: cmake
run: |
cmake -B build
sudo apt install gettext
- name: cargo clippy
# This used to have --deny=warnings, but that turns rust release day
# into automatic CI failure day, so we don't do that.
run: cargo clippy --workspace --all-targets
run: cargo clippy --workspace --all-targets -- --deny=warnings
clippy-msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/rust-toolchain@oldest-supported
with:
components: clippy
- name: Install deps
run: |
sudo apt install gettext
- name: cargo clippy
run: cargo clippy --workspace --all-targets -- --deny=warnings
rustdoc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: ./.github/actions/rust-toolchain@stable
- name: cargo doc
run: |
RUSTDOCFLAGS='-D warnings' cargo doc --workspace
- name: cargo doctest
run: |
cargo test --doc --workspace
# Disabling for now because it also checks "advisories",
# making CI fail for reasons unrelated to the patch
# cargo-deny:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# - uses: EmbarkStudios/cargo-deny-action@v1

View File

@@ -16,7 +16,7 @@ jobs:
contents: read
steps:
- uses: dtolnay/rust-toolchain@1.70
- uses: ./.github/actions/rust-toolchain@oldest-supported
- uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
contents: read
steps:
- uses: dtolnay/rust-toolchain@1.70
- uses: ./.github/actions/rust-toolchain@oldest-supported
- uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -64,6 +64,7 @@ New or improved bindings
this is only enabled by default if the terminal advertises support for the ``indn`` capability via XTGETTCAP.
- Bindings using shift with non-ASCII letters (such as :kbd:`ctrl-shift-ä`) are now supported.
If there is any modifier other than shift, this is the recommended notation (as opposed to :kbd:`ctrl-Ä`).
- Vi mode has learned :kbd:`ctrl-a` (increment) and :kbd:`ctrl-x` (decrement) (:issue:`11570`).
Completions
^^^^^^^^^^^

View File

@@ -325,7 +325,7 @@ fn get_version(src_dir: &Path) -> String {
let path = src_dir.join("version");
if let Ok(strver) = read_to_string(path) {
return strver.to_string();
return strver;
}
let args = &["describe", "--always", "--dirty=-dirty"];
@@ -448,10 +448,7 @@ fn build_man(build_dir: &Path) {
);
}
Ok(out) => {
if out.success() {
// Success!
return;
} else {
if !out.success() {
panic!("sphinx-build failed to build the man pages.");
}
}

View File

@@ -30,7 +30,7 @@ X86_64_DEPLOY_TARGET='MACOSX_DEPLOYMENT_TARGET=10.9'
# The first supported version of macOS on arm64 is 10.15, so any Rust is fine for arm64.
# We wish to support back to 10.9 on x86-64; the last version of Rust to support that is
# version 1.73.0.
RUST_VERSION_X86_64=1.73.0
RUST_VERSION_X86_64=1.70.0
while getopts "sf:i:p:e:nj:" opt; do
case $opt in

View File

@@ -62,17 +62,16 @@ if set -q fish_files[1]
if not type -q fish_indent
echo
echo $yellow'Could not find `fish_indent` in `$PATH`.'$normal
echo
else
echo === Running "$green"fish_indent"$normal"
if set -l -q _flag_check
if not fish_indent --check -- $fish_files
echo $red"Fish files are not formatted correctly."$normal
exit 1
end
else
fish_indent -w -- $fish_files
exit 127
end
echo === Running "$green"fish_indent"$normal"
if set -l -q _flag_check
if not fish_indent --check -- $fish_files
echo $red"Fish files are not formatted correctly."$normal
exit 1
end
else
fish_indent -w -- $fish_files
end
end
@@ -80,17 +79,16 @@ if set -q python_files[1]
if not type -q black
echo
echo $yellow'Please install `black` to style python'$normal
echo
else
echo === Running "$green"black"$normal"
if set -l -q _flag_check
if not black --check $python_files
echo $red"Python files are not formatted correctly."$normal
exit 1
end
else
black $python_files
exit 127
end
echo === Running "$green"black"$normal"
if set -l -q _flag_check
if not black --check $python_files
echo $red"Python files are not formatted correctly."$normal
exit 1
end
else
black $python_files
end
end
@@ -98,30 +96,29 @@ if not cargo fmt --version >/dev/null
echo
echo $yellow'Please install "rustfmt" to style Rust, e.g. via:'
echo "rustup component add rustfmt"$normal
echo
else
echo === Running "$green"rustfmt"$normal"
if set -l -q _flag_check
if set -l -q _flag_all
if not cargo fmt --check
echo $red"Rust files are not formatted correctly."$normal
exit 1
end
else
if set -q rust_files[1]
if not rustfmt --check --files-with-diff $rust_files
echo $red"Rust files are not formatted correctly."
exit 1
end
end
exit 127
end
echo === Running "$green"rustfmt"$normal"
if set -l -q _flag_check
if set -l -q _flag_all
if not cargo fmt --check
echo $red"Rust files are not formatted correctly."$normal
exit 1
end
else
if set -l -q _flag_all
cargo fmt
else
if set -q rust_files[1]
rustfmt $rust_files
if set -q rust_files[1]
if not rustfmt --check --files-with-diff $rust_files
echo $red"Rust files are not formatted correctly."
exit 1
end
end
end
else
if set -l -q _flag_all
cargo fmt
else
if set -q rust_files[1]
rustfmt $rust_files
end
end
end

View File

@@ -507,7 +507,13 @@ Command mode is also known as normal mode.
- :kbd:`backspace` moves the cursor left.
- :kbd:`g` / :kbd:`G` moves the cursor to the beginning/end of the commandline, respectively.
- :kbd:`g,g` / :kbd:`G` moves the cursor to the beginning/end of the commandline, respectively.
- :kbd:`~` toggles the case (upper/lower) of the character and moves to the next character.
- :kbd:`g,u` lowercases to the end of the word.
- :kbd:`g,U` uppercases to the end of the word.
- :kbd:`:,q` exits fish.
@@ -551,6 +557,10 @@ Visual mode
- :kbd:`~` toggles the case (upper/lower) on the selection, and enters :ref:`command mode <vi-mode-command>`.
- :kbd:`g,u` lowercases the selection, and enters :ref:`command mode <vi-mode-command>`.
- :kbd:`g,U` uppercases the selection, and enters :ref:`command mode <vi-mode-command>`.
- :kbd:`",*,y` copies the selection to the clipboard, and enters :ref:`command mode <vi-mode-command>`.
.. _custom-binds:

View File

@@ -6,11 +6,9 @@ ENV LC_ALL=C.UTF-8
RUN zypper --non-interactive install \
bash \
cmake \
diffutils \
gcc-c++ \
git-core \
ninja \
pcre2-devel \
python311 \
python311-pip \
@@ -35,4 +33,6 @@ WORKDIR /home/fishuser
COPY fish_run_tests.sh /
ENV FISH_CHECK_LINT=false
CMD /fish_run_tests.sh

471
po/de.po

File diff suppressed because it is too large Load Diff

471
po/en.po

File diff suppressed because it is too large Load Diff

471
po/fr.po

File diff suppressed because it is too large Load Diff

471
po/pl.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

471
po/sv.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -279,6 +279,7 @@ fn format_a(mut y: f64, params: FormatParams<'_, impl Write>) -> Result<usize, E
// Compute the number of hex digits in the mantissa after the decimal.
// -1 for leading 1 bit (we are to the range [1, 2)), then divide by 4, rounding up.
#[allow(unknown_lints)] // for old clippy
#[allow(clippy::manual_div_ceil)]
const MANTISSA_HEX_DIGITS: usize = (MANTISSA_BITS - 1 + 3) / 4;
if had_prec && prec < MANTISSA_HEX_DIGITS {
@@ -495,7 +496,7 @@ fn format_mantissa_e(
let digit = if d < decimal.len_i32() { decimal[d] } else { 0 };
let min_width = if d > 0 { DIGIT_WIDTH } else { 1 };
buf.clear();
write!(buf, "{:0width$}", digit, width = min_width)?;
write!(buf, "{digit:0min_width$}")?;
let mut s = buf.as_str();
if d == 0 {
// First digit. Emit it, and likely also a decimal point.

View File

@@ -464,7 +464,7 @@ pub fn sprintf_locale(
let uint = arg.as_uint()?;
if uint != 0 {
prefix = "0x";
write!(buf, "{:x}", uint)?;
write!(buf, "{uint:x}")?;
}
buf
}
@@ -478,9 +478,9 @@ pub fn sprintf_locale(
prefix = if lower { "0x" } else { "0X" };
}
if lower {
write!(buf, "{:x}", uint)?;
write!(buf, "{uint:x}")?;
} else {
write!(buf, "{:X}", uint)?;
write!(buf, "{uint:X}")?;
}
}
buf
@@ -488,7 +488,7 @@ pub fn sprintf_locale(
CS::o => {
let uint = arg.as_uint()?;
if uint != 0 {
write!(buf, "{:o}", uint)?;
write!(buf, "{uint:o}")?;
}
if flags.alt_form && desired_precision.unwrap_or(0) <= buf.len() + 1 {
desired_precision = Some(buf.len() + 1);
@@ -498,7 +498,7 @@ pub fn sprintf_locale(
CS::u => {
let uint = arg.as_uint()?;
if uint != 0 {
write!(buf, "{}", uint)?;
write!(buf, "{uint}")?;
}
buf
}

View File

@@ -890,7 +890,7 @@ fn test_exhaustive(rust_fmt: &str, c_fmt: *const c_char) {
// "There's only 4 billion floats so test them all."
// This tests a format string expected to be of the form "%.*g" or "%.*e".
// That is, it takes a precision and a double.
println!("Testing {}", rust_fmt);
println!("Testing {rust_fmt}");
let mut rust_str = String::with_capacity(128);
let mut c_storage = [0u8; 128];
let c_storage_ptr = c_storage.as_mut_ptr() as *mut c_char;

View File

@@ -0,0 +1 @@
cilium completion fish 2>/dev/null | source

102
share/completions/cjpm.fish Normal file
View File

@@ -0,0 +1,102 @@
# cjpm.fish - Fish completion script for Cangjie Package Manager
# Global options
complete -c cjpm -l help -s h -d "Help for cjpm"
complete -c cjpm -l version -s v -d "Version for cjpm"
# Subcommands
complete -c cjpm -n __fish_use_subcommand -f -a init -d "Init a new cangjie module"
complete -c cjpm -n __fish_use_subcommand -f -a check -d "Check the dependencies"
complete -c cjpm -n __fish_use_subcommand -f -a update -d "Update cjpm.lock"
complete -c cjpm -n __fish_use_subcommand -f -a tree -d "Display the package dependencies in the source code"
complete -c cjpm -n __fish_use_subcommand -f -a build -d "Compile the current module"
complete -c cjpm -n __fish_use_subcommand -f -a run -d "Compile and run an executable product"
complete -c cjpm -n __fish_use_subcommand -f -a test -d "Unittest a local package or module"
complete -c cjpm -n __fish_use_subcommand -f -a bench -d "Run benchmarks in a local package or module"
complete -c cjpm -n __fish_use_subcommand -f -a clean -d "Clean up the target directory"
complete -c cjpm -n __fish_use_subcommand -f -a install -d "Install a cangjie binary"
complete -c cjpm -n __fish_use_subcommand -f -a uninstall -d "Uninstall a cangjie binary"
# 'init' subcommand options
complete -c cjpm -n "__fish_seen_subcommand_from init" -f -l help -s h -d "Help for init"
complete -c cjpm -n "__fish_seen_subcommand_from init" -f -l workspace -d "Initialize a workspace's default configuration file"
complete -c cjpm -n "__fish_seen_subcommand_from init" -f -l name -d "Specify root package name (default: current directory)" -r
complete -c cjpm -n "__fish_seen_subcommand_from init" -l path -d "Specify path to create the module (default: current directory)" -r
complete -c cjpm -n "__fish_seen_subcommand_from init" -f -l type -d "Define output type of current module" -r -f -a "executable static dynamic"
# 'run' subcommand options
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -l name -d "Name of the executable product to run (default: main)" -r
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -l build-args -d "Arguments to pass to the build process" -r
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -l skip-build -d "Skip compile, only run the executable product"
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -l run-args -d "Arguments to pass to the executable product" -r
complete -c cjpm -n "__fish_seen_subcommand_from run" -l target-dir -d "Specify target directory" -r
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -s g -d "Enable debug version"
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -s h -l help -d "Help for run"
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -s V -l verbose -d "Enable verbose"
complete -c cjpm -n "__fish_seen_subcommand_from run" -f -l skip-script -d "Disable script 'build.cj'"
# 'install' subcommand options
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -s h -l help -d "Help for install"
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -s V -l verbose -d "Enable verbose"
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -s m -l member -d "Specify a member module of the workspace" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -s g -d "Enable install debug version target"
complete -c cjpm -n "__fish_seen_subcommand_from install" -l path -d "Specify path of source module (default: current path)" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -l root -d "Specify path of installed binary" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l git -d "Specify URL of installed git module" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l branch -d "Specify branch of installed git module" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l tag -d "Specify tag of installed git module" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l commit -d "Specify commit ID of installed git module" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -s j -l jobs -d "Number of jobs to spawn in parallel" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l cfg -d "Enable the customized option 'cfg'"
complete -c cjpm -n "__fish_seen_subcommand_from install" -l target-dir -d "Specify target directory" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l name -d "Specify product name to install (default: all)" -r
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l skip-build -d "Install binary in target directory without building"
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l list -d "List all installed modules and their versions"
complete -c cjpm -n "__fish_seen_subcommand_from install" -f -l skip-script -d "Disable script 'build.cj'"
# 'build' subcommand options
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s h -l help -d "Help for build"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s i -l incremental -d "Enable incremental compilation"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s j -l jobs -d "Number of jobs to spawn in parallel" -r
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s V -l verbose -d "Enable verbose"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s g -d "Enable compile debug version target"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -l coverage -d "Enable coverage"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -l cfg -d "Enable the customized option 'cfg'"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s m -l member -d "Specify a member module of the workspace" -r
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -l target -d "Generate code for the given target platform" -r
complete -c cjpm -n "__fish_seen_subcommand_from build" -l target-dir -d "Specify target directory" -r
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s o -l output -d "Specify product name when compiling an executable file" -r
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -s l -l lint -d "Enable cjlint code check"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -l mock -d "Enable support of mocking classes in tests"
complete -c cjpm -n "__fish_seen_subcommand_from build" -f -l skip-script -d "Disable script 'build.cj'"
# 'test' subcommand options
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s h -l help -d "Help for test"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s j -l jobs -d "Number of jobs to spawn in parallel" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s V -l verbose -d "Enable verbose"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s g -d "Enable compile debug version tests"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s i -l incremental -d "Enable incremental compilation"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l no-run -d "Compile, but don't run tests"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l skip-build -d "Skip compile, only run tests"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l coverage -d "Enable coverage"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l cfg -d "Enable the customized option 'cfg'"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l module -d "Specify modules to test (default: current module)" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -s m -l member -d "Specify a member module of the workspace" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l target -d "Unittest for the given target platform" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -l target-dir -d "Specify target directory" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l dry-run -d "Print tests without execution"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l filter -d "Enable filter test" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l include-tags -d "Run tests with specified tags" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l exclude-tags -d "Run tests without specified tags" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l no-color -d "Enable colorless result output"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l random-seed -d "Enable random seed" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l timeout-each -d "Specify default timeout for test cases" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l parallel -d "Number of workers running tests" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l show-all-output -d "Show output for all test cases"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l no-capture-output -d "Disable test output capturing"
complete -c cjpm -n "__fish_seen_subcommand_from test" -l report-path -d "Specify path to directory of report" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l report-format -d "Specify format of report" -r
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l skip-script -d "Disable script 'build.cj'"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l no-progress -d "Disable progress report"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l progress-brief -d "Display brief progress report"
complete -c cjpm -n "__fish_seen_subcommand_from test" -f -l progress-entries-limit -d "Limit number of entries shown in progress report"

View File

@@ -0,0 +1,45 @@
# fish completion for Perl's cpan
function __fish_cpan_list_installed_modules
# Following IRC's #fish suggestion to use </dev/null as cpan might go interactive
cpan -l </dev/null | while read -l line
# Filter out unrelated messages or notifications
if string match -qr -- '^\w.*\t\w.*$' $line
string replace -r -- '\t.*' '' $line |
string escape --style=script
end
end
end
complete -c cpan -s a -d "Creates a CPAN.pm autobundle with CPAN::Shell->autobundle"
complete -c cpan -s A -d "Show primary maintainer for specified module" -xa "(__fish_cpan_list_installed_modules)"
complete -c cpan -s c -d "Runs a `make clean` in the specified modules directories"
complete -c cpan -s C -d "Show the Changes files for specified module" -a "(__fish_cpan_list_installed_modules)"
complete -c cpan -s D -d "Show module details" -a "(__fish_cpan_list_installed_modules)"
complete -c cpan -s f -d "Force the specified action"
complete -c cpan -s F -d "Turn off CPAN.pm's attempts to lock anything"
complete -c cpan -s g -d "Download latest distribution of module to current directory" -a "(__fish_cpan_list_installed_modules)"
# complete -c cpan -s G -d "UNIMPLEMENTED"
complete -c cpan -s h -d "Print a help message and exit"
complete -c cpan -s i -d "Install specified module"
complete -c cpan -s I -d "Load 'local::lib' (think like '-I' for loading lib paths)"
complete -c cpan -s j -d "Load file with CPAN configuration data"
complete -c cpan -s J -d "Dump the configuration in the same format that CPAN.pm uses"
complete -c cpan -s l -d "list all installed modules with their versions"
complete -c cpan -s L -d "List the modules by the specified authors"
complete -c cpan -s m -d "Make the specified modules"
complete -c cpan -s M -d "Comma-separated list of mirrors to use for this run" -x
#complete -c cpan -s n -d "Do a dry run, but dont actually install anything. (unimplemented)"
complete -c cpan -s O -d "Show the out-of-date modules"
complete -c cpan -s p -d "Ping the configured mirrors and print a report"
complete -c cpan -s P -d "Find and the best mirrors available"
complete -c cpan -s r -d "Recompiles dynamically loaded modules with CPAN::Shell->recompile"
complete -c cpan -s s -d "Drop in the CPAN.pm shell"
complete -c cpan -s t -d "Run a `make test` on the specified modules"
complete -c cpan -s T -d "Do not test modules. Simply install them"
complete -c cpan -s u -d "Upgrade all installed modules"
complete -c cpan -s v -d "Print the script version and CPAN.pm version then exit"
complete -c cpan -s V -d "Print detailed information about the cpan client"
# complete -c cpan -s w -d "UNIMPLEMENTED"
complete -c cpan -s x -d "Find close matches to named module. Requires Text::Levenshtein or others"
complete -c cpan -s X -d "Dump all the namespaces to standard output"

View File

@@ -1,5 +1,6 @@
complete -c fish_indent -s h -l help -d 'Display help and exit'
complete -c fish_indent -s v -l version -d 'Display version and exit'
complete -c fish_indent -s c -l check -d 'Do not indent, only return 0 if the code is already indented as fish_indent would'
complete -c fish_indent -s i -l no-indent -d 'Do not indent output, only reformat into one job per line'
complete -c fish_indent -l only-indent -d 'Do not reformat, only indent lines'
complete -c fish_indent -l only-unindent -d 'Do not reformat, only unindent lines'

View File

@@ -2023,8 +2023,9 @@ __fish_git_add_revision_completion -n '__fish_git_using_command rebase'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l continue -d 'Restart the rebasing process'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l abort -d 'Abort the rebase operation'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l edit-todo -d 'Edit the todo list'
complete -f -c git -n '__fish_git_using_command rebase' -l keep-empty -d "Keep the commits that don't change anything"
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l skip -d 'Restart the rebasing process by skipping the current patch'
complete -f -c git -n '__fish_git_using_command rebase' -l keep-empty -d "Keep the commits that don't change anything"
complete -f -c git -n '__fish_git_using_command rebase' -l keep-base -d 'Keep the base commit as-is'
complete -f -c git -n '__fish_git_using_command rebase' -s m -l merge -d 'Use merging strategies to rebase'
complete -f -c git -n '__fish_git_using_command rebase' -s q -l quiet -d 'Be quiet'
complete -f -c git -n '__fish_git_using_command rebase' -s v -l verbose -d 'Be verbose'

View File

@@ -0,0 +1 @@
hubble completion fish 2>/dev/null | source

View File

@@ -0,0 +1 @@
k9s completion fish 2>/dev/null | source

View File

@@ -24,6 +24,7 @@ complete -c objdump -l target -s b -d "Specify target object format" -x -a "elf6
complete -c objdump -l architecture -s m -d "Specify target architecture" -x -a "i386 i386:x86-64 i386:x64-32 i8086 i386:intel i386:x86-64:intel i386:x64-32:intel i386:nacl i386:x86-64:nacl i386:x64-32:nacl iamcu iamcu:intel l1om l1om:intel k1om k1om:intel plugin"
complete -c objdump -l section -s j -d "Only display information for given section" -x
complete -c objdump -l disassembler-options -s M -d "Pass given options on to disassembler" -x
complete -c objdump -l disassembler-color -d "Control disassembler syntax highlighting style" -x -a "off terminal on extended"
complete -c objdump -l endian -x -d "Set format endianness when disassembling" -a "big little"
complete -c objdump -o EB -d "Assume big endian format when disassembling"
complete -c objdump -o EL -d "Assume little endian format when disassembling"

View File

@@ -2,11 +2,16 @@ function __fish_ollama_list
ollama list 2>/dev/null | tail -n +2 | string replace --regex "\s.*" ""
end
function __fish_ollama_ps
ollama ps 2>/dev/null | tail -n +2 | string replace --regex "\s.*" ""
end
complete -f -c ollama
complete -c ollama -n __fish_use_subcommand -a serve -d "Start ollama"
complete -c ollama -n __fish_use_subcommand -a create -d "Create a model from a Modelfile"
complete -c ollama -n __fish_use_subcommand -a show -d "Show information for a model"
complete -c ollama -n __fish_use_subcommand -a run -d "Run a model"
complete -c ollama -n __fish_use_subcommand -a stop -d "Stop a running model."
complete -c ollama -n __fish_use_subcommand -a pull -d "Pull a model from a registry"
complete -c ollama -n __fish_use_subcommand -a push -d "Push a model to a registry"
complete -c ollama -n __fish_use_subcommand -a list -d "List models"
@@ -19,3 +24,4 @@ complete -c ollama -f -a "(__fish_ollama_list)" --condition '__fish_seen_subcomm
complete -c ollama -f -a "(__fish_ollama_list)" --condition '__fish_seen_subcommand_from run'
complete -c ollama -f -a "(__fish_ollama_list)" --condition '__fish_seen_subcommand_from cp'
complete -c ollama -f -a "(__fish_ollama_list)" --condition '__fish_seen_subcommand_from rm'
complete -c ollama -f -a "(__fish_ollama_ps)" --condition '__fish_seen_subcommand_from stop'

View File

@@ -0,0 +1,9 @@
complete -c protontricks-launch -f -s h -l help -d 'Show help message and exit'
complete -c protontricks-launch -l no-term -d 'Specify no terminal is available for errors, use dialogs instead'
complete -c protontricks-launch -s v -l verbose -d 'Increase log verbosity, can be supplied twice'
complete -c protontricks-launch -l no-runtime -d 'Disable Steam Runtime'
complete -c protontricks-launch -l no-bwrap -d 'Disable bwrap containerization when using Steam Runtime'
complete -c protontricks-launch -l background-wineserver -d 'Launch a wineserver process to improve Wine startup time'
complete -c protontricks-launch -l no-background-wineserver -d 'Do not launch wineserver process'
complete -c protontricks-launch -l appid -xka '(__fish_protontricks_complete_appid)'
complete -c protontricks-launch -l cwd-app -d 'Change to the Steam app directory when launching command'

View File

@@ -0,0 +1,24 @@
function __fish_protontricks_complete_winetricks_command
complete -C 'winetricks '
end
function __fish_protontricks_is_search
__fish_contains_opt -s s search
end
complete -c protontricks -f
complete -c protontricks -n 'not __fish_protontricks_is_search' -n '__fish_is_nth_token 1' -ka '(__fish_protontricks_complete_appid)'
complete -c protontricks -n 'not __fish_protontricks_is_search' -n 'not __fish_is_nth_token 1' -a '(__fish_protontricks_complete_winetricks_command)'
complete -c protontricks -s h -l help -d 'Show help message and exit'
complete -c protontricks -s v -l verbose -d 'Increase log verbosity, can be supplied twice'
complete -c protontricks -l no-term -d 'Specify that no terminal is available to Protontricks'
complete -c protontricks -s s -l search -d 'Search for game(s) with the given name'
complete -c protontricks -s l -l list -d 'List all apps'
complete -c protontricks -s c -l command -xa '(__fish_complete_subcommand)' -d 'Run a command with Wine-related environment variables set'
complete -c protontricks -l gui -d 'Launch the Protontricks GUI'
complete -c protontricks -l no-runtime -d 'Disable Steam Runtime'
complete -c protontricks -l no-bwrap -d 'Disable bwrap containerization when using Steam Runtime'
complete -c protontricks -l background-wineserver -d 'Launch a wineserver process to improve Wine startup time'
complete -c protontricks -l no-background-wineserver -d 'Do not launch wineserver process'
complete -c protontricks -l cwd-app -d 'Change to the Steam app directory when launching command'
complete -c protontricks -s V -l version -d 'Show version number and exit'

View File

@@ -3,8 +3,8 @@ set -l commands list-units list-sockets start stop reload restart try-restart re
isolate kill is-active is-failed status show get-cgroup-attr set-cgroup-attr unset-cgroup-attr set-cgroup help \
reset-failed list-unit-files enable disable is-enabled reenable preset mask unmask link load list-jobs cancel dump \
list-dependencies snapshot delete daemon-reload daemon-reexec show-environment set-environment unset-environment \
default rescue emergency halt poweroff reboot kexec exit suspend hibernate hybrid-sleep switch-root list-timers \
set-property import-environment get-default list-automounts is-system-running try-reload-or-restart freeze \
default rescue emergency halt poweroff reboot kexec exit suspend suspend-then-hibernate hibernate hybrid-sleep switch-root \
list-timers set-property import-environment get-default list-automounts is-system-running try-reload-or-restart freeze \
thaw mount-image bind clean
if test $systemd_version -gt 208 2>/dev/null
set commands $commands cat

View File

@@ -0,0 +1,31 @@
# fish completion for t-rec (https://github.com/sassman/t-rec-rs
function __fish_t_rec_time_unit
set -l cur (commandline --current-token)
if string match -qr '^\d+$' -- $cur
echo $cur"ms"\t"milliseconds"
echo $cur"s"\t"seconds"
echo $cur"m"\t"minutes"
end
end
function __fish_t_rec_window_list
string replace -r -- '\s*(.*)\|\s+(\d+)' '$2\t$1' (t-rec --ls | tail -n +2)
end
# Options
complete -c t-rec -d "Command to run instead of shell" -xa "(complete -C '' | string split \t -f1)"
complete -c t-rec -s v -l verbose -d "Enable verbose insights for the curious"
complete -c t-rec -s q -l quiet -d "Quiet mode, suppresses the banner"
complete -c t-rec -s m -l video -d "Generate both gif and mp4 video"
complete -c t-rec -s M -l video-only -d "Generate only mp4 video, not gif"
complete -c t-rec -s d -l decor -d "Decorate animation" -xa "shadow none"
complete -c t-rec -s b -l bg -d "Background color when decors are used" -xa "white black transparent"
complete -c t-rec -s n -l natural -d "Natural typing, disables idle detection and sampling optimization"
complete -c t-rec -s l -l ls -d "List windows available for recording by their id"
complete -c t-rec -s w -l win-id -d "Id of window to capture" -xa "(__t_rec_window_list)"
complete -c t-rec -s e -l end-pause -d "Pause time at end of animation" -xa "(__fish_t_rec_time_unit)" -r
complete -c t-rec -s s -l start-pause -d "Pause time at start of animation" -xa "(__fish_t_rec_time_unit)" -r
complete -c t-rec -s o -l output -d "Output file (without extension); defaults to t-rec" -r
complete -c t-rec -s h -l help -d "Print help"
complete -c t-rec -s V -l version -d "Print version"

View File

@@ -0,0 +1,52 @@
function __fish_tmuxp_ls
tmuxp ls 2>/dev/null
end
complete -c tmuxp -f
complete -c tmuxp -s h -l help -d Help
complete -c tmuxp -s V -l version -d Version
complete -c tmuxp -l log-level -x -a "debug info warning error critical" -d "Log level"
complete -c tmuxp -n __fish_use_subcommand -a load -d "Load tmuxp workspace"
complete -c tmuxp -n __fish_use_subcommand -a shell -d "Launch python shell"
complete -c tmuxp -n __fish_use_subcommand -a import -d "Import workspace"
complete -c tmuxp -n __fish_use_subcommand -a convert -d "Convert workspace"
complete -c tmuxp -n __fish_use_subcommand -a debug-info -d "Print diagnostics"
complete -c tmuxp -n __fish_use_subcommand -a ls -d "List workspaces"
complete -c tmuxp -n __fish_use_subcommand -a edit -d "Edit workspace"
complete -c tmuxp -n __fish_use_subcommand -a freeze -d "Freeze session to worskpace"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -a "(__fish_tmuxp_ls)"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s L -r -F -d "Passthru to tmux -L"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s S -r -F -d "Passthru to tmux -S"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s f -r -F -d "Passthru to tmux -f"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s s -r -d "Session name"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s d -d "Detached session"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s a -r -d "Attach current windows to session"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s y -l yes -d "Always answer yes"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s 2 -d "Assume 256 color support"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -s 8 -d "Assume 88 color support"
complete -c tmuxp -n "__fish_seen_subcommand_from load" -l log-file -r -F -d "Log file path"
complete -c tmuxp -n "__fish_seen_subcommand_from import" -a "teamocil tmuxinator"
complete -c tmuxp -n "__fish_seen_subcommand_from convert" -s y -l yes -d "Always answer yes"
complete -c tmuxp -n "__fish_seen_subcommand_from convert" -r -F
complete -c tmuxp -n "__fish_seen_subcommand_from edit" -a "(__fish_tmuxp_ls)"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s L -r -F -d "Passthru to tmux -L"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s S -r -F -d "Passthru to tmux -S"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s f -l worskpace-format -x -a "json yaml" -d "File format"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s o -l save-to -r -F -d "Output file"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s y -l yes -d "Always answer yes"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -s q -l quiet -d "Do not prompt for confirmation"
complete -c tmuxp -n "__fish_seen_subcommand_from freeze" -l force -d "Overwrite existing file"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -s L -r -F -d "Passthru to tmux -L"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -s S -r -F -d "Passthru to tmux -S"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -s c -r -d "Run code and exit"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l best -d "Use best shell available"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l pdb -d "Use pdb"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l code -d "Use code.interact()"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l ptipython -d "Use ptpython+ipython"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l ptpython -d "Use ptpython"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l ipython -d "Use ipython"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l bpython -d "Use bpython"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l use-pythonrc -d "Load ~/.pythonrc.py"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l no-startup -d "Do not load ~/.pythonrc.py"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l use-vi-mode -d "Use vi input mode"
complete -c tmuxp -n "__fish_seen_subcommand_from shell" -l no-vi-mode -d "Do not use vi input mode"

View File

@@ -0,0 +1 @@
volta completions fish 2>/dev/null | source

View File

@@ -76,6 +76,11 @@ function __fish_print_hostnames -d "Print a list of known hostnames"
set -l new_paths
for path in $paths
# while ssh_config is using brackets to resolve env, they should be removed
# example
# in ssh_config: ${SOME_PATH}
# in fish: $SOME_PATH
set path (string replace -r '\${([^}]+)}' '$1' $path)
set -l expanded_path
# Scope "relative" paths in accordance to ssh path resolution
if string match -qrv '^[~/]' $path

View File

@@ -0,0 +1,5 @@
function __fish_protontricks_complete_appid
protontricks -l |
string match --regex '.*\(\d+\)' |
string replace --regex '(.*) \((\d+)\)' '$2\t$1'
end

View File

@@ -52,11 +52,13 @@ function fish_default_key_bindings -d "emacs-like key binds"
bind --preset $argv alt-c capitalize-word
if test (__fish_uname) = Darwin
bind --preset $argv alt-backspace backward-kill-word
bind --preset $argv ctrl-alt-h backward-kill-word
bind --preset $argv ctrl-backspace backward-kill-token
bind --preset $argv alt-delete kill-word
bind --preset $argv ctrl-delete kill-token
else
bind --preset $argv alt-backspace backward-kill-token
bind --preset $argv ctrl-alt-h backward-kill-token
bind --preset $argv ctrl-backspace backward-kill-word
bind --preset $argv alt-delete kill-token
bind --preset $argv ctrl-delete kill-word

View File

@@ -1,3 +1,80 @@
alias fish_vi_dec 'fish_vi_inc_dec dec'
alias fish_vi_inc 'fish_vi_inc_dec inc'
# TODO: Currently we do not support hexadecimal and octal values.
function fish_vi_inc_dec --description 'increment or decrement the number below the cursor'
# The cursor is zero based, but all string functions assume 1 to be
# the lowest index. Adjust accordingly.
set --local cursor (math -- (commandline --cursor) + 1)
set --local line (commandline --current-buffer | string collect)
set --local candidate (string sub --start $cursor -- $line | string collect)
if set --local just_found (string match --regex '^-?[0-9]+' -- $candidate)
# Search from the current cursor position backwards for as long as we
# can identify a valid number.
set --function found $just_found
set --function found_at $cursor
set --local end (math -- $cursor + (string length -- $found) - 1)
set i (math -- $cursor - 1)
while [ $i -ge 1 ]
set candidate (string sub --start $i --end $end -- $line)
if set just_found (string match --regex '^-?[0-9]+$' -- $candidate)
set found $just_found
set found_at $i
# We found a candidate, but continue to make sure that we captured
# the complete number and not just part of it.
else
# We have already found a number earlier. Work with that.
break
end
set i (math -- $i - 1)
end
else
# We didn't find a match below the cursor. Mirror Vim behavior by
# checking ahead as well.
for i in (seq (math -- $cursor + 1) (math -- (string length -- $line) - 1))
set candidate (string sub --start $i -- $line | string collect)
if set just_found (string match --regex '^-?[0-9]+' -- $candidate)
set found $just_found
set found_at $i
break
end
end
if [ -z "$found" ]
return
end
end
if [ $argv = inc ]
set number (math -- $found + 1)
else if [ $argv = dec ]
set number (math -- $found - 1)
end
set --local number_abs (string trim --left --chars=- -- $number)
set --local signed $status
set --local found_abs (string trim --left --chars=- -- $found)
set number (string pad --char 0 --width (string length -- $found_abs) -- $number_abs)
if test $signed -eq 0
set number "-$number"
end
# `string sub` may bitch about `--end` being zero if `found_at` is 1.
# So ignore errors here...
set --local before (string sub --end (math -- $found_at - 1) -- $line 2> /dev/null | string collect)
set --local after (string sub --start (math -- $found_at + (string length -- $found)) -- $line | string collect)
commandline --replace -- "$before$number$after"
# Need to subtract two here because 1) cursor is zero based 2)
# `found_at` is the index of the first character of the match, but we
# want the one before that.
commandline --cursor -- (math -- $found_at + (string length -- $number) - 2)
commandline --function -- repaint
end
function fish_vi_key_bindings --description 'vi-like key bindings for fish'
if contains -- -h $argv
or contains -- --help $argv
@@ -271,6 +348,12 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
bind -s --preset -M replace backspace backward-char
bind -s --preset -M replace shift-backspace backward-char
#
# Increment or decrement number under the cursor with ctrl+x ctrl+a
#
bind -s --preset -M default ctrl-a fish_vi_inc
bind -s --preset -M default ctrl-x fish_vi_dec
#
# visual mode
#
@@ -317,7 +400,8 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
bind -s --preset -M visual -m default '",*,y' "fish_clipboard_copy; commandline -f end-selection repaint-mode"
bind -s --preset -M visual -m default '",+,y' "fish_clipboard_copy; commandline -f end-selection repaint-mode"
bind -s --preset -M visual -m default '~' togglecase-selection end-selection repaint-mode
bind -s --preset -M visual -m default g,U togglecase-selection end-selection repaint-mode
bind -s --preset -M visual -m default g,u downcase-selection end-selection repaint-mode
bind -s --preset -M visual -m default g,U upcase-selection end-selection repaint-mode
bind -s --preset -M visual -m default ctrl-c end-selection repaint-mode
bind -s --preset -M visual -m default escape end-selection repaint-mode

View File

@@ -47,7 +47,7 @@ function funced --description 'Edit function definition'
functions --no-details -- $funcname | __fish_indent --only-unindent | __fish_indent --no-indent | read -z init
end
set -l prompt 'printf "%s%s%s> " (set_color green) '$funcname' (set_color normal)'
set -l prompt 'printf "%s%s%s> " (set_color green) $funcname (set_color normal)'
if read -p $prompt -c "$init" --shell cmd
echo -n $cmd | __fish_indent --only-unindent | read -lz cmd
eval "$cmd"

View File

@@ -315,7 +315,7 @@ def unparse_color(col):
if col["bold"]:
ret += " --bold"
if col["underline"] is not None:
ret += " --underline=" + col["underline"]
ret += " --underline=" + str(col["underline"])
if col["italics"]:
ret += " --italics"
if col["dim"]:

View File

@@ -250,6 +250,8 @@ pub fn is_same_node(lhs: &dyn Node, rhs: &dyn Node) -> bool {
}
// Same base pointer and same vtable => same object.
#[allow(renamed_and_removed_lints)]
#[allow(clippy::vtable_address_comparisons)] // for old clippy
if std::ptr::eq(lhs, rhs) {
return true;
}

View File

@@ -166,7 +166,7 @@ fn source_config_in_directory(parser: &Parser, dir: &wstr) -> bool {
parser.libdata_mut().within_fish_init = true;
let _ = parser.eval(&cmd, &IoChain::new());
parser.libdata_mut().within_fish_init = false;
return true;
true
}
/// Parse init files. exec_path is the path of fish executable as determined by argv[0].

View File

@@ -636,19 +636,16 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
transient = parser.libdata().transient_commandline.clone().unwrap();
current_buffer = &transient;
current_cursor_pos = transient.len();
} else if rstate.initialized {
} else if is_interactive_session() {
current_buffer = &rstate.text;
current_cursor_pos = rstate.cursor_pos;
} else {
// There is no command line, either because we are not interactive, or because we are
// interactive and are still reading init files (in which case we silently ignore this).
if !is_interactive_session() {
streams.err.append(cmd);
streams
.err
.append(L!(": Can not set commandline in non-interactive mode\n"));
builtin_print_error_trailer(parser, streams.err, cmd);
}
// There is no command line because we are not interactive.
streams.err.append(cmd);
streams
.err
.append(L!(": Can not set commandline in non-interactive mode\n"));
builtin_print_error_trailer(parser, streams.err, cmd);
return Err(STATUS_CMD_ERROR);
}

View File

@@ -8,6 +8,7 @@
use crate::parse_constants::ParseErrorList;
use crate::parse_util::parse_util_detect_errors_in_argument_list;
use crate::parse_util::{parse_util_detect_errors, parse_util_token_extent};
use crate::proc::is_interactive_session;
use crate::reader::{commandline_get_state, completion_apply_to_command_line};
use crate::wcstringutil::string_suffixes_string;
use crate::{
@@ -465,11 +466,12 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
None => {
// No argument given, try to use the current commandline.
let commandline_state = commandline_get_state(true);
if !commandline_state.initialized {
// This corresponds to using 'complete -C' in non-interactive mode.
// See #2361 .
builtin_missing_argument(parser, streams, cmd, L!("-C"), true);
return Err(STATUS_INVALID_ARGS);
if !is_interactive_session() {
streams.err.append(cmd);
streams
.err
.append(L!(": Can not get commandline in non-interactive mode\n"));
return Err(STATUS_CMD_ERROR);
}
commandline_state.text
}

View File

@@ -110,7 +110,7 @@ fn parse_numeric_sequence<I>(chars: I) -> Option<(usize, u8)>
return None;
}
let mut val = 0;
let mut val: u8 = 0;
let mut consumed = start;
for digit in chars
.skip(start)
@@ -120,7 +120,7 @@ fn parse_numeric_sequence<I>(chars: I) -> Option<(usize, u8)>
// base is either 8 or 16, so digit can never be >255
let digit = u8::try_from(digit).unwrap();
val = val * base + digit;
val = val.wrapping_mul(base).wrapping_add(digit);
consumed += 1;
}

View File

@@ -2,10 +2,10 @@
use crate::fds::make_fd_blocking;
use crate::proc::Pid;
use crate::reader::reader_write_title;
use crate::reader::{reader_save_screen_state, reader_write_title};
use crate::tokenizer::tok_command;
use crate::wutil::perror;
use crate::{env::EnvMode, proc::TtyTransfer};
use crate::{env::EnvMode, tty_handoff::TtyHandoff};
use libc::{STDIN_FILENO, TCSADRAIN};
use super::prelude::*;
@@ -139,12 +139,13 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
// Note if tty transfer fails, we still try running the job.
parser.job_promote_at(job_pos);
let mut handoff = TtyHandoff::new(reader_save_screen_state);
let _ = make_fd_blocking(STDIN_FILENO);
{
let job_group = job.group();
job_group.set_is_foreground(true);
if job.entitled_to_terminal() {
crate::input_common::terminal_protocols_disable_ifn();
handoff.disable_tty_protocols();
}
let tmodes = job_group.tmodes.borrow();
if job_group.wants_terminal() && tmodes.is_some() {
@@ -155,16 +156,15 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
}
}
}
let mut transfer = TtyTransfer::new();
transfer.to_job_group(job.group.as_ref().unwrap());
handoff.to_job_group(job.group.as_ref().unwrap());
let resumed = job.resume();
if resumed {
job.continue_job(parser);
}
if job.is_stopped() {
transfer.save_tty_modes();
handoff.save_tty_modes();
}
transfer.reclaim();
handoff.reclaim();
if resumed {
Ok(SUCCESS)
} else {

View File

@@ -359,6 +359,7 @@ fn compute_multi_line_brace_statement_locations(&self) -> Vec<usize> {
{
next_newline += 1;
}
#[allow(clippy::nonminimal_bool)] // for old clippy; false positive?
let contains_newline = next_newline != newline_offsets.len() && {
let newline_offset = newline_offsets[next_newline];
assert!(newline_offset >= brace_statement.source_range().start());

View File

@@ -7,7 +7,7 @@
//!
//! Type "exit" or "quit" to terminate the program.
use std::{cell::RefCell, ops::ControlFlow, os::unix::prelude::OsStrExt, sync::atomic::Ordering};
use std::{cell::RefCell, ops::ControlFlow, os::unix::prelude::OsStrExt};
use libc::{STDIN_FILENO, TCSANOW, VEOF, VINTR};
use once_cell::unsync::OnceCell;
@@ -20,8 +20,8 @@
env::{env_init, EnvStack, Environment},
future_feature_flags,
input_common::{
match_key_event_to_key, terminal_protocol_hacks, terminal_protocols_enable_ifn, CharEvent,
InputEventQueue, InputEventQueuer, KeyEvent, QueryResponseEvent, TerminalQuery,
match_key_event_to_key, CharEvent, InputEventQueue, InputEventQueuer, KeyEvent,
QueryResponseEvent, TerminalQuery,
},
key::{char_to_symbol, Key},
nix::isatty,
@@ -30,9 +30,13 @@
proc::set_interactive_session,
reader::{check_exit_loop_maybe_warning, initial_query, reader_init},
signal::signal_set_handlers,
terminal::{Capability, KITTY_KEYBOARD_SUPPORTED},
terminal::Capability,
threads,
topic_monitor::topic_monitor_init,
tty_handoff::{
get_kitty_keyboard_capability, initialize_tty_metadata, set_kitty_keyboard_capability,
TtyHandoff,
},
wchar::prelude::*,
wgetopt::{wopt, ArgType, WGetopter, WOption},
};
@@ -87,16 +91,16 @@ fn process_input(streams: &mut IoStreams, continuous_mode: bool, verbose: bool)
let mut recent_chars = vec![];
streams.err.appendln("Press a key:\n");
while (!first_char_seen || continuous_mode) && !check_exit_loop_maybe_warning(None) {
terminal_protocols_enable_ifn();
let mut handoff = TtyHandoff::new(|| {});
handoff.enable_tty_protocols();
while (!first_char_seen || continuous_mode) && !check_exit_loop_maybe_warning(None) {
let kevt = match queue.readch() {
CharEvent::Key(kevt) => kevt,
CharEvent::Readline(_) | CharEvent::Command(_) | CharEvent::Implicit(_) => continue,
CharEvent::QueryResponse(QueryResponseEvent::PrimaryDeviceAttribute) => {
if KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed) == Capability::Unknown as _ {
KITTY_KEYBOARD_SUPPORTED
.store(Capability::NotSupported as _, Ordering::Release);
if get_kitty_keyboard_capability() == Capability::Unknown {
set_kitty_keyboard_capability(|| {}, Capability::NotSupported);
}
continue;
}
@@ -151,7 +155,7 @@ fn setup_and_process_keys(
// We need to set the shell-modes for ICRNL,
// in fish-proper this is done once a command is run.
unsafe { libc::tcsetattr(0, TCSANOW, &*shell_modes()) };
terminal_protocol_hacks();
initialize_tty_metadata();
let blocking_query: OnceCell<RefCell<Option<TerminalQuery>>> = OnceCell::new();
initial_query(&blocking_query, streams.out, None);

View File

@@ -12,16 +12,17 @@
use crate::env::READ_BYTE_LIMIT;
use crate::env::{EnvVar, EnvVarFlags};
use crate::input_common::decode_input_byte;
use crate::input_common::terminal_protocols_disable_ifn;
use crate::input_common::DecodeState;
use crate::input_common::InvalidPolicy;
use crate::nix::isatty;
use crate::reader::commandline_set_buffer;
use crate::reader::reader_save_screen_state;
use crate::reader::ReaderConfig;
use crate::reader::{reader_pop, reader_push, reader_readline};
use crate::tokenizer::Tokenizer;
use crate::tokenizer::TOK_ACCEPT_UNFINISHED;
use crate::tokenizer::TOK_ARGUMENT_LIST;
use crate::tty_handoff::TtyHandoff;
use crate::wcstringutil::split_about;
use crate::wcstringutil::split_string_tok;
use crate::wutil;
@@ -39,7 +40,7 @@ struct Options {
prompt: Option<WString>,
prompt_str: Option<WString>,
right_prompt: WString,
commandline: WString,
commandline: Option<WString>,
// If a delimiter was given. Used to distinguish between the default
// empty string and a given empty delimiter.
delimiter: Option<WString>,
@@ -100,7 +101,7 @@ fn parse_cmd_opts(
opts.array = true;
}
'c' => {
opts.commandline = w.woptarg.unwrap().to_owned();
opts.commandline = Some(w.woptarg.unwrap().to_owned());
}
'd' => {
opts.delimiter = Some(w.woptarg.unwrap().to_owned());
@@ -207,7 +208,7 @@ fn read_interactive(
silent: bool,
prompt: &wstr,
right_prompt: &wstr,
commandline: &wstr,
commandline: &Option<WString>,
inputfd: RawFd,
) -> BuiltinResult {
let mut exit_res = Ok(SUCCESS);
@@ -238,13 +239,16 @@ fn read_interactive(
s.readonly_commandline = false;
})
});
commandline_set_buffer(parser, Some(commandline.to_owned()), None);
if let Some(commandline) = commandline {
commandline_set_buffer(parser, Some(commandline.clone()), None);
}
let mline = {
let _interactive = parser.push_scope(|s| s.is_interactive = true);
let mut scoped_handoff = TtyHandoff::new(reader_save_screen_state);
scoped_handoff.enable_tty_protocols();
reader_readline(parser, NonZeroUsize::try_from(nchars).ok())
};
terminal_protocols_disable_ifn();
if let Some(line) = mline {
*buff = line;
if nchars > 0 && nchars < buff.len() {

View File

@@ -1355,7 +1355,7 @@ pub fn valid_func_name(name: &wstr) -> bool {
/// A rusty port of the C++ `write_loop()` function from `common.cpp`. This should be deprecated in
/// favor of native rust read/write methods at some point.
pub fn write_loop<Fd: AsRawFd>(fd: &Fd, buf: &[u8]) -> std::io::Result<()> {
pub fn safe_write_loop<Fd: AsRawFd>(fd: &Fd, buf: &[u8]) -> std::io::Result<()> {
let fd = fd.as_raw_fd();
let mut total = 0;
while total < buf.len() {
@@ -1374,6 +1374,8 @@ pub fn write_loop<Fd: AsRawFd>(fd: &Fd, buf: &[u8]) -> std::io::Result<()> {
Ok(())
}
pub use safe_write_loop as write_loop;
// Output writes always succeed; this adapter allows us to use it in a write-like macro.
struct OutputWriteAdapter<'a, T: Output>(&'a mut T);

View File

@@ -117,7 +117,7 @@
FLOG!(config, "Using compiled in paths:");
paths = ConfigPaths {
data: data.clone(),
data,
sysconf: PathBuf::from(SYSCONF_DIR).join("fish"),
doc: DOC_DIR.into(),
bin,

View File

@@ -622,6 +622,7 @@ pub fn use_posix_spawn() -> bool {
}
/// Whether or not we are running on an OS where we allow ourselves to use `posix_spawn()`.
#[allow(clippy::needless_bool)] // for old clippy
const fn allow_use_posix_spawn() -> bool {
// OpenBSD's posix_spawn returns status 127 instead of erroring with ENOEXEC when faced with a
// shebang-less script. Disable posix_spawn on OpenBSD.

View File

@@ -38,12 +38,12 @@
use crate::proc::{
hup_jobs, is_interactive_session, jobs_requiring_warning_on_exit, no_exec,
print_exit_warning_for_jobs, InternalProc, Job, JobGroupRef, ProcStatus, Process, ProcessType,
TtyTransfer,
};
use crate::reader::{reader_run_count, safe_restore_term_mode};
use crate::redirection::{dup2_list_resolve_chain, Dup2List};
use crate::threads::{iothread_perform_cant_wait, is_forked_child};
use crate::trace::trace_if_enabled_with_args;
use crate::tty_handoff::TtyHandoff;
use crate::wchar::prelude::*;
use crate::wchar_ext::ToWString;
use crate::wutil::{fish_wcstol, perror};
@@ -110,7 +110,7 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
let deferred_process = get_deferred_process(job);
// We may want to transfer tty ownership to the pgroup leader.
let mut transfer = TtyTransfer::new();
let mut handoff = TtyHandoff::new(|| {});
// This loop loops over every process_t in the job, starting it as appropriate. This turns out
// to be rather complex, since a process_t can be one of many rather different things.
@@ -175,7 +175,7 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
// Transfer tty?
if p.leads_pgrp && job.group().wants_terminal() {
transfer.to_job_group(job.group.as_ref().unwrap());
handoff.to_job_group(job.group.as_ref().unwrap());
}
}
drop(pipe_next_read);
@@ -236,9 +236,9 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
}
if job.is_stopped() {
transfer.save_tty_modes();
handoff.save_tty_modes();
}
transfer.reclaim();
handoff.reclaim();
true
}

View File

@@ -38,8 +38,6 @@
pub struct ExpandFlags : u16 {
/// Fail expansion if there is a command substitution.
const FAIL_ON_CMDSUBST = 1 << 0;
/// Skip command substitutions.
const SKIP_CMDSUBST = 1 << 14;
/// Skip variable expansion.
const SKIP_VARIABLES = 1 << 1;
/// Skip wildcard expansion.
@@ -75,6 +73,8 @@ pub struct ExpandFlags : u16 {
const SPECIAL_FOR_COMMAND = 1 << 13;
/// The token has an unclosed brace, so don't add a space.
const NO_SPACE_FOR_UNCLOSED_BRACE = 1 << 14;
/// Skip command substitutions.
const SKIP_CMDSUBST = 1 << 15;
}
}

View File

@@ -766,7 +766,7 @@ fn test_ispath() {
let tester = temp.file_tester();
let file_path = temp.filepath("file.txt");
File::create(&file_path).unwrap();
File::create(file_path).unwrap();
let result = tester.test_path(L!("file.txt"), false);
assert!(result);
@@ -794,7 +794,7 @@ fn test_ispath() {
// Directories are also files.
let dir_path = temp.filepath("somedir");
create_dir_all(&dir_path).unwrap();
create_dir_all(dir_path).unwrap();
let result = tester.test_path(L!("somedir"), false);
assert!(result);
@@ -818,7 +818,7 @@ fn test_iscdpath() {
// rather than IsFile(false).
let dir_path = temp.filepath("somedir");
create_dir_all(&dir_path).unwrap();
create_dir_all(dir_path).unwrap();
let result = tester.test_cd_path(L!("somedir"), false);
assert_eq!(result, Ok(IsFile(true)));

View File

@@ -144,6 +144,7 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat
make_md(L!("delete-char"), ReadlineCmd::DeleteChar),
make_md(L!("delete-or-exit"), ReadlineCmd::DeleteOrExit),
make_md(L!("down-line"), ReadlineCmd::DownLine),
make_md(L!("downcase-selection"), ReadlineCmd::DowncaseSelection),
make_md(L!("downcase-word"), ReadlineCmd::DowncaseWord),
make_md(L!("end-of-buffer"), ReadlineCmd::EndOfBuffer),
make_md(L!("end-of-history"), ReadlineCmd::EndOfHistory),
@@ -205,6 +206,7 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat
make_md(L!("transpose-words"), ReadlineCmd::TransposeWords),
make_md(L!("undo"), ReadlineCmd::Undo),
make_md(L!("up-line"), ReadlineCmd::UpLine),
make_md(L!("upcase-selection"), ReadlineCmd::UpcaseSelection),
make_md(L!("upcase-word"), ReadlineCmd::UpcaseWord),
make_md(L!("yank"), ReadlineCmd::Yank),
make_md(L!("yank-pop"), ReadlineCmd::YankPop),

View File

@@ -1,28 +1,18 @@
use crate::common::{
fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes,
str2wcstring, ScopeGuard, WSL,
str2wcstring, WSL,
};
use crate::env::{EnvStack, Environment};
use crate::fd_readable_set::{FdReadableSet, Timeout};
use crate::flog::{FloggableDebug, FloggableDisplay, FLOG};
use crate::fork_exec::flog_safe::FLOG_SAFE;
use crate::global_safety::RelaxedAtomicBool;
use crate::key::{
self, alt, canonicalize_control_char, canonicalize_keyed_control_char, char_to_symbol, ctrl,
function_key, shift, Key, Modifiers, ViewportPosition,
};
use crate::reader::{reader_current_data, reader_test_and_clear_interrupted};
use crate::terminal::TerminalCommand::{
ApplicationKeypadModeDisable, ApplicationKeypadModeEnable, DecrstBracketedPaste,
DecrstFocusReporting, DecsetBracketedPaste, DecsetFocusReporting,
KittyKeyboardProgressiveEnhancementsDisable, KittyKeyboardProgressiveEnhancementsEnable,
ModifyOtherKeysDisable, ModifyOtherKeysEnable,
};
use crate::terminal::{
Capability, Output, Outputter, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED,
SCROLL_FORWARD_TERMINFO_CODE,
};
use crate::threads::{iothread_port, is_main_thread};
use crate::reader::{reader_save_screen_state, reader_test_and_clear_interrupted};
use crate::terminal::{Capability, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE};
use crate::threads::iothread_port;
use crate::tty_handoff::{get_kitty_keyboard_capability, set_kitty_keyboard_capability};
use crate::universal_notifier::default_notifier;
use crate::wchar::{encode_byte_to_char, prelude::*};
use crate::wutil::encoding::{mbrtowc, mbstate_t, zero_mbstate};
@@ -31,9 +21,7 @@
use std::collections::VecDeque;
use std::mem::MaybeUninit;
use std::os::fd::RawFd;
use std::os::unix::ffi::OsStrExt;
use std::ptr;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
// The range of key codes for inputrc-style keyboard functions.
@@ -108,6 +96,8 @@ pub enum ReadlineCmd {
DowncaseWord,
CapitalizeWord,
TogglecaseChar,
UpcaseSelection,
DowncaseSelection,
TogglecaseSelection,
Execute,
BeginningOfBuffer,
@@ -522,8 +512,35 @@ enum ReadbResult {
NothingToRead,
}
fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult {
fn readb(in_fd: RawFd, blocking: bool, pasting: bool) -> ReadbResult {
let do_readb = || {
let mut arr: [u8; 1] = [0];
if read_blocked(in_fd, &mut arr) != Ok(1) {
// The terminal has been closed.
return ReadbResult::Eof;
}
let c = arr[0];
FLOG!(reader, "Read byte", char_to_symbol(char::from(c), true));
// The common path is to return a u8.
return ReadbResult::Byte(c);
};
assert!(in_fd >= 0, "Invalid in fd");
if !blocking {
return if check_fd_readable(
in_fd,
Duration::from_millis(
if pasting || get_kitty_keyboard_capability() == Capability::Supported {
300
} else {
1
},
),
) {
do_readb()
} else {
ReadbResult::NothingToRead
};
}
let mut fdset = FdReadableSet::new();
loop {
fdset.clear();
@@ -541,11 +558,7 @@ fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult {
}
// Here's where we call select().
let select_res = fdset.check_readable(if blocking {
Timeout::Forever
} else {
Timeout::Duration(Duration::from_millis(1))
});
let select_res = fdset.check_readable(Timeout::Forever);
if select_res < 0 {
let err = errno::errno().0;
if err == libc::EINTR || err == libc::EAGAIN {
@@ -557,32 +570,18 @@ fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult {
}
}
if blocking {
// select() did not return an error, so we may have a readable fd.
// The priority order is: uvars, stdin, ioport.
// Check to see if we want a universal variable barrier.
if let Some(notifier_fd) = notifier_fd {
if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd)
{
return ReadbResult::UvarNotified;
}
// select() did not return an error, so we may have a readable fd.
// The priority order is: uvars, stdin, ioport.
// Check to see if we want a universal variable barrier.
if let Some(notifier_fd) = notifier_fd {
if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd) {
return ReadbResult::UvarNotified;
}
}
// Check stdin.
if fdset.test(in_fd) {
let mut arr: [u8; 1] = [0];
if read_blocked(in_fd, &mut arr) != Ok(1) {
// The terminal has been closed.
return ReadbResult::Eof;
}
let c = arr[0];
FLOG!(reader, "Read byte", char_to_symbol(char::from(c), true));
// The common path is to return a u8.
return ReadbResult::Byte(c);
}
if !blocking {
return ReadbResult::NothingToRead;
return do_readb();
}
// Check for iothread completions only if there is no data to be read from the stdin.
@@ -593,6 +592,55 @@ fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult {
}
}
pub fn check_fd_readable(in_fd: RawFd, timeout: Duration) -> bool {
use std::ptr;
// We are not prepared to handle a signal immediately; we only want to know if we get input on
// our fd before the timeout. Use pselect to block all signals; we will handle signals
// before the next call to readch().
let mut sigs = MaybeUninit::uninit();
let mut sigs = unsafe {
libc::sigfillset(sigs.as_mut_ptr());
sigs.assume_init()
};
// pselect expects timeouts in nanoseconds.
const NSEC_PER_MSEC: u64 = 1000 * 1000;
const NSEC_PER_SEC: u64 = NSEC_PER_MSEC * 1000;
let wait_nsec: u64 = (timeout.as_millis() as u64) * NSEC_PER_MSEC;
let timeout = libc::timespec {
tv_sec: (wait_nsec / NSEC_PER_SEC).try_into().unwrap(),
tv_nsec: (wait_nsec % NSEC_PER_SEC).try_into().unwrap(),
};
// We have one fd of interest.
let mut fdset = MaybeUninit::uninit();
let mut fdset = unsafe {
libc::FD_ZERO(fdset.as_mut_ptr());
fdset.assume_init()
};
unsafe {
libc::FD_SET(in_fd, &mut fdset);
}
let res = unsafe {
libc::pselect(
in_fd + 1,
&mut fdset,
ptr::null_mut(),
ptr::null_mut(),
&timeout,
&sigs,
)
};
// Prevent signal starvation on WSL causing the `torn_escapes.py` test to fail
if is_windows_subsystem_for_linux(WSL::V1) {
// Merely querying the current thread's sigmask is sufficient to deliver a pending signal
let _ = unsafe { libc::pthread_sigmask(0, ptr::null(), &mut sigs) };
}
res > 0
}
// Update the wait_on_escape_ms value in response to the fish_escape_delay_ms user variable being
// set.
pub fn update_wait_on_escape_ms(vars: &EnvStack) {
@@ -643,120 +691,6 @@ pub fn update_wait_on_sequence_key_ms(vars: &EnvStack) {
}
}
static TERMINAL_PROTOCOLS: AtomicBool = AtomicBool::new(false);
static BRACKETED_PASTE: AtomicBool = AtomicBool::new(false);
static IS_TMUX: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub(crate) static IN_MIDNIGHT_COMMANDER: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub(crate) static IN_DVTM: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
static ITERM_NO_KITTY_KEYBOARD: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub fn terminal_protocol_hacks() {
use std::env::var_os;
IN_MIDNIGHT_COMMANDER.store(var_os("MC_TMPDIR").is_some());
IN_DVTM
.store(var_os("TERM").is_some_and(|term| term.as_os_str().as_bytes() == b"dvtm-256color"));
IS_TMUX.store(var_os("TMUX").is_some());
ITERM_NO_KITTY_KEYBOARD.store(
var_os("LC_TERMINAL").is_some_and(|term| term.as_os_str().as_bytes() == b"iTerm2")
&& var_os("LC_TERMINAL_VERSION").is_some_and(|version| {
let Some(version) = parse_version(&str2wcstring(version.as_os_str().as_bytes()))
else {
return false;
};
version < (3, 5, 12)
}),
);
}
fn parse_version(version: &wstr) -> Option<(i64, i64, i64)> {
let mut numbers = version.split('.');
let major = fish_wcstol(numbers.next()?).ok()?;
let minor = fish_wcstol(numbers.next()?).ok()?;
let patch = numbers.next()?;
let patch = &patch[..patch
.chars()
.position(|c| !c.is_ascii_digit())
.unwrap_or(patch.len())];
let patch = fish_wcstol(patch).ok()?;
Some((major, minor, patch))
}
#[test]
fn test_parse_version() {
assert_eq!(parse_version(L!("3.5.2")), Some((3, 5, 2)));
assert_eq!(parse_version(L!("3.5.3beta")), Some((3, 5, 3)));
}
pub fn terminal_protocols_enable_ifn() {
let did_write = RelaxedAtomicBool::new(false);
let _save_screen_state = ScopeGuard::new((), |()| {
if did_write.load() {
reader_current_data().map(|data| data.save_screen_state());
}
});
let mut out = Outputter::stdoutput().borrow_mut();
if !BRACKETED_PASTE.load(Ordering::Relaxed) {
BRACKETED_PASTE.store(true, Ordering::Release);
out.write_command(DecsetBracketedPaste);
if IS_TMUX.load() {
out.write_command(DecsetFocusReporting);
}
did_write.store(true);
}
let kitty_keyboard_supported = KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed);
if kitty_keyboard_supported == Capability::Unknown as _ {
return;
}
if TERMINAL_PROTOCOLS.load(Ordering::Relaxed) {
return;
}
TERMINAL_PROTOCOLS.store(true, Ordering::Release);
FLOG!(term_protocols, "Enabling extended keys");
if kitty_keyboard_supported == Capability::NotSupported as _ || ITERM_NO_KITTY_KEYBOARD.load() {
out.write_command(ModifyOtherKeysEnable); // XTerm's modifyOtherKeys
out.write_command(ApplicationKeypadModeEnable); // set application keypad mode, so the keypad keys send unique codes
} else {
out.write_command(KittyKeyboardProgressiveEnhancementsEnable);
}
did_write.store(true);
}
pub(crate) fn terminal_protocols_disable_ifn() {
let did_write = RelaxedAtomicBool::new(false);
let _save_screen_state = is_main_thread().then(|| {
ScopeGuard::new((), |()| {
if did_write.load() {
reader_current_data().map(|data| data.save_screen_state());
}
})
});
let mut out = Outputter::stdoutput().borrow_mut();
if BRACKETED_PASTE.load(Ordering::Acquire) {
out.write_command(DecrstBracketedPaste);
if IS_TMUX.load() {
out.write_command(DecrstFocusReporting);
}
BRACKETED_PASTE.store(false, Ordering::Release);
did_write.store(true);
}
if !TERMINAL_PROTOCOLS.load(Ordering::Acquire) {
return;
}
FLOG_SAFE!(term_protocols, "Disabling extended keys");
let kitty_keyboard_supported = KITTY_KEYBOARD_SUPPORTED.load(Ordering::Acquire);
assert_ne!(kitty_keyboard_supported, Capability::Unknown as _);
if kitty_keyboard_supported == Capability::NotSupported as _ || ITERM_NO_KITTY_KEYBOARD.load() {
out.write_command(ModifyOtherKeysDisable);
out.write_command(ApplicationKeypadModeDisable);
} else {
out.write_command(KittyKeyboardProgressiveEnhancementsDisable);
}
TERMINAL_PROTOCOLS.store(false, Ordering::Release);
did_write.store(true);
}
fn parse_mask(mask: u32) -> (Modifiers, bool) {
let modifiers = Modifiers {
ctrl: (mask & 4) != 0,
@@ -880,7 +814,7 @@ fn try_readch(&mut self, blocking: bool) -> Option<CharEvent> {
return Some(mevt);
}
let rr = readb(self.get_in_fd(), blocking);
let rr = readb(self.get_in_fd(), blocking, /*pasting=*/ false);
match rr {
ReadbResult::Eof => {
return Some(CharEvent::Implicit(ImplicitEvent::Eof));
@@ -923,10 +857,16 @@ fn try_readch(&mut self, blocking: bool) -> Option<CharEvent> {
let mut i = 0;
let ok = loop {
if i == buffer.len() {
buffer.push(match readb(self.get_in_fd(), /*blocking=*/ true) {
ReadbResult::Byte(b) => b,
_ => 0,
});
buffer.push(
match readb(
self.get_in_fd(),
/*blocking=*/ true,
/*pasting=*/ false,
) {
ReadbResult::Byte(b) => b,
_ => 0,
},
);
}
match decode_input_byte(
&mut seq,
@@ -1005,7 +945,11 @@ fn try_readch(&mut self, blocking: bool) -> Option<CharEvent> {
}
fn try_readb(&mut self, buffer: &mut Vec<u8>) -> Option<u8> {
let ReadbResult::Byte(next) = readb(self.get_in_fd(), /*blocking=*/ false) else {
let ReadbResult::Byte(next) = readb(
self.get_in_fd(),
/*blocking=*/ false,
self.paste_is_buffering(),
) else {
return None;
};
buffer.push(next);
@@ -1065,7 +1009,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
// The maximum number of CSI parameters is defined by NPAR, nominally 16.
let mut params = [[0_u32; 4]; 16];
let Some(mut c) = self.try_readb(buffer) else {
return Some(KeyEvent::from(ctrl('[')));
return Some(KeyEvent::from(alt('[')));
};
let mut next_char = |zelf: &mut Self| zelf.try_readb(buffer).unwrap_or(0xff);
let private_mode;
@@ -1078,7 +1022,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
}
let mut count = 0;
let mut subcount = 0;
while count < 16 && c >= 0x30 && c <= 0x3f {
while count < 16 && (0x30..=0x3f).contains(&c) {
if c.is_ascii_digit() {
// Return None on invalid ascii numeric CSI parameter exceeding u32 bounds
match params[count][subcount]
@@ -1291,7 +1235,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
reader,
"Received kitty progressive enhancement flags, marking as supported"
);
KITTY_KEYBOARD_SUPPORTED.store(Capability::Supported as _, Ordering::Release);
set_kitty_keyboard_capability(reader_save_screen_state, Capability::Supported);
return None;
}
@@ -1525,50 +1469,12 @@ fn readch_timed(&mut self, wait_time_ms: usize) -> Option<CharEvent> {
if let Some(evt) = self.try_pop() {
return Some(evt);
}
terminal_protocols_enable_ifn();
// We are not prepared to handle a signal immediately; we only want to know if we get input on
// our fd before the timeout. Use pselect to block all signals; we will handle signals
// before the next call to readch().
let mut sigs = MaybeUninit::uninit();
unsafe { libc::sigfillset(sigs.as_mut_ptr()) };
// pselect expects timeouts in nanoseconds.
const NSEC_PER_MSEC: u64 = 1000 * 1000;
const NSEC_PER_SEC: u64 = NSEC_PER_MSEC * 1000;
let wait_nsec: u64 = (wait_time_ms as u64) * NSEC_PER_MSEC;
let timeout = libc::timespec {
tv_sec: (wait_nsec / NSEC_PER_SEC).try_into().unwrap(),
tv_nsec: (wait_nsec % NSEC_PER_SEC).try_into().unwrap(),
};
// We have one fd of interest.
let mut fdset = MaybeUninit::uninit();
let in_fd = self.get_in_fd();
unsafe {
libc::FD_ZERO(fdset.as_mut_ptr());
libc::FD_SET(in_fd, fdset.as_mut_ptr());
};
let res = unsafe {
libc::pselect(
in_fd + 1,
fdset.as_mut_ptr(),
ptr::null_mut(),
ptr::null_mut(),
&timeout,
sigs.as_ptr(),
)
};
// Prevent signal starvation on WSL causing the `torn_escapes.py` test to fail
if is_windows_subsystem_for_linux(WSL::V1) {
// Merely querying the current thread's sigmask is sufficient to deliver a pending signal
let _ = unsafe { libc::pthread_sigmask(0, ptr::null(), sigs.as_mut_ptr()) };
}
if res > 0 {
return Some(self.readch());
}
None
check_fd_readable(
self.get_in_fd(),
Duration::from_millis(u64::try_from(wait_time_ms).unwrap()),
)
.then(|| self.readch())
}
/// Return the fd from which to read.

View File

@@ -16,6 +16,9 @@
#![allow(clippy::incompatible_msrv)]
#![allow(clippy::len_without_is_empty)]
#![allow(clippy::manual_is_ascii_check)]
#![allow(clippy::manual_range_contains)]
#![allow(clippy::needless_lifetimes)]
#![allow(clippy::needless_return)]
#![allow(clippy::new_without_default)]
#![allow(clippy::option_map_unit_fn)]
#![allow(clippy::too_many_arguments)]
@@ -93,6 +96,7 @@
pub mod tokenizer;
pub mod topic_monitor;
pub mod trace;
pub mod tty_handoff;
pub mod universal_notifier;
pub mod util;
pub mod wait_handle;

View File

@@ -492,7 +492,7 @@ fn expand_command(
STATUS_UNMATCHED_WILDCARD,
statement,
WILDCARD_ERR_MSG,
&self.node_source(statement)
self.node_source(statement)
);
}
ExpandResultCode::cancel => {
@@ -1084,7 +1084,7 @@ fn run_switch_statement(
STATUS_UNMATCHED_WILDCARD,
&statement.argument,
WILDCARD_ERR_MSG,
&self.node_source(&statement.argument)
self.node_source(&statement.argument)
);
}
ExpandResultCode::ok => {
@@ -1370,7 +1370,7 @@ fn expand_arguments_from_nodes(
STATUS_UNMATCHED_WILDCARD,
arg_node,
WILDCARD_ERR_MSG,
&self.node_source(*arg_node)
self.node_source(*arg_node)
);
}
}
@@ -1420,7 +1420,7 @@ fn determine_redirections(
STATUS_INVALID_ARGS,
redir_node,
"Invalid redirection: %ls",
&self.node_source(redir_node)
self.node_source(redir_node)
);
}
};
@@ -1801,7 +1801,7 @@ fn populate_job_from_job_node(
STATUS_INVALID_ARGS,
&jc.pipe,
ILLEGAL_FD_ERR_MSG,
&self.node_source(&jc.pipe)
self.node_source(&jc.pipe)
);
break;
}

View File

@@ -14,7 +14,7 @@
};
use crate::fds::{open_dir, BEST_O_SEARCH};
use crate::global_safety::RelaxedAtomicBool;
use crate::input_common::{terminal_protocols_disable_ifn, TerminalQuery};
use crate::input_common::TerminalQuery;
use crate::io::IoChain;
use crate::job_group::MaybeJobId;
use crate::operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT};
@@ -604,7 +604,7 @@ pub fn eval_file_wstr(
let sb = self.push_block(Block::source_block(filename.clone()));
let _filename_push = self
.library_data
.scoped_set(Some(filename.clone()), |s| &mut s.current_filename);
.scoped_set(Some(filename), |s| &mut s.current_filename);
let ret = self.eval_wstr(src, io, job_group, BlockType::top);
@@ -681,8 +681,6 @@ pub fn eval_node<T: Node>(
// Create a new execution context.
let mut execution_context = ExecutionContext::new(ps, block_io.clone(), &self.line_counter);
terminal_protocols_disable_ifn();
// Check the exec count so we know if anything got executed.
let prev_exec_count = self.libdata().exec_count;
let prev_status_count = self.libdata().status_count;

View File

@@ -6,8 +6,6 @@
use crate::env::{EnvMode, EnvStack, Environment};
use crate::expand::{expand_tilde, HOME_DIRECTORY};
use crate::flog::{FLOG, FLOGF};
#[cfg(not(target_os = "linux"))]
use crate::libc::{MNT_LOCAL, ST_LOCAL};
use crate::wchar::prelude::*;
use crate::wutil::{normalize_path, path_normalize_for_cd, waccess, wdirname, wstat};
use errno::{errno, set_errno, Errno};
@@ -705,25 +703,49 @@ pub fn path_remoteness(path: &wstr) -> DirRemoteness {
}
#[cfg(not(target_os = "linux"))]
{
// ST_LOCAL is a flag to statvfs, which is itself standardized.
// In practice the only system to define it is NetBSD.
let local_flag = ST_LOCAL() | MNT_LOCAL();
if local_flag != 0 {
fn remoteness_via_statfs<StatFS, Flags>(
statfn: unsafe extern "C" fn(*const i8, *mut StatFS) -> libc::c_int,
flagsfn: fn(&StatFS) -> Flags,
is_local_flag: u64,
path: &std::ffi::CStr,
) -> DirRemoteness
where
u64: From<Flags>,
{
if is_local_flag == 0 {
return DirRemoteness::unknown;
}
let mut buf = MaybeUninit::uninit();
if unsafe { libc::statfs(narrow.as_ptr(), buf.as_mut_ptr()) } < 0 {
if unsafe { (statfn)(path.as_ptr(), buf.as_mut_ptr()) } < 0 {
return DirRemoteness::unknown;
}
let buf = unsafe { buf.assume_init() };
// statfs::f_flag is hard-coded as 64-bits on 32/64-bit FreeBSD but it's a (4-byte)
// long on 32-bit NetBSD.. and always 4-bytes on macOS (even on 64-bit builds).
#[allow(clippy::useless_conversion)]
return if u64::from(buf.f_flags) & local_flag != 0 {
if u64::from((flagsfn)(&buf)) & is_local_flag != 0 {
DirRemoteness::local
} else {
DirRemoteness::remote
};
}
}
DirRemoteness::unknown
// ST_LOCAL is a flag to statvfs, which is itself standardized.
// In practice the only system to define it is NetBSD.
#[cfg(target_os = "netbsd")]
let remoteness = remoteness_via_statfs(
libc::statvfs,
|stat: &libc::statvfs| stat.f_flag,
crate::libc::ST_LOCAL(),
&narrow,
);
#[cfg(not(target_os = "netbsd"))]
let remoteness = remoteness_via_statfs(
libc::statfs,
|stat: &libc::statfs| stat.f_flags,
crate::libc::MNT_LOCAL(),
&narrow,
);
remoteness
}
}

View File

@@ -17,17 +17,15 @@
use crate::reader::{fish_is_unwinding_for_exit, reader_schedule_prompt_repaint};
use crate::redirection::RedirectionSpecList;
use crate::signal::{signal_set_handlers_once, Signal};
use crate::threads::MainThread;
use crate::topic_monitor::{topic_monitor_principal, GenerationsList, Topic};
use crate::wait_handle::{InternalJobId, WaitHandle, WaitHandleRef, WaitHandleStore};
use crate::wchar::prelude::*;
use crate::wchar_ext::ToWString;
use crate::wutil::{perror, wbasename, wperror};
use crate::wutil::{wbasename, wperror};
use libc::{
EBADF, EINVAL, ENOTTY, EPERM, EXIT_SUCCESS, SIGABRT, SIGBUS, SIGCONT, SIGFPE, SIGHUP, SIGILL,
SIGINT, SIGKILL, SIGPIPE, SIGQUIT, SIGSEGV, SIGSYS, SIGTTOU, SIG_DFL, SIG_IGN, STDIN_FILENO,
WCONTINUED, WEXITSTATUS, WIFCONTINUED, WIFEXITED, WIFSIGNALED, WIFSTOPPED, WNOHANG, WTERMSIG,
WUNTRACED, _SC_CLK_TCK,
EXIT_SUCCESS, SIGABRT, SIGBUS, SIGCONT, SIGFPE, SIGHUP, SIGILL, SIGINT, SIGKILL, SIGPIPE,
SIGQUIT, SIGSEGV, SIGSYS, SIGTTOU, SIG_DFL, SIG_IGN, WCONTINUED, WEXITSTATUS, WIFCONTINUED,
WIFEXITED, WIFSIGNALED, WIFSTOPPED, WNOHANG, WTERMSIG, WUNTRACED, _SC_CLK_TCK,
};
use once_cell::sync::Lazy;
#[cfg(not(target_has_atomic = "64"))]
@@ -35,14 +33,13 @@
use std::cell::{Cell, Ref, RefCell, RefMut};
use std::fs;
use std::io::{Read, Write};
use std::mem::MaybeUninit;
use std::num::NonZeroU32;
use std::os::fd::RawFd;
use std::rc::Rc;
#[cfg(target_has_atomic = "64")]
use std::sync::atomic::AtomicU64;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, OnceLock};
use std::sync::{Arc, Mutex, OnceLock};
/// Types of processes.
#[derive(Default)]
@@ -270,202 +267,6 @@ pub fn get_id(&self) -> u64 {
}
}
// Allows transferring the tty to a job group, while it runs.
#[derive(Default)]
pub struct TtyTransfer {
// The job group which owns the tty, or empty if none.
owner: Option<JobGroupRef>,
}
impl TtyTransfer {
pub fn new() -> Self {
Default::default()
}
/// Transfer to the given job group, if it wants to own the terminal.
#[allow(clippy::wrong_self_convention)]
pub fn to_job_group(&mut self, jg: &JobGroupRef) {
assert!(self.owner.is_none(), "Terminal already transferred");
if TtyTransfer::try_transfer(jg) {
self.owner = Some(jg.clone());
}
}
/// Reclaim the tty if we transferred it.
pub fn reclaim(&mut self) {
if self.owner.is_some() {
FLOG!(proc_pgroup, "fish reclaiming terminal");
if unsafe { libc::tcsetpgrp(STDIN_FILENO, libc::getpgrp()) } == -1 {
FLOG!(warning, "Could not return shell to foreground");
perror("tcsetpgrp");
}
self.owner = None;
}
}
/// Save the current tty modes into the owning job group, if we are transferred.
pub fn save_tty_modes(&mut self) {
if let Some(ref mut owner) = self.owner {
let mut tmodes = MaybeUninit::uninit();
if unsafe { libc::tcgetattr(STDIN_FILENO, tmodes.as_mut_ptr()) } == 0 {
owner.tmodes.replace(Some(unsafe { tmodes.assume_init() }));
} else if errno::errno().0 != ENOTTY {
perror("tcgetattr");
}
}
}
fn try_transfer(jg: &JobGroup) -> bool {
if !jg.wants_terminal() {
// The job doesn't want the terminal.
return false;
}
// Get the pgid; we must have one if we want the terminal.
let pgid = jg.get_pgid().unwrap();
// It should never be fish's pgroup.
let fish_pgrp = crate::nix::getpgrp();
assert!(
pgid.as_pid_t() != fish_pgrp,
"Job should not have fish's pgroup"
);
// Ok, we want to transfer to the child.
// Note it is important to be very careful about calling tcsetpgrp()!
// fish ignores SIGTTOU which means that it has the power to reassign the tty even if it doesn't
// own it. This means that other processes may get SIGTTOU and become zombies.
// Check who own the tty now. There's four cases of interest:
// 1. There is no tty at all (tcgetpgrp() returns -1). For example running from a pure script.
// Of course do not transfer it in that case.
// 2. The tty is owned by the process. This comes about often, as the process will call
// tcsetpgrp() on itself between fork and exec. This is the essential race inherent in
// tcsetpgrp(). In this case we want to reclaim the tty, but do not need to transfer it
// ourselves since the child won the race.
// 3. The tty is owned by a different process. This may come about if fish is running in the
// background with job control enabled. Do not transfer it.
// 4. The tty is owned by fish. In that case we want to transfer the pgid.
let current_owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
if current_owner < 0 {
// Case 1.
return false;
} else if current_owner == pgid.get() {
// Case 2.
return true;
} else if current_owner != pgid.get() && current_owner != fish_pgrp {
// Case 3.
return false;
}
// Case 4 - we do want to transfer it.
// The tcsetpgrp(2) man page says that EPERM is thrown if "pgrp has a supported value, but
// is not the process group ID of a process in the same session as the calling process."
// Since we _guarantee_ that this isn't the case (the child calls setpgid before it calls
// SIGSTOP, and the child was created in the same session as us), it seems that EPERM is
// being thrown because of an caching issue - the call to tcsetpgrp isn't seeing the
// newly-created process group just yet. On this developer's test machine (WSL running Linux
// 4.4.0), EPERM does indeed disappear on retry. The important thing is that we can
// guarantee the process isn't going to exit while we wait (which would cause us to possibly
// block indefinitely).
while unsafe { libc::tcsetpgrp(STDIN_FILENO, pgid.as_pid_t()) } != 0 {
FLOGF!(proc_termowner, "tcsetpgrp failed: %d", errno::errno().0);
// Before anything else, make sure that it's even necessary to call tcsetpgrp.
// Since it usually _is_ necessary, we only check in case it fails so as to avoid the
// unnecessary syscall and associated context switch, which profiling has shown to have
// a significant cost when running process groups in quick succession.
let getpgrp_res = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
if getpgrp_res < 0 {
match errno::errno().0 {
ENOTTY | EBADF => {
// stdin is not a tty. This may come about if job control is enabled but we are
// not a tty - see #6573.
return false;
}
_ => {
perror("tcgetpgrp");
return false;
}
}
}
if getpgrp_res == pgid.get() {
FLOGF!(
proc_termowner,
"Process group %d already has control of terminal",
pgid
);
return true;
}
let pgroup_terminated;
if errno::errno().0 == EINVAL {
// OS X returns EINVAL if the process group no longer lives. Probably other OSes,
// too. Unlike EPERM below, EINVAL can only happen if the process group has
// terminated.
pgroup_terminated = true;
} else if errno::errno().0 == EPERM {
// Retry so long as this isn't because the process group is dead.
let mut result: libc::c_int = 0;
let wait_result = unsafe { libc::waitpid(-pgid.as_pid_t(), &mut result, WNOHANG) };
if wait_result == -1 {
// Note that -1 is technically an "error" for waitpid in the sense that an
// invalid argument was specified because no such process group exists any
// longer. This is the observed behavior on Linux 4.4.0. a "success" result
// would mean processes from the group still exist but is still running in some
// state or the other.
pgroup_terminated = true;
} else {
// Debug the original tcsetpgrp error (not the waitpid errno) to the log, and
// then retry until not EPERM or the process group has exited.
FLOGF!(
proc_termowner,
"terminal_give_to_job(): EPERM with pgid %d.",
pgid
);
continue;
}
} else if errno::errno().0 == ENOTTY {
// stdin is not a TTY. In general we expect this to be caught via the tcgetpgrp
// call's EBADF handler above.
return false;
} else {
FLOGF!(
warning,
"Could not send job %d ('%ls') with pgid %d to foreground",
jg.job_id.to_wstring(),
jg.command,
pgid
);
perror("tcsetpgrp");
return false;
}
if pgroup_terminated {
// All processes in the process group has exited.
// Since we delay reaping any processes in a process group until all members of that
// job/group have been started, the only way this can happen is if the very last
// process in the group terminated and didn't need to access the terminal, otherwise
// it would have hung waiting for terminal IO (SIGTTIN). We can safely ignore this.
FLOGF!(
proc_termowner,
"tcsetpgrp called but process group %d has terminated.\n",
pgid
);
return false;
}
break;
}
true
}
}
/// The destructor will assert if reclaim() has not been called.
impl Drop for TtyTransfer {
fn drop(&mut self) {
assert!(self.owner.is_none(), "Forgot to reclaim() the tty");
}
}
/// A type-safe equivalent to [`libc::pid_t`].
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
@@ -720,19 +521,15 @@ pub fn is_completed(&self) -> bool {
/// As a process does not know its job id, we pass it in.
/// Note this will return null if the process is not waitable (has no pid).
pub fn make_wait_handle(&self, jid: InternalJobId) -> Option<WaitHandleRef> {
if !matches!(self.typ, ProcessType::External) || self.pid().is_none() {
// Not waitable.
None
} else {
if self.wait_handle.borrow().is_none() {
self.wait_handle.replace(Some(WaitHandle::new(
self.pid().unwrap(),
jid,
wbasename(&self.actual_cmd.clone()).to_owned(),
)));
}
self.get_wait_handle()
let pid = self.pid()?;
if self.wait_handle.borrow().is_none() {
self.wait_handle.replace(Some(WaitHandle::new(
pid,
jid,
wbasename(&self.actual_cmd.clone()).to_owned(),
)));
}
self.get_wait_handle()
}
}
@@ -1349,7 +1146,7 @@ pub fn hup_jobs(jobs: &JobList) {
/// Add a job to the list of PIDs/PGIDs we wait on even though they are not associated with any
/// jobs. Used to avoid zombie processes after disown.
pub fn add_disowned_job(j: &Job) {
let mut disowned_pids = DISOWNED_PIDS.get().borrow_mut();
let mut disowned_pids = DISOWNED_PIDS.lock().unwrap();
for process in j.external_procs() {
disowned_pids.push(process.pid().unwrap());
}
@@ -1357,7 +1154,7 @@ pub fn add_disowned_job(j: &Job) {
// Reap any pids in our disowned list that have exited. This is used to avoid zombies.
fn reap_disowned_pids() {
let mut disowned_pids = DISOWNED_PIDS.get().borrow_mut();
let mut disowned_pids = DISOWNED_PIDS.lock().unwrap();
// waitpid returns 0 iff the PID/PGID in question has not changed state; remove the pid/pgid
// if it has changed or an error occurs (presumably ECHILD because the child does not exist)
disowned_pids.retain(|pid| {
@@ -1372,7 +1169,7 @@ fn reap_disowned_pids() {
/// A list of pids that have been disowned. They are kept around until either they exit or
/// we exit. Poll these from time-to-time to prevent zombie processes from happening (#5342).
static DISOWNED_PIDS: MainThread<RefCell<Vec<Pid>>> = MainThread::new(RefCell::new(Vec::new()));
static DISOWNED_PIDS: Mutex<Vec<Pid>> = Mutex::new(Vec::new());
/// See if any reapable processes have exited, and mark them accordingly.
/// \param block_ok if no reapable processes have exited, block until one is (or until we receive a

View File

@@ -11,11 +11,16 @@
//! When the user searches forward, i.e. presses Alt-down, the list is consulted for previous search
//! result, and subsequent backwards searches are also handled by consulting the list up until the
//! end of the list is reached, at which point regular searching will commence.
//!
//! In general interactive reads work with the tty protocols (CSI-U, etc) enabled; these are disabled
//! before calling out to fish script, wildcards, or completions. Note CSI-U protocol prevents
//! control-C from generating SIGINT, so failing to disable these would prevent cancellation of wildcard
//! expansion, etc.
use libc::{
c_char, ECHO, EINTR, EIO, EISDIR, ENOTTY, EPERM, ESRCH, ICANON, ICRNL, IEXTEN, INLCR, IXOFF,
IXON, ONLCR, OPOST, O_NONBLOCK, O_RDONLY, SIGINT, SIGTTIN, STDIN_FILENO, STDOUT_FILENO,
TCSANOW, VMIN, VQUIT, VSUSP, VTIME, _POSIX_VDISABLE,
IXON, ONLCR, OPOST, O_NONBLOCK, O_RDONLY, SIGINT, SIGTTIN, STDERR_FILENO, STDIN_FILENO,
STDOUT_FILENO, TCSANOW, VMIN, VQUIT, VSUSP, VTIME, _POSIX_VDISABLE,
};
use nix::fcntl::OFlag;
use nix::sys::stat::Mode;
@@ -81,17 +86,9 @@
SearchType,
};
use crate::input::init_input;
use crate::input_common::stop_query;
use crate::input_common::terminal_protocols_disable_ifn;
use crate::input_common::CursorPositionQuery;
use crate::input_common::ImplicitEvent;
use crate::input_common::QueryResponseEvent;
use crate::input_common::TerminalQuery;
use crate::input_common::IN_DVTM;
use crate::input_common::IN_MIDNIGHT_COMMANDER;
use crate::input_common::{
terminal_protocol_hacks, terminal_protocols_enable_ifn, CharEvent, CharInputStyle, InputData,
ReadlineCmd,
stop_query, CharEvent, CharInputStyle, CursorPositionQuery, ImplicitEvent, InputData,
QueryResponseEvent, ReadlineCmd, TerminalQuery,
};
use crate::io::IoChain;
use crate::key::ViewportPosition;
@@ -134,9 +131,7 @@
QueryCursorPosition, QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute,
QueryXtgettcap, QueryXtversion,
};
use crate::terminal::{
Capability, KITTY_KEYBOARD_SUPPORTED, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE,
};
use crate::terminal::{Capability, SCROLL_FORWARD_SUPPORTED, SCROLL_FORWARD_TERMINFO_CODE};
use crate::termsize::{termsize_invalidate_tty, termsize_last, termsize_update};
use crate::text_face::parse_text_face;
use crate::text_face::TextFace;
@@ -150,6 +145,10 @@
tok_command, MoveWordStateMachine, MoveWordStyle, TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED,
TOK_SHOW_COMMENTS,
};
use crate::tty_handoff::{
get_kitty_keyboard_capability, get_tty_protocols_active, initialize_tty_metadata,
safe_deactivate_tty_protocols, set_kitty_keyboard_capability, tty_metadata, TtyHandoff,
};
use crate::wchar::prelude::*;
use crate::wcstringutil::string_prefixes_string_maybe_case_insensitive;
use crate::wcstringutil::{
@@ -232,7 +231,6 @@ fn debounce_history_pager() -> &'static Debounce {
}
fn redirect_tty_after_sighup() {
use libc::{EIO, ENOTTY, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
use std::fs::OpenOptions;
// If we have received SIGHUP, redirect the tty to avoid a user script triggering SIGTTIN or
@@ -265,10 +263,8 @@ pub(crate) fn initial_query(
vars: Option<&dyn Environment>,
) {
blocking_query.get_or_init(|| {
let query = if is_dumb()
|| IN_MIDNIGHT_COMMANDER.load()
|| IN_DVTM.load()
|| !isatty(STDOUT_FILENO)
let md = tty_metadata();
let query = if is_dumb() || md.in_midnight_commander || md.in_dvtm || !isatty(STDOUT_FILENO)
{
None
} else {
@@ -318,11 +314,12 @@ pub fn reader_push<'a>(parser: &'a Parser, history_name: &wstr, conf: ReaderConf
assert_is_main_thread();
let hist = History::with_name(history_name);
hist.resolve_pending();
let data = ReaderData::new(hist, conf);
let is_top_level = reader_data_stack().is_empty();
let data = ReaderData::new(hist, conf, is_top_level);
reader_data_stack().push(data);
let data = current_data().unwrap();
data.command_line_changed(EditableLineTag::Commandline, AutosuggestionUpdate::Remove);
if reader_data_stack().len() == 1 {
if is_top_level {
reader_interactive_init(parser);
}
Reader { data, parser }
@@ -401,8 +398,6 @@ pub struct CommandlineState {
pub search_field: Option<(WString, usize)>,
/// pager is visible and search is active
pub search_mode: bool,
/// if false, the reader has not yet been entered
pub initialized: bool,
}
impl CommandlineState {
@@ -416,7 +411,6 @@ const fn new() -> Self {
pager_fully_disclosed: false,
search_field: None,
search_mode: false,
initialized: false,
}
}
}
@@ -719,6 +713,11 @@ fn read_i(parser: &Parser) {
let mut data = reader_push(parser, &history_session_id(parser.vars()), conf);
data.import_history_if_necessary();
// Set up tty protocols. These should be enabled while we're reading interactively,
// and disabled before we run fish script, wildcards, or completions. This is scoped.
// Note this may be disabled within the loop, e.g. when running fish script bound to keys.
let mut tty = TtyHandoff::new(reader_save_screen_state);
while !check_exit_loop_maybe_warning(Some(&mut data)) {
RUN_COUNT.fetch_add(1, Ordering::Relaxed);
@@ -730,6 +729,8 @@ fn read_i(parser: &Parser) {
continue;
}
// Got a command. Disable tty protocols while we execute it.
tty.disable_tty_protocols();
data.clear(EditableLineTag::Commandline);
data.update_buff_pos(EditableLineTag::Commandline, None);
BufferedOutputter::new(Outputter::stdoutput()).write_command(Osc133CommandStart(&command));
@@ -765,7 +766,8 @@ fn read_i(parser: &Parser) {
}
reader_pop();
// If we got SIGHUP, ensure the tty is redirected.
// If we got SIGHUP, ensure the tty is redirected and release tty handoff without
// trying to muck with protocols.
if reader_received_sighup() {
// If we are the top-level reader, then we translate SIGHUP into exit_forced.
redirect_tty_after_sighup();
@@ -899,11 +901,9 @@ pub fn reader_init(will_restore_foreground_pgroup: bool) {
}
}
// TODO(pca): this is run in our "AT_EXIT" handler from a SIGTERM handler.
// It must be made async-signal-safe (or not invoked).
pub fn reader_deinit(restore_foreground_pgroup: bool) {
safe_restore_term_mode();
crate::input_common::terminal_protocols_disable_ifn();
safe_deactivate_tty_protocols();
if restore_foreground_pgroup {
restore_term_foreground_process_group_for_exit();
}
@@ -1205,12 +1205,18 @@ fn reader_received_sighup() -> bool {
}
impl ReaderData {
fn new(history: Arc<History>, conf: ReaderConfig) -> Pin<Box<Self>> {
fn new(history: Arc<History>, conf: ReaderConfig, is_top_level: bool) -> Pin<Box<Self>> {
let input_data = InputData::new(conf.inputfd);
let mut command_line = EditableLine::default();
if is_top_level {
let state = commandline_state_snapshot();
command_line.push_edit(Edit::new(0..0, state.text.clone()), false);
command_line.set_position(state.cursor_pos);
}
Pin::new(Box::new(Self {
canary: Rc::new(()),
conf,
command_line: Default::default(),
command_line,
command_line_transient_edit: None,
rendered_layout: Default::default(),
autosuggestion: Default::default(),
@@ -1371,7 +1377,6 @@ fn update_commandline_state(&self) {
});
}
snapshot.search_mode = self.history_search.active();
snapshot.initialized = true;
}
/// Apply any changes from the reader snapshot. This is called after running fish script,
@@ -1479,6 +1484,10 @@ pub fn mouse_left_click(&mut self, cursor: ViewportPosition, click_position: Vie
}
}
pub fn reader_save_screen_state() {
current_data().map(|data| data.save_screen_state());
}
/// Given a command line and an autosuggestion, return the string that gets shown to the user.
/// Exposed for testing purposes only.
pub fn combine_command_and_autosuggestion(
@@ -1493,7 +1502,7 @@ pub fn combine_command_and_autosuggestion(
assert!(!autosuggestion.is_empty());
assert!(autosuggestion.len() >= line_range.len());
let available = autosuggestion.len() - line_range.len();
let line = &cmdline[line_range.clone()];
let line = &cmdline[line_range];
if !string_prefixes_string(line, autosuggestion) {
// We have an autosuggestion which is not a prefix of the command line, i.e. a case
@@ -2177,6 +2186,8 @@ impl<'a> Reader<'a> {
/// Read a command to execute, respecting input bindings.
/// Return the command, or none if we were asked to cancel (e.g. SIGHUP).
fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
let mut tty = TtyHandoff::new(reader_save_screen_state);
self.rls = Some(ReadlineLoopState::new());
// Suppress fish_trace during executing key bindings.
@@ -2244,11 +2255,18 @@ fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
self.force_exec_prompt_and_repaint = true;
while !self.rls().finished && !check_exit_loop_maybe_warning(Some(self)) {
// Enable tty protocols while we read input.
tty.enable_tty_protocols();
if self.handle_char_event(None).is_break() {
break;
}
}
// Disable tty protocols now that we're going to execute a command.
if tty.disable_tty_protocols() {
self.save_screen_state();
}
if self.conf.transient_prompt {
self.exec_prompt(true, true);
}
@@ -2308,10 +2326,16 @@ fn readline(&mut self, nchars: Option<NonZeroUsize>) -> Option<WString> {
fn eval_bind_cmd(&mut self, cmd: &wstr) {
let last_statuses = self.parser.vars().get_last_statuses();
let prev_exec_external_count = self.parser.libdata().exec_external_count;
// Disable TTY protocols while we run a bind command, because it may call out.
let mut scoped_tty = TtyHandoff::new(reader_save_screen_state);
let mut modified_tty = scoped_tty.disable_tty_protocols();
self.parser.eval(cmd, &IoChain::new());
self.parser.set_last_statuses(last_statuses);
if self.parser.libdata().exec_external_count != prev_exec_external_count
&& self.data.left_prompt_buff.contains('\n')
modified_tty |= scoped_tty.reclaim();
if modified_tty
|| (self.parser.libdata().exec_external_count != prev_exec_external_count
&& self.data.left_prompt_buff.contains('\n'))
{
self.save_screen_state();
}
@@ -2353,7 +2377,6 @@ fn read_normal_chars(&mut self) -> Option<CharEvent> {
let mut accumulated_chars = WString::new();
while accumulated_chars.len() < limit {
terminal_protocols_enable_ifn();
let evt = self.read_char();
let CharEvent::Key(kevt) = &evt else {
event_needing_handling = Some(evt);
@@ -2547,11 +2570,11 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
// Rogue reply.
return ControlFlow::Continue(());
}
if KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed)
== Capability::Unknown as _
{
KITTY_KEYBOARD_SUPPORTED
.store(Capability::NotSupported as _, Ordering::Release);
if get_kitty_keyboard_capability() == Capability::Unknown {
set_kitty_keyboard_capability(
reader_save_screen_state,
Capability::NotSupported,
);
}
}
QueryResponseEvent::CursorPositionReport(cursor_pos) => {
@@ -2591,6 +2614,8 @@ fn send_xtgettcap_query(out: &mut impl Output, cap: &'static str) {
out.write_command(QueryXtgettcap(cap));
}
#[allow(renamed_and_removed_lints)]
#[allow(clippy::blocks_in_if_conditions)] // for old clippy
fn query_capabilities_via_dcs(out: &mut impl Output, vars: &dyn Environment) {
if vars.get_unless_empty(L!("STY")).is_some()
|| vars.get_unless_empty(L!("TERM")).is_some_and(|term| {
@@ -2796,7 +2821,14 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
}
} else {
// Either the user hit tab only once, or we had no visible completion list.
// Disable tty protocols while we compute completions, so that control-C
// triggers SIGINT (suppressed by CSI-U).
let mut tty = TtyHandoff::new(reader_save_screen_state);
tty.disable_tty_protocols();
self.compute_and_apply_completions(c);
if tty.reclaim() {
self.save_screen_state();
}
}
}
rl::PagerToggleSearch => {
@@ -3570,6 +3602,33 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
self.update_buff_pos(elt, Some(buff_pos));
}
}
rl::UpcaseSelection | rl::DowncaseSelection => {
let (elt, el) = self.active_edit_line();
// Check that we have an active selection and get the bounds.
if let Some(selection) = self.get_selection() {
let text = &el.text().as_char_slice()[selection.clone()];
let replacement = if c == rl::UpcaseSelection {
WString::from_iter(text.iter().flat_map(|c| c.to_uppercase()))
} else {
WString::from_iter(text.iter().flat_map(|c| c.to_lowercase()))
};
let buff_pos = el.position();
self.replace_substring(elt, selection, replacement);
// Restore the buffer position since replace_substring moves
// the buffer position ahead of the replaced text.
// Note: This does not take string length changes into account.
// E.g.: When the cursor was at the right of the selection,
// the selection contains 'ẞ', which is uppercased into 'SS',
// the cursor will stay at the same offset, but it will not be on the same
// character as before.
// The position calculations work on codepoints rather than graphemes, which can
// result in additional issues.
self.update_buff_pos(elt, Some(buff_pos));
}
}
rl::UpcaseWord | rl::DowncaseWord | rl::CapitalizeWord => {
let (elt, el) = self.active_edit_line();
// For capitalize_word, whether we've capitalized a character so far.
@@ -4464,7 +4523,7 @@ fn reader_interactive_init(parser: &Parser) {
.vars()
.set_one(L!("_"), EnvMode::GLOBAL, L!("fish").to_owned());
terminal_protocol_hacks();
initialize_tty_metadata();
}
/// Destroy data for interactive use.
@@ -4560,6 +4619,10 @@ fn exec_prompt(&mut self, full_prompt: bool, final_prompt: bool) {
// Prompts must be run non-interactively.
let _noninteractive = self.parser.push_scope(|s| s.is_interactive = false);
// Suppress TTY protocols in a scoped way so that e.g. control-C can cancel the prompt.
let mut scoped_tty = TtyHandoff::new(reader_save_screen_state);
scoped_tty.disable_tty_protocols();
// Update the termsize now.
// This allows prompts to react to $COLUMNS.
self.update_termsize();
@@ -4809,7 +4872,7 @@ fn get_autosuggestion_performer(
};
let mut result = AutosuggestionResult::new(
command_line,
search_string_range.clone(),
search_string_range,
suggestion,
true, // normal completions are case-insensitive
/*is_whole_item_from_history=*/ false,
@@ -5653,6 +5716,10 @@ fn check_for_orphaned_process(loop_count: usize, shell_pgid: libc::pid_t) -> boo
/// Run the specified command with the correct terminal modes, and while taking care to perform job
/// notification, set the title, etc.
fn reader_run_command(parser: &Parser, cmd: &wstr) -> EvalRes {
assert!(
!get_tty_protocols_active(),
"TTY protocols should not be active"
);
let ft = tok_command(cmd);
// Provide values for `status current-command` and `status current-commandline`
@@ -6160,6 +6227,7 @@ pub fn completion_apply_to_command_line(
&& unescaped_quote(command_line, insertion_point) != quote
{
// This is a quoted parameter, first print a quote.
#[allow(clippy::unnecessary_unwrap)] // for old clippy
result.insert(new_cursor_pos, quote.unwrap());
new_cursor_pos += 1;
}
@@ -6204,6 +6272,10 @@ fn compute_and_apply_completions(&mut self, c: ReadlineCmd) {
c,
ReadlineCmd::Complete | ReadlineCmd::CompleteAndSearch
));
assert!(
!get_tty_protocols_active(),
"should not be called with TTY protocols active"
);
// Remove a trailing backslash. This may trigger an extra repaint, but this is
// rare.
@@ -6233,9 +6305,6 @@ fn compute_and_apply_completions(&mut self, c: ReadlineCmd) {
token_range.start += cmdsub_range.start;
token_range.end += cmdsub_range.start;
// Wildcard expansion and completion below check for cancellation.
terminal_protocols_disable_ifn();
// Check if we have a wildcard within this string; if so we first attempt to expand the
// wildcard; if that succeeds we don't then apply user completions (#8593).
let mut wc_expanded = WString::new();

View File

@@ -7,6 +7,7 @@
use crate::reader::{reader_handle_sigint, reader_sighup, safe_restore_term_mode};
use crate::termsize::TermsizeContainer;
use crate::topic_monitor::{topic_monitor_principal, Generation, GenerationsList, Topic};
use crate::tty_handoff::{safe_deactivate_tty_protocols, safe_mark_tty_invalid};
use crate::wchar::prelude::*;
use crate::wutil::{fish_wcstoi, perror};
use errno::{errno, set_errno};
@@ -83,13 +84,15 @@ extern "C" fn fish_signal_handler(
// Exit unless the signal was trapped.
if !observed {
reader_sighup();
safe_mark_tty_invalid();
}
topic_monitor_principal().post(Topic::sighupint);
}
libc::SIGTERM => {
// Handle sigterm. The only thing we do is restore the front process ID, then die.
// Handle sigterm. The only thing we do is restore the front process ID and disable protocols, then die.
if !observed {
safe_restore_term_mode();
safe_deactivate_tty_protocols();
// Safety: signal() and raise() are async-signal-safe.
unsafe {
libc::signal(libc::SIGTERM, libc::SIG_DFL);

View File

@@ -216,15 +216,14 @@ fn maybe_terminfo(
true
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
pub(crate) enum Capability {
pub enum Capability {
Unknown,
Supported,
NotSupported,
}
pub(crate) static KITTY_KEYBOARD_SUPPORTED: AtomicU8 = AtomicU8::new(Capability::Unknown as _);
pub(crate) static SCROLL_FORWARD_SUPPORTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
pub(crate) static SCROLL_FORWARD_TERMINFO_CODE: &str = "indn";
@@ -878,7 +877,7 @@ pub fn setup() {
if let Ok(result) = res {
// Create a new `Term` instance, prepopulate the capabilities we care about.
let term = Arc::new(Term::new(result));
*global_term = Some(term.clone());
*global_term = Some(term);
} else {
*global_term = None;
}

View File

@@ -63,7 +63,7 @@ fn callback(&self, fd: &mut AutoCloseFd) {
let mut buf = [0u8; 1024];
let res = nix::unistd::read(&fd, &mut buf);
let amt = res.expect("read error!");
self.length_read.fetch_add(amt as usize, Ordering::Relaxed);
self.length_read.fetch_add(amt, Ordering::Relaxed);
let was_closed = amt == 0;
self.total_calls.fetch_add(1, Ordering::Relaxed);

View File

@@ -702,6 +702,7 @@ fn test_expand_argument_list() {
fn test_1_cancellation(parser: &Parser, src: &wstr) {
let filler = IoBufferfill::create().unwrap();
let delay = Duration::from_millis(100);
#[allow(clippy::unnecessary_cast)]
let thread = unsafe { libc::pthread_self() } as usize;
iothread_perform(move || {
// Wait a while and then SIGINT the main thread.

606
src/tty_handoff.rs Normal file
View File

@@ -0,0 +1,606 @@
//! Utility for transferring the tty to a child process in a scoped way,
//! and reclaiming it after.
use crate::common::{self, safe_write_loop};
use crate::flog::{FLOG, FLOGF};
use crate::global_safety::RelaxedAtomicBool;
use crate::job_group::JobGroup;
use crate::proc::JobGroupRef;
use crate::terminal::TerminalCommand::{
self, ApplicationKeypadModeDisable, ApplicationKeypadModeEnable, DecrstBracketedPaste,
DecrstFocusReporting, DecsetBracketedPaste, DecsetFocusReporting,
KittyKeyboardProgressiveEnhancementsDisable, KittyKeyboardProgressiveEnhancementsEnable,
ModifyOtherKeysDisable, ModifyOtherKeysEnable,
};
use crate::terminal::{Capability, Output, Outputter};
use crate::threads::assert_is_main_thread;
use crate::wchar_ext::ToWString;
use crate::wutil::perror;
use libc::{EINVAL, ENOTTY, EPERM, STDIN_FILENO, WNOHANG};
use std::mem::MaybeUninit;
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU8, Ordering};
// Facts about our environment, which inform how we handle the tty.
#[derive(Debug, Copy, Clone)]
pub struct TtyMetadata {
// Whether we are running under Midnight Commander.
pub in_midnight_commander: bool,
// Whether we are running under dvtm.
pub in_dvtm: bool,
// Whether we are running under tmux.
pub in_tmux: bool,
// If set, we are running before iTerm2 3.5.12, which does not support CSI-U.
pub pre_kitty_iterm2: bool,
}
impl TtyMetadata {
// Create a new TtyMetadata instance with the current environment.
fn detect() -> Self {
use std::env::{var, var_os};
let in_midnight_commander = var_os("MC_TMPDIR").is_some();
let in_dvtm = var("TERM").as_deref() == Ok("dvtm-256color");
let in_tmux = var_os("TMUX").is_some();
// Detect iTerm2 before 3.5.12.
let pre_kitty_iterm2 = get_iterm2_version().is_some_and(|v| v < (3, 5, 12));
Self {
in_midnight_commander,
in_dvtm,
in_tmux,
pre_kitty_iterm2,
}
}
}
// Whether CSI-U ("Kitty") support is present in the TTY.
static KITTY_KEYBOARD_SUPPORTED: AtomicU8 = AtomicU8::new(Capability::Unknown as _);
// Get the support capability for CSI-U ("Kitty") protocols.
pub fn get_kitty_keyboard_capability() -> Capability {
let cap = KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed);
match cap {
x if x == Capability::Supported as _ => Capability::Supported,
x if x == Capability::NotSupported as _ => Capability::NotSupported,
_ => Capability::Unknown,
}
}
// Set CSI-U ("Kitty") support capability.
// This correctly handles the case where we think protocols are already enabled.
pub fn set_kitty_keyboard_capability(on_write: fn(), cap: Capability) {
assert_is_main_thread();
// Disable and renable protocols around capabilities.
let mut tty = TtyHandoff::new(on_write);
tty.disable_tty_protocols();
KITTY_KEYBOARD_SUPPORTED.store(cap as _, Ordering::Relaxed);
FLOG!(
term_protocols,
"Set Kitty keyboard capability to",
format!("{:?}", cap)
);
tty.reclaim();
}
// Helper to determine which keyboard protocols to enable.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ProtocolKind {
CSI_U, // Kitty keyboard support with CSI-U
Other, // Other protocols (e.g., modifyOtherKeys)
None, // No protocols
}
// Commands to emit to enable or disable TTY protocols. Each of these contains
// the full serialized command sequence as bytes. It's structured in this awkward
// way so that we can use it from a signal handler - no need to allocate or deallocate
// as Kitty support is discovered through tty queries.
struct ProtocolBytes {
csi_u: Box<[u8]>,
other: Box<[u8]>,
none: Box<[u8]>,
}
// The combined set of TTY protocols.
// This is created once at startup and then leaked, so it may be used
// from the SIGTERM handler.
struct TtyProtocolsSet {
// TTY metadata.
md: TtyMetadata,
// Variants to enable or disable tty protocols.
enablers: ProtocolBytes,
disablers: ProtocolBytes,
}
impl TtyProtocolsSet {
// Get commands to enable or disable TTY protocols, based on the metadata
// and the KITTY_KEYBOARD_SUPPORTED global variable.
// THIS IS USED FROM A SIGNAL HANDLER.
pub fn safe_get_commands(&self, enable: bool) -> &[u8] {
let protocol = self.md.safe_get_supported_protocol();
let cmds = if enable {
&self.enablers
} else {
&self.disablers
};
match protocol {
ProtocolKind::CSI_U => &cmds.csi_u,
ProtocolKind::Other => &cmds.other,
ProtocolKind::None => &cmds.none,
}
}
}
// Serialize a sequence of terminal commands into a byte array.
fn serialize_commands<'a>(cmds: impl Iterator<Item = TerminalCommand<'a>>) -> Box<[u8]> {
let mut out = Outputter::new_buffering();
for cmd in cmds {
out.write_command(cmd);
}
out.contents().into()
}
impl TtyMetadata {
// Determine which keyboard protocol to use based on the metadata
// and the KITTY_KEYBOARD_SUPPORTED global variable.
// This is used from a signal handler.
fn safe_get_supported_protocol(&self) -> ProtocolKind {
if self.pre_kitty_iterm2 {
return ProtocolKind::Other;
}
let cap = KITTY_KEYBOARD_SUPPORTED.load(Ordering::Relaxed);
match cap {
x if x == Capability::Supported as _ => ProtocolKind::CSI_U,
x if x == Capability::NotSupported as _ => ProtocolKind::Other,
_ => ProtocolKind::None,
}
}
// Return the protocols set to enable or disable TTY protocols.
fn get_protocols(self) -> TtyProtocolsSet {
// Enable focus reporting under tmux
let focus_reporting_on = || self.in_tmux.then_some(DecsetFocusReporting).into_iter();
let focus_reporting_off = || self.in_tmux.then_some(DecrstFocusReporting).into_iter();
let maybe_enable_focus_reporting = |protocols: &'static [TerminalCommand<'static>]| {
protocols.iter().cloned().chain(focus_reporting_on())
};
let maybe_disable_focus_reporting = |protocols: &'static [TerminalCommand<'static>]| {
protocols.iter().cloned().chain(focus_reporting_off())
};
let enablers = ProtocolBytes {
csi_u: serialize_commands(maybe_enable_focus_reporting(&[
DecsetBracketedPaste, // Enable bracketed paste
KittyKeyboardProgressiveEnhancementsEnable, // Kitty keyboard progressive enhancements
])),
other: serialize_commands(maybe_enable_focus_reporting(&[
DecsetBracketedPaste,
ModifyOtherKeysEnable, // XTerm's modifyOtherKeys
ApplicationKeypadModeEnable, // set application keypad mode, so the keypad keys send unique codes
])),
none: serialize_commands(maybe_enable_focus_reporting(&[DecsetBracketedPaste])),
};
let disablers = ProtocolBytes {
csi_u: serialize_commands(maybe_disable_focus_reporting(&[
DecrstBracketedPaste, // Disable bracketed paste
KittyKeyboardProgressiveEnhancementsDisable, // Kitty keyboard progressive enhancements
])),
other: serialize_commands(maybe_disable_focus_reporting(&[
DecrstBracketedPaste,
ModifyOtherKeysDisable,
ApplicationKeypadModeDisable,
])),
none: serialize_commands(maybe_disable_focus_reporting(&[DecrstBracketedPaste])),
};
TtyProtocolsSet {
md: self,
enablers,
disablers,
}
}
}
// The global tty protocols. This is set once at startup and not changed thereafter.
// This is an AtomicPtr and not a OnceLock, etc. so that it can be used from a signal handler.
static TTY_PROTOCOLS: AtomicPtr<TtyProtocolsSet> = AtomicPtr::new(std::ptr::null_mut());
// Get the TTY protocols, without initializing it.
fn tty_protocols() -> Option<&'static TtyProtocolsSet> {
// Safety: TTY_PROTOCOLS is never modified after initialization.
unsafe { TTY_PROTOCOLS.load(Ordering::Acquire).as_ref() }
}
// Get the TTY protocols, initializing it if necessary.
// This also initializes the terminal enable and disable serialized commands.
// Note in practice this is only used from the main thread - races are very unlikely.
fn get_or_init_tty_protocols() -> &'static TtyProtocolsSet {
use std::sync::atomic::Ordering::{Acquire, Release};
// Standard lazy-init pattern from rust-atomics-and-locks.
let mut p = TTY_PROTOCOLS.load(Acquire);
if p.is_null() {
// Try to swap in a new TTY protocols set.
p = Box::into_raw(Box::new(TtyMetadata::detect().get_protocols()));
if let Err(e) = TTY_PROTOCOLS.compare_exchange(std::ptr::null_mut(), p, Release, Acquire) {
// Safety: p comes from Box::into_raw right above,
// and wasn't shared with any other thread.
drop(unsafe { Box::from_raw(p) });
p = e;
}
}
// Safety: p is not null and points to a properly initialized value.
unsafe { &*p }
}
// Get the TTY metadata, initializing it if necessary.
pub fn tty_metadata() -> TtyMetadata {
get_or_init_tty_protocols().md
}
// Cover to merely initialize the TTY metadata, for clarity at call sites.
pub fn initialize_tty_metadata() {
tty_metadata();
}
// A marker of the current state of the tty protocols.
static TTY_PROTOCOLS_ACTIVE: AtomicBool = AtomicBool::new(false);
// A marker that the tty has been closed (SIGHUP, etc) and so we should not try to write to it.
static TTY_INVALID: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
// Enable or disable TTY protocols by writing the appropriate commands to the tty.
// Return true if we emitted any bytes to the tty.
// Note this does NOT intialize the TTY protocls if not already initialized.
fn set_tty_protocols_active(on_write: fn(), enable: bool) -> bool {
assert_is_main_thread();
// Have protocols at all? We require someone else to have initialized them.
let Some(protocols) = tty_protocols() else {
return false;
};
// Already set?
// Note we don't need atomic swaps as this is only called on the main thread.
// Also note we (logically) set and clear this even if we got SIGHUP.
if TTY_PROTOCOLS_ACTIVE.load(Ordering::Relaxed) == enable {
return false;
}
if enable {
TTY_PROTOCOLS_ACTIVE.store(true, Ordering::Release);
}
// Did we get SIGHUP?
if TTY_INVALID.load() {
return false;
}
// Write the commands to the tty, ignoring errors.
let commands = protocols.safe_get_commands(enable);
let _ = common::write_loop(&libc::STDOUT_FILENO, commands);
if !enable {
TTY_PROTOCOLS_ACTIVE.store(false, Ordering::Relaxed);
}
// Flog any terminal protocol changes of interest.
let mode = if enable { "Enabling" } else { "Disabling" };
match protocols.md.safe_get_supported_protocol() {
ProtocolKind::CSI_U => FLOG!(term_protocols, mode, "CSI-U extended keys"),
ProtocolKind::Other => FLOG!(term_protocols, mode, "other extended keys"),
ProtocolKind::None => (),
};
(on_write)();
true
}
// Helper to check if TTY protocols are active.
pub fn get_tty_protocols_active() -> bool {
TTY_PROTOCOLS_ACTIVE.load(Ordering::Relaxed)
}
// Called from a signal handler to deactivate TTY protocols before exiting.
// Only async-signal-safe code can be run here.
pub fn safe_deactivate_tty_protocols() {
// Safety: TTY_PROTOCOLS is never modified after initialization.
let protocols = unsafe { TTY_PROTOCOLS.load(Ordering::Acquire).as_ref() };
let Some(protocols) = protocols else {
// No protocols set, nothing to do.
return;
};
if !TTY_PROTOCOLS_ACTIVE.load(Ordering::Acquire) {
return;
}
// Did we get SIGHUP?
if TTY_INVALID.load() {
return;
}
let commands = protocols.safe_get_commands(false);
// Safety: just writing data to stdout.
let _ = safe_write_loop(&libc::STDOUT_FILENO, commands);
TTY_PROTOCOLS_ACTIVE.store(false, Ordering::Release);
}
// Called from a signal handler to mark the tty as invalid (e.g. SIGHUP).
// This suppresses any further attempts to write protocols to the tty,
pub fn safe_mark_tty_invalid() {
TTY_INVALID.store(true);
}
// Allows transferring the tty to a job group, while it runs, in a scoped fashion.
// This has several responsibilities:
// - Invoking tcsetpgrp() to transfer the tty to the job group.
// Note this is complex because it is inherently "racey."
// - Saving tty modes if a job stops. That is, if a job is running and
// then it stops in the background, we want to record the tty modes
// it has in the job, so that we can restore them when the job is resumed.
// - Managing enabling and disabling terminal protocols (bracketed paste, etc).
// Note it only ever makes sense to run this on the main thread.
pub struct TtyHandoff {
// The job group which owns the tty, or empty if none.
owner: Option<JobGroupRef>,
// Whether terminal protocols were initially enabled.
// reclaim() restores the state to this.
tty_protocols_initial: bool,
// The state of terminal protocols that we set.
// Note we track this separately from TTY_PROTOCOLS_ACTIVE. We undo the changes
// we make.
tty_protocols_applied: bool,
// Whether reclaim was called, restoring the tty to its pre-scoped value.
reclaimed: bool,
// Called after writing to the TTY.
on_write: fn(),
}
impl TtyHandoff {
pub fn new(on_write: fn()) -> Self {
let protocols_active = get_tty_protocols_active();
TtyHandoff {
owner: None,
tty_protocols_initial: protocols_active,
tty_protocols_applied: protocols_active,
reclaimed: false,
on_write,
}
}
/// Mark terminal modes as enabled.
/// Return true if something was written to the tty.
pub fn enable_tty_protocols(&mut self) -> bool {
if self.tty_protocols_applied {
return false; // Already enabled.
}
self.tty_protocols_applied = true;
set_tty_protocols_active(self.on_write, true)
}
/// Mark terminal modes as disabled.
/// Return true if something was written to the tty.
pub fn disable_tty_protocols(&mut self) -> bool {
if !self.tty_protocols_applied {
return false; // Already disabled.
};
self.tty_protocols_applied = false;
set_tty_protocols_active(self.on_write, false)
}
/// Transfer to the given job group, if it wants to own the terminal.
#[allow(clippy::wrong_self_convention)]
pub fn to_job_group(&mut self, jg: &JobGroupRef) {
assert!(self.owner.is_none(), "Terminal already transferred");
if Self::try_transfer(jg) {
self.owner = Some(jg.clone());
}
}
/// Reclaim the tty if we transferred it.
/// Returns true if data was written to the tty, as part of
/// re-enabling terminal protocols.
pub fn reclaim(mut self) -> bool {
self.reclaim_impl()
}
/// Release the tty, meaning no longer restore anything in Drop - similar to `mem::forget`.
pub fn release(mut self) {
self.reclaimed = true;
}
/// Implementation of reclaim, factored out for use in Drop.
fn reclaim_impl(&mut self) -> bool {
assert!(!self.reclaimed, "Terminal already reclaimed");
self.reclaimed = true;
if self.owner.is_some() {
FLOG!(proc_pgroup, "fish reclaiming terminal");
if unsafe { libc::tcsetpgrp(STDIN_FILENO, libc::getpgrp()) } == -1 {
FLOG!(warning, "Could not return shell to foreground");
perror("tcsetpgrp");
}
self.owner = None;
}
// Restore the terminal protocols. Note this does nothing if they were unchanged.
if self.tty_protocols_initial {
self.enable_tty_protocols()
} else {
self.disable_tty_protocols()
}
}
/// Save the current tty modes into the owning job group, if we are transferred.
pub fn save_tty_modes(&mut self) {
if let Some(ref mut owner) = self.owner {
let mut tmodes = MaybeUninit::uninit();
if unsafe { libc::tcgetattr(STDIN_FILENO, tmodes.as_mut_ptr()) } == 0 {
owner.tmodes.replace(Some(unsafe { tmodes.assume_init() }));
} else if errno::errno().0 != ENOTTY {
perror("tcgetattr");
}
}
}
fn try_transfer(jg: &JobGroup) -> bool {
if !jg.wants_terminal() {
// The job doesn't want the terminal.
return false;
}
// Get the pgid; we must have one if we want the terminal.
let pgid = jg.get_pgid().unwrap();
// It should never be fish's pgroup.
let fish_pgrp = crate::nix::getpgrp();
assert!(
pgid.as_pid_t() != fish_pgrp,
"Job should not have fish's pgroup"
);
// Ok, we want to transfer to the child.
// Note it is important to be very careful about calling tcsetpgrp()!
// fish ignores SIGTTOU which means that it has the power to reassign the tty even if it doesn't
// own it. This means that other processes may get SIGTTOU and become zombies.
// Check who own the tty now. There's four cases of interest:
// 1. There is no tty at all (tcgetpgrp() returns -1). For example running from a pure script.
// Of course do not transfer it in that case.
// 2. The tty is owned by the process. This comes about often, as the process will call
// tcsetpgrp() on itself between fork and exec. This is the essential race inherent in
// tcsetpgrp(). In this case we want to reclaim the tty, but do not need to transfer it
// ourselves since the child won the race.
// 3. The tty is owned by a different process. This may come about if fish is running in the
// background with job control enabled. Do not transfer it.
// 4. The tty is owned by fish. In that case we want to transfer the pgid.
let current_owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
if current_owner < 0 {
// Case 1.
return false;
} else if current_owner == pgid.get() {
// Case 2.
return true;
} else if current_owner != pgid.get() && current_owner != fish_pgrp {
// Case 3.
return false;
}
// Case 4 - we do want to transfer it.
// The tcsetpgrp(2) man page says that EPERM is thrown if "pgrp has a supported value, but
// is not the process group ID of a process in the same session as the calling process."
// Since we _guarantee_ that this isn't the case (the child calls setpgid before it calls
// SIGSTOP, and the child was created in the same session as us), it seems that EPERM is
// being thrown because of an caching issue - the call to tcsetpgrp isn't seeing the
// newly-created process group just yet. On this developer's test machine (WSL running Linux
// 4.4.0), EPERM does indeed disappear on retry. The important thing is that we can
// guarantee the process isn't going to exit while we wait (which would cause us to possibly
// block indefinitely).
while unsafe { libc::tcsetpgrp(STDIN_FILENO, pgid.as_pid_t()) } != 0 {
FLOGF!(proc_termowner, "tcsetpgrp failed: %d", errno::errno().0);
// Before anything else, make sure that it's even necessary to call tcsetpgrp.
// Since it usually _is_ necessary, we only check in case it fails so as to avoid the
// unnecessary syscall and associated context switch, which profiling has shown to have
// a significant cost when running process groups in quick succession.
let getpgrp_res = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
if getpgrp_res < 0 {
match errno::errno().0 {
ENOTTY => {
// stdin is not a tty. This may come about if job control is enabled but we are
// not a tty - see #6573.
return false;
}
_ => {
perror("tcgetpgrp");
return false;
}
}
}
if getpgrp_res == pgid.get() {
FLOGF!(
proc_termowner,
"Process group %d already has control of terminal",
pgid
);
return true;
}
let pgroup_terminated;
if errno::errno().0 == EINVAL {
// OS X returns EINVAL if the process group no longer lives. Probably other OSes,
// too. Unlike EPERM below, EINVAL can only happen if the process group has
// terminated.
pgroup_terminated = true;
} else if errno::errno().0 == EPERM {
// Retry so long as this isn't because the process group is dead.
let mut result: libc::c_int = 0;
let wait_result = unsafe { libc::waitpid(-pgid.as_pid_t(), &mut result, WNOHANG) };
if wait_result == -1 {
// Note that -1 is technically an "error" for waitpid in the sense that an
// invalid argument was specified because no such process group exists any
// longer. This is the observed behavior on Linux 4.4.0. a "success" result
// would mean processes from the group still exist but is still running in some
// state or the other.
pgroup_terminated = true;
} else {
// Debug the original tcsetpgrp error (not the waitpid errno) to the log, and
// then retry until not EPERM or the process group has exited.
FLOGF!(
proc_termowner,
"terminal_give_to_job(): EPERM with pgid %d.",
pgid
);
continue;
}
} else if errno::errno().0 == ENOTTY {
// stdin is not a TTY. In general we expect this to be caught via the tcgetpgrp
// call's EBADF handler above.
return false;
} else {
FLOGF!(
warning,
"Could not send job %d ('%ls') with pgid %d to foreground",
jg.job_id.to_wstring(),
jg.command,
pgid
);
perror("tcsetpgrp");
return false;
}
if pgroup_terminated {
// All processes in the process group has exited.
// Since we delay reaping any processes in a process group until all members of that
// job/group have been started, the only way this can happen is if the very last
// process in the group terminated and didn't need to access the terminal, otherwise
// it would have hung waiting for terminal IO (SIGTTIN). We can safely ignore this.
FLOGF!(
proc_termowner,
"tcsetpgrp called but process group %d has terminated.\n",
pgid
);
return false;
}
break;
}
true
}
}
/// The destructor will assert if reclaim() has not been called.
impl Drop for TtyHandoff {
fn drop(&mut self) {
if !self.reclaimed {
self.reclaim_impl();
}
}
}
// If we are running under iTerm2, get the version as a tuple of (major, minor, patch).
fn get_iterm2_version() -> Option<(u32, u32, u32)> {
use std::env::var;
let term = var("LC_TERMINAL").ok()?;
if term != "iTerm2" {
return None;
}
let version = var("LC_TERMINAL_VERSION").ok()?;
let mut parts = version.split('.');
Some((
parts.next()?.parse().ok()?,
parts.next()?.parse().ok()?,
parts.next()?.parse().ok()?,
))
}

View File

@@ -143,6 +143,11 @@ echo -e 'abc\121def'
echo -e 'abc\1212def'
#CHECK: abcQdef
#CHECK: abcQ2def
# Test octal overflow: \5555 = 555 octal = 365 decimal, wraps to 109 decimal (155 octal)
# Followed by literal '5' character (065 octal)
echo -ne '\5555' | display_bytes
#CHECK: 0000000 155 065
#CHECK: 0000002
echo -e 'abc\cdef' # won't output a newline!
#CHECK: abc
echo ''

View File

@@ -1,5 +1,7 @@
#RUN: %fish %s
set -g fish (status fish-path)
commandline --input "echo foo | bar" --is-valid
and echo Valid
# CHECK: Valid
@@ -50,3 +52,16 @@ commandline --input "echo > {a,b}" --tokens-expanded
commandline --input "echo {arg1,arg2} <in >out" --tokens-raw
# CHECK: echo
# CHECK: {arg1,arg2}
$fish -ic '
commandline hello
commandline
commandline -i world
commandline
commandline --cursor 5
commandline -i " "
commandline
'
# CHECK: hello
# CHECK: helloworld
# CHECK: hello world

115
tests/checks/vi.fish Normal file
View File

@@ -0,0 +1,115 @@
# RUN: %fish --interactive %s
fish_vi_key_bindings
commandline '1'; commandline --cursor 0; fish_vi_dec
commandline --current-buffer
# CHECK: 0
commandline '0'; commandline --cursor 0; fish_vi_dec
commandline --current-buffer
# CHECK: -1
commandline -- '-1'; commandline --cursor 0; fish_vi_dec
commandline --current-buffer
# CHECK: -2
commandline -- '-1'; commandline --cursor 0; fish_vi_inc
commandline --current-buffer
# CHECK: 0
commandline '0'; commandline --cursor 0; fish_vi_inc
commandline --current-buffer
# CHECK: 1
commandline '123'; commandline --cursor 0; fish_vi_inc
commandline --current-buffer
# CHECK: 124
commandline '123'; commandline --cursor 0; fish_vi_dec
commandline --current-buffer
# CHECK: 122
commandline '123'; commandline --cursor 1; fish_vi_inc
commandline --current-buffer
# CHECK: 124
commandline '123'; commandline --cursor 1; fish_vi_dec
commandline --current-buffer
# CHECK: 122
commandline '123'; commandline --cursor 2; fish_vi_inc
commandline --current-buffer
# CHECK: 124
commandline '123'; commandline --cursor 2; fish_vi_dec
commandline --current-buffer
# CHECK: 122
commandline 'abc123'; commandline --cursor 1; fish_vi_inc
commandline --current-buffer
# CHECK: abc124
commandline 'abc123'; commandline --cursor 1; fish_vi_dec
commandline --current-buffer
# CHECK: abc122
commandline 'abc123def'; commandline --cursor 1; fish_vi_inc
commandline --current-buffer
# CHECK: abc124def
commandline 'abc123def'; commandline --cursor 1; fish_vi_dec
commandline --current-buffer
# CHECK: abc122def
commandline 'abc123def'; commandline --cursor 5; fish_vi_inc
commandline --current-buffer
# CHECK: abc124def
commandline 'abc123def'; commandline --cursor 5; fish_vi_dec
commandline --current-buffer
# CHECK: abc122def
commandline 'abc123def'; commandline --cursor 6; fish_vi_inc
commandline --current-buffer
# CHECK: abc123def
commandline 'abc123def'; commandline --cursor 6; fish_vi_dec
commandline --current-buffer
# CHECK: abc123def
commandline 'abc99def'; commandline --cursor 1; fish_vi_inc
commandline --current-buffer
# CHECK: abc100def
commandline 'abc99def'; commandline --cursor 1; fish_vi_dec
commandline --current-buffer
# CHECK: abc98def
commandline 'abc-99def'; commandline --cursor 1; fish_vi_inc
commandline --current-buffer
# CHECK: abc-98def
commandline 'abc-99def'; commandline --cursor 1; fish_vi_dec
commandline --current-buffer
# CHECK: abc-100def
commandline '2022-04-09'; commandline --cursor 7; fish_vi_inc
commandline --current-buffer
# CHECK: 2022-04-08
commandline 'to 2022-04-09'; commandline --cursor 4; fish_vi_inc
commandline --current-buffer
# CHECK: to 2023-04-09
commandline 'to 2022-04-09'; commandline --cursor 4; fish_vi_dec
commandline --current-buffer
# CHECK: to 2021-04-09
commandline 'to 2022-04-09'; commandline --cursor 11; fish_vi_dec
commandline --current-buffer
# CHECK: to 2022-04-10
commandline 'to 2022-04-09'; commandline --cursor 11; fish_vi_inc
commandline --current-buffer
# CHECK: to 2022-04-08

View File

@@ -102,6 +102,18 @@ send(control("k"))
sendline('echo "process extent is [$tmp]"')
expect_str("process extent is [echo process # comment]")
sendline(
"""$fish -C 'commandline "sq 2; exit"; commandline --cursor 1; commandline -i e'"""
)
expect_str("seq 2")
send("\r")
expect_str("1\r\n2\r\n")
sendline("""$fish -C 'commandline 123; read'""")
expect_str("read> 123")
sendline("456; exit")
expect_str("123456")
# DISABLED because it keeps failing under ASAN
# sendline(r"bind ctrl-b 'set tmp (commandline --current-process | count)'")
# sendline(r'commandline "echo line1 \\" "# comment" "line2"')

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env python3
import argparse
import asyncio
from dataclasses import dataclass
from datetime import datetime
import os
from pathlib import Path
import resource
import shutil
import subprocess
import sys
@@ -205,25 +205,33 @@ async def main():
if semaphore is not None:
semaphore.release()
tasks = [run(f, arg) for f, arg in files]
for task in asyncio.as_completed(tasks):
result = await task
# TODO(python>3.8): use match statement
if isinstance(result, TestSkip):
arg = result.arg
skipcount += 1
print_result(arg, "SKIPPED", BLUE)
elif isinstance(result, TestFail):
# fmt: off
arg, duration_ms, error_message = result.arg, result.duration_ms, result.error_message
# fmt: on
failcount += 1
failed += [arg]
print_result(arg, "FAILED", RED, duration_ms, error_message)
elif isinstance(result, TestPass):
arg, duration_ms = result.arg, result.duration_ms
passcount += 1
print_result(arg, "PASSED", GREEN, duration_ms)
tasks = [asyncio.create_task(run(f, arg), name=arg) for f, arg in files]
while tasks:
done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
for task in done:
try:
result = await task
except Exception as e:
arg = task.get_name()
result = TestFail(
arg, None, f"Test '{arg}' raised an exception: {e}"
)
# TODO(python>3.8): use match statement
if isinstance(result, TestSkip):
arg = result.arg
skipcount += 1
print_result(arg, "SKIPPED", BLUE)
elif isinstance(result, TestFail):
# fmt: off
arg, duration_ms, error_message = result.arg, result.duration_ms, result.error_message
# fmt: on
failcount += 1
failed += [arg]
print_result(arg, "FAILED", RED, duration_ms, error_message)
elif isinstance(result, TestPass):
arg, duration_ms = result.arg, result.duration_ms
passcount += 1
print_result(arg, "PASSED", GREEN, duration_ms)
if passcount + failcount + skipcount > 1:
print(f"{passcount} / {passcount + failcount} passed ({skipcount} skipped)")
@@ -235,23 +243,35 @@ async def main():
return 1 if failcount else 0
@dataclass
# TODO(python>=3.7): @dataclass
class TestSkip:
arg: str
def __init__(self, arg: str):
self.arg = arg
@dataclass
class TestFail:
arg: str
duration_ms: Optional[int]
error_message: Optional[str]
def __init__(
self, arg: str, duration_ms: Optional[int], error_message: Optional[str]
):
self.arg = arg
self.duration_ms = duration_ms
self.error_message = error_message
@dataclass
class TestPass:
arg: str
duration_ms: int
def __init__(self, arg: str, duration_ms: Optional[int]):
self.arg = arg
self.duration_ms = duration_ms
TestResult = Union[TestSkip, TestFail, TestPass]
@@ -334,9 +354,27 @@ async def run_test(
return TestFail(arg, None, "Error in test driver. This should be unreachable.")
if sys.version_info < (3, 7):
def asyncio_run(coro):
loop = asyncio.get_event_loop()
try:
return loop.run_until_complete(coro)
finally:
if not loop.is_closed():
loop.close()
else:
asyncio_run = asyncio.run
if __name__ == "__main__":
# Increase the maximum number of open files to at least 4096,
# as we run tests concurrently.
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
if soft < 4096:
resource.setrlimit(resource.RLIMIT_NOFILE, (min(4096, hard), hard))
try:
ret = asyncio.run(main())
ret = asyncio_run(main())
sys.exit(ret)
except KeyboardInterrupt:
sys.exit(130)