mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-05-26 07:41:15 -03:00
Compare commits
77 Commits
raw-tokens
...
torn-seque
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65556ac2ae | ||
|
|
25e5cc23c1 | ||
|
|
4012345ba9 | ||
|
|
43d583d991 | ||
|
|
d69886efe0 | ||
|
|
bd8cc6d317 | ||
|
|
9c5b3f3d57 | ||
|
|
5970f34a60 | ||
|
|
eaa837effa | ||
|
|
e52cf2f6a7 | ||
|
|
8c7568c0cb | ||
|
|
07979782a6 | ||
|
|
59b43986e9 | ||
|
|
51f3722e02 | ||
|
|
db0f9c1d53 | ||
|
|
c9901398ed | ||
|
|
6181ba3b56 | ||
|
|
d27f5a5293 | ||
|
|
c1d165de9d | ||
|
|
f0e007c439 | ||
|
|
65a4cb5245 | ||
|
|
c7262d6c05 | ||
|
|
f3c264722d | ||
|
|
39742cafa0 | ||
|
|
295d2bd218 | ||
|
|
3588b41744 | ||
|
|
82e3311756 | ||
|
|
b611c96cdd | ||
|
|
1d6fa258f6 | ||
|
|
6312b1dbd8 | ||
|
|
5fa2f62536 | ||
|
|
bbf7568ebd | ||
|
|
72347517b2 | ||
|
|
95475c35ff | ||
|
|
560d21cd86 | ||
|
|
bef453f69b | ||
|
|
fa832ead65 | ||
|
|
eb7afd2a9c | ||
|
|
f4ddcfa694 | ||
|
|
1605d8d6ce | ||
|
|
a7559a62c4 | ||
|
|
e9327d234d | ||
|
|
a3d03fc0fb | ||
|
|
1e981a9827 | ||
|
|
770f4ce6d1 | ||
|
|
aa782bdad7 | ||
|
|
e4c55131c7 | ||
|
|
e6ad78cda7 | ||
|
|
578e162f35 | ||
|
|
e9bb150a41 | ||
|
|
b5eccdf9f6 | ||
|
|
75716bd6b0 | ||
|
|
9e628995da | ||
|
|
5b39efc96d | ||
|
|
b5bb50d742 | ||
|
|
32c36aa5f8 | ||
|
|
fc37d8d5a8 | ||
|
|
8d361b4290 | ||
|
|
9789e6b731 | ||
|
|
4d67ca7c58 | ||
|
|
fbe5a53dc9 | ||
|
|
dcd93e4c52 | ||
|
|
7acd20dc7e | ||
|
|
1d893b77d3 | ||
|
|
b451650faa | ||
|
|
3e0a53ae4f | ||
|
|
e01aafab1c | ||
|
|
fd0fba83b9 | ||
|
|
6644cc9b0e | ||
|
|
f5370e6f22 | ||
|
|
d62fb9cc74 | ||
|
|
88ab024d7d | ||
|
|
1cc900ab7f | ||
|
|
144725e947 | ||
|
|
d969577f0b | ||
|
|
3cbb5e384b | ||
|
|
90b35335ee |
22
.github/actions/rust-toolchain@oldest-supported/action.yml
vendored
Normal file
22
.github/actions/rust-toolchain@oldest-supported/action.yml
vendored
Normal 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}}
|
||||
22
.github/actions/rust-toolchain@stable/action.yml
vendored
Normal file
22
.github/actions/rust-toolchain@stable/action.yml
vendored
Normal 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 }}
|
||||
6
.github/workflows/mac_codesign.yml
vendored
6
.github/workflows/mac_codesign.yml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
@@ -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".
|
||||
|
||||
45
.github/workflows/rust_checks.yml
vendored
45
.github/workflows/rust_checks.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/staticbuild.yml
vendored
4
.github/workflows/staticbuild.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
^^^^^^^^^^^
|
||||
|
||||
7
build.rs
7
build.rs
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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/pt_BR.po
471
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
473
po/zh_CN.po
473
po/zh_CN.po
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
1
share/completions/cilium.fish
Normal file
1
share/completions/cilium.fish
Normal file
@@ -0,0 +1 @@
|
||||
cilium completion fish 2>/dev/null | source
|
||||
102
share/completions/cjpm.fish
Normal file
102
share/completions/cjpm.fish
Normal 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"
|
||||
45
share/completions/cpan.fish
Normal file
45
share/completions/cpan.fish
Normal 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"
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
1
share/completions/hubble.fish
Normal file
1
share/completions/hubble.fish
Normal file
@@ -0,0 +1 @@
|
||||
hubble completion fish 2>/dev/null | source
|
||||
1
share/completions/k9s.fish
Normal file
1
share/completions/k9s.fish
Normal file
@@ -0,0 +1 @@
|
||||
k9s completion fish 2>/dev/null | source
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
9
share/completions/protontricks-launch.fish
Normal file
9
share/completions/protontricks-launch.fish
Normal 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'
|
||||
24
share/completions/protontricks.fish
Normal file
24
share/completions/protontricks.fish
Normal 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'
|
||||
@@ -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
|
||||
|
||||
31
share/completions/t-rec.fish
Normal file
31
share/completions/t-rec.fish
Normal 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"
|
||||
52
share/completions/tmuxp.fish
Normal file
52
share/completions/tmuxp.fish
Normal 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"
|
||||
1
share/completions/volta.fish
Normal file
1
share/completions/volta.fish
Normal file
@@ -0,0 +1 @@
|
||||
volta completions fish 2>/dev/null | source
|
||||
@@ -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
|
||||
|
||||
5
share/functions/__fish_protontricks_complete_appid.fish
Normal file
5
share/functions/__fish_protontricks_complete_appid.fish
Normal file
@@ -0,0 +1,5 @@
|
||||
function __fish_protontricks_complete_appid
|
||||
protontricks -l |
|
||||
string match --regex '.*\(\d+\)' |
|
||||
string replace --regex '(.*) \((\d+)\)' '$2\t$1'
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
2
src/env/config_paths.rs
vendored
2
src/env/config_paths.rs
vendored
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
10
src/exec.rs
10
src/exec.rs
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
42
src/path.rs
42
src/path.rs
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
235
src/proc.rs
235
src/proc.rs
@@ -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
|
||||
|
||||
161
src/reader.rs
161
src/reader.rs
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
606
src/tty_handoff.rs
Normal 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()?,
|
||||
))
|
||||
}
|
||||
@@ -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 ''
|
||||
|
||||
@@ -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
115
tests/checks/vi.fish
Normal 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
|
||||
@@ -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"')
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user