Merge pull request #49 from Dark-Alex-17/develop

chore: Pre-release merge develop into main
This commit is contained in:
Alex Clarke
2025-08-29 16:14:22 -06:00
committed by GitHub
260 changed files with 22167 additions and 16474 deletions
+4
View File
@@ -0,0 +1,4 @@
--artifact-server-path=./.act/artifacts
--cache-server-path=./.act/cache
--container-options --privileged
--env ACT=true
+4 -4
View File
@@ -76,15 +76,15 @@ jobs:
RUSTDOCFLAGS: --cfg docsrs RUSTDOCFLAGS: --cfg docsrs
msrv: msrv:
# check that we can build using the minimal rust version that is specified by this crate # check that we can build using the minimal rust version that is specified by this crate
name: 1.85.0 / check name: 1.89.0 / check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install 1.85.0 - name: Install 1.89.0
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.85.0 toolchain: 1.89.0
- name: cargo +1.85.0 check - name: cargo +1.89.0 check
run: cargo check run: cargo check
+114 -9
View File
@@ -23,6 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Configure SSH for Git - name: Configure SSH for Git
if: env.ACT != 'true'
run: | run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
echo "${{ secrets.RELEASE_BOT_SSH_KEY }}" > ~/.ssh/id_ed25519 echo "${{ secrets.RELEASE_BOT_SSH_KEY }}" > ~/.ssh/id_ed25519
@@ -30,11 +31,18 @@ jobs:
ssh-keyscan -H github.com >> ~/.ssh/known_hosts ssh-keyscan -H github.com >> ~/.ssh/known_hosts
- name: Checkout repository - name: Checkout repository
if: env.ACT != 'true'
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }} ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
fetch-depth: 0 fetch-depth: 0
- name: Checkout repository
if: env.ACT == 'true'
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
@@ -62,12 +70,6 @@ jobs:
- name: Install Rust stable - name: Install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Update the Cargo.lock
run: |
cargo update
git add Cargo.lock
git commit -m "chore: Bump the version in Cargo.lock"
- name: Get the new version tag - name: Get the new version tag
id: version id: version
run: | run: |
@@ -84,6 +86,46 @@ jobs:
echo "Previous tag: $PREV_TAG" echo "Previous tag: $PREV_TAG"
echo "prev_version=$PREV_TAG" >> $GITHUB_ENV echo "prev_version=$PREV_TAG" >> $GITHUB_ENV
- name: Bump Cargo.toml version
shell: bash
working-directory: ${{ github.workspace }}
env:
VERSION: ${{ env.version }}
run: |
set -euo pipefail
: "${VERSION:?env.version is empty}"
# Ignore Act's local artifact dir noise
echo artifacts/ >> .git/info/exclude || true
# Edit the version line right after name="managarr"
sed -E -i '
/^[[:space:]]*name[[:space:]]*=[[:space:]]*"managarr"[[:space:]]*$/ {
n
s|^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"|version = "'"$VERSION"'"|
}
' Cargo.toml
cargo update || true
# Git config that helps in containers (Act)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
# Debug: show what changed
git status --porcelain
git diff --name-only -- Cargo.toml Cargo.lock || true
# Only commit if one of these files actually changed
if ! git diff --quiet -- Cargo.toml Cargo.lock; then
# Stage only modifications of already tracked files (won't pick up artifacts/)
git add -u -- Cargo.toml Cargo.lock
git commit -m "chore: bump Cargo.toml to $VERSION"
else
echo "No changes to commit (already at $VERSION)"
fi
- name: Generate changelog for the version bump - name: Generate changelog for the version bump
id: changelog id: changelog
run: | run: |
@@ -92,6 +134,7 @@ jobs:
echo "changelog_body=$(cat artifacts/changelog.md)" >> $GITHUB_ENV echo "changelog_body=$(cat artifacts/changelog.md)" >> $GITHUB_ENV
- name: Push changes - name: Push changes
if: env.ACT != 'true'
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
@@ -102,6 +145,15 @@ jobs:
with: with:
path: artifacts path: artifacts
- name: Upload the changed Cargo files (Act)
if: env.ACT == 'true'
uses: actions/upload-artifact@v4
with:
name: bumped-cargo-files
path: |
Cargo.toml
Cargo.lock
build-release-artifacts: build-release-artifacts:
name: build-release name: build-release
needs: [bump-version] needs: [bump-version]
@@ -129,7 +181,7 @@ jobs:
steps: steps:
- name: Check if actor is repository owner - name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }} if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: | run: |
echo "You are not authorized to run this workflow." echo "You are not authorized to run this workflow."
exit 1 exit 1
@@ -140,10 +192,18 @@ jobs:
fetch-depth: 1 fetch-depth: 1
- name: Ensure repository is up-to-date - name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: | run: |
git fetch --all git fetch --all
git pull git pull
- name: Get bumped Cargo files (Act)
if: env.ACT == 'true'
uses: actions/download-artifact@v4
with:
name: bumped-cargo-files
path: ${{ github.workspace }}
- uses: actions/cache@v3 - uses: actions/cache@v3
name: Cache Cargo registry name: Cache Cargo registry
with: with:
@@ -246,6 +306,12 @@ jobs:
needs: [build-release-artifacts] needs: [build-release-artifacts]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
@@ -258,6 +324,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Ensure repository is up-to-date - name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: | run: |
git fetch --all git fetch --all
git pull git pull
@@ -269,7 +336,13 @@ jobs:
changelog_body="$(cat ./artifacts/changelog.md)" changelog_body="$(cat ./artifacts/changelog.md)"
echo "changelog_body=$(cat artifacts/changelog.md)" >> $GITHUB_ENV echo "changelog_body=$(cat artifacts/changelog.md)" >> $GITHUB_ENV
- name: Validate release environment variables
run: |
echo "Release version: ${{ env.RELEASE_VERSION }}"
echo "Changelog body: ${{ env.changelog_body }}"
- name: Create a GitHub Release - name: Create a GitHub Release
if: env.ACT != 'true'
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -316,6 +389,12 @@ jobs:
name: Publish Chocolatey Package name: Publish Chocolatey Package
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -344,6 +423,7 @@ jobs:
echo "Release version: ${{ env.RELEASE_VERSION }}" echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Package and Publish package to Chocolatey - name: Package and Publish package to Chocolatey
if: env.ACT != 'true'
run: | run: |
mkdir ./deployment/chocolatey/tools mkdir ./deployment/chocolatey/tools
# Run packaging script # Run packaging script
@@ -363,6 +443,12 @@ jobs:
name: Update Homebrew formulas name: Update Homebrew formulas
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -395,11 +481,13 @@ jobs:
echo "Release version: ${{ env.RELEASE_VERSION }}" echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Execute Homebrew packaging script - name: Execute Homebrew packaging script
if: env.ACT != 'true'
run: | run: |
# run packaging script # run packaging script
python "./deployment/homebrew/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/homebrew/managarr.rb.template" "./managarr.rb" ${{ env.MACOS_SHA }} ${{ env.MACOS_SHA_ARM }} ${{ env.LINUX_SHA }} python "./deployment/homebrew/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/homebrew/managarr.rb.template" "./managarr.rb" ${{ env.MACOS_SHA }} ${{ env.MACOS_SHA_ARM }} ${{ env.LINUX_SHA }}
- name: Push changes to Homebrew tap - name: Push changes to Homebrew tap
if: env.ACT != 'true'
env: env:
TOKEN: ${{ secrets.MANAGARR_GITHUB_TOKEN }} TOKEN: ${{ secrets.MANAGARR_GITHUB_TOKEN }}
run: | run: |
@@ -419,6 +507,12 @@ jobs:
name: Publishing Docker image to Docker Hub name: Publishing Docker image to Docker Hub
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -431,6 +525,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Ensure repository is up-to-date - name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: | run: |
git fetch --all git fetch --all
git pull git pull
@@ -451,6 +546,7 @@ jobs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub - name: Login to Docker Hub
if: env.ACT != 'true'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
@@ -462,7 +558,7 @@ jobs:
context: . context: .
file: Dockerfile file: Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: ${{ env.ACT != 'true' }}
tags: darkalex17/managarr:latest, darkalex17/managarr:${{ env.version }} tags: darkalex17/managarr:latest, darkalex17/managarr:${{ env.version }}
publish-crate: publish-crate:
@@ -471,7 +567,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check if actor is repository owner - name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }} if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: | run: |
echo "You are not authorized to run this workflow." echo "You are not authorized to run this workflow."
exit 1 exit 1
@@ -481,7 +577,15 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Get bumped Cargo files (Act)
if: env.ACT == 'true'
uses: actions/download-artifact@v4
with:
name: bumped-cargo-files
path: ${{ github.workspace }}
- name: Ensure repository is up-to-date - name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: | run: |
git fetch --all git fetch --all
git pull git pull
@@ -501,5 +605,6 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- uses: katyo/publish-crates@v2 - uses: katyo/publish-crates@v2
if: env.ACT != 'true'
with: with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+1
View File
@@ -175,6 +175,7 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Upload to codecov.io - name: Upload to codecov.io
if: env.ACT != 'true'
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
with: with:
fail_ci_if_error: true fail_ci_if_error: true
+1
View File
@@ -1,3 +1,4 @@
/target /target
/.idea/ /.idea/
/.scannerwork/ /.scannerwork/
/.act/
+21 -1
View File
@@ -63,12 +63,32 @@ cz commit
## Setup workspace ## Setup workspace
1. Clone this repo 1. Clone this repo
2. Run `cargo test` to setup hooks 2. Run `cargo test` to set up hooks
3. Make changes 3. Make changes
4. Run the application using `make run` or `cargo run` 4. Run the application using `make run` or `cargo run`
5. Commit changes. This will trigger pre-commit hooks that will run format, test and lint. If there are errors or warnings from Clippy, please fix them. 5. Commit changes. This will trigger pre-commit hooks that will run format, test and lint. If there are errors or warnings from Clippy, please fix them.
6. Push your code to a new branch named after the feature/bug/etc. you're adding. This will trigger pre-push hooks that will run lint and test. 6. Push your code to a new branch named after the feature/bug/etc. you're adding. This will trigger pre-push hooks that will run lint and test.
7. Create a PR 7. Create a PR
### CI/CD Testing with Act
If you also are planning on testing out your changes before pushing them with [Act](https://github.com/nektos/act), you will need to set up `act`,
`docker`, and configure your local system to run different architectures:
1. Install `docker` by following the instructions on the [official Docker installation page](https://docs.docker.com/get-docker/).
2. Install `act` by following the instructions on the [official Act installation page](https://nektosact.com/installation/index.html).
3. Install `binfmt` on your system once so that `act` can run the correct architecture for the CI/CD workflows.
You can do this by running:
```shell
sudo docker run --rm --privileged tonistiigi/binfmt --install all
```
Then, you can run workflows locally without having to commit and see if the GitHub action passes or fails.
**For example**: To test the [release.yml](.github/workflows/release.yml) workflow locally, you can run:
```shell
act -W .github/workflows/release.yml --input_type bump=minor
```
## Questions? Reach out to me! ## Questions? Reach out to me!
If you encounter any questions while developing Managarr, please don't hesitate to reach out to me at alex.j.tusa@gmail.com. I'm happy to help contributors, new and experienced in any way I can! If you encounter any questions while developing Managarr, please don't hesitate to reach out to me at alex.j.tusa@gmail.com. I'm happy to help contributors, new and experienced in any way I can!
Generated
+592 -444
View File
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -10,11 +10,14 @@ homepage = "https://github.com/Dark-Alex-17/managarr"
readme = "README.md" readme = "README.md"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
rust-version = "1.85.0" rust-version = "1.89.0"
exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"] exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
[workspace] [workspace]
members = ["proc_macros/enum_display_style_derive"] members = [
"proc_macros/enum_display_style_derive",
"proc_macros/validate_theme_derive",
]
[dependencies] [dependencies]
anyhow = "1.0.68" anyhow = "1.0.68"
@@ -37,7 +40,7 @@ serde_json = "1.0.91"
serde = { version = "1.0.214", features = ["derive"] } serde = { version = "1.0.214", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] }
strum_macros = "0.26.4" strum_macros = "0.26.4"
tokio = { version = "1.36.0", features = ["full"] } tokio = { version = "1.44.2", features = ["full"] }
tokio-util = "0.7.8" tokio-util = "0.7.8"
ratatui = { version = "0.29.0", features = [ ratatui = { version = "0.29.0", features = [
"all-widgets", "all-widgets",
@@ -63,6 +66,7 @@ deunicode = "1.6.0"
paste = "1.0.15" paste = "1.0.15"
openssl = { version = "0.10.70", features = ["vendored"] } openssl = { version = "0.10.70", features = ["vendored"] }
veil = "0.2.0" veil = "0.2.0"
validate_theme_derive = { path = "proc_macros/validate_theme_derive" }
enum_display_style_derive = { path = "proc_macros/enum_display_style_derive" } enum_display_style_derive = { path = "proc_macros/enum_display_style_derive" }
[dev-dependencies] [dev-dependencies]
+2 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.85 AS builder FROM rust:1.89 AS builder
WORKDIR /usr/src WORKDIR /usr/src
# Download and compile Rust dependencies in an empty project and cache as a separate Docker layer # Download and compile Rust dependencies in an empty project and cache as a separate Docker layer
@@ -6,6 +6,7 @@ RUN USER=root cargo new --bin managarr-temp
WORKDIR /usr/src/managarr-temp WORKDIR /usr/src/managarr-temp
COPY Cargo.* . COPY Cargo.* .
COPY proc_macros ./proc_macros
RUN cargo build --release RUN cargo build --release
# remove src from empty project # remove src from empty project
RUN rm -r src RUN rm -r src
+40 -20
View File
@@ -28,7 +28,7 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built
- [ ] ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/) - [ ] ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/)
- [ ] ![tautulli_logo](logos/tautulli.png) [Tautulli](https://tautulli.com/) - [ ] ![tautulli_logo](logos/tautulli.png) [Tautulli](https://tautulli.com/)
## Try Before You Buy ## Try Out the Demo
To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository. To try out Managarr before linking it to your HTPC, you can use the purpose built [managarr-demo](https://github.com/Dark-Alex-17/managarr-demo) repository.
Simply run the following command to start a demo: Simply run the following command to start a demo:
@@ -63,7 +63,7 @@ Please note that you will need to create and popular your configuration file fir
**Note:** If you run into errors using relative file paths when mounting the volume with the configuration file, try using an absolute path. **Note:** If you run into errors using relative file paths when mounting the volume with the configuration file, try using an absolute path.
### Homebrew (Mac and Linux) ### Homebrew (Mac and Linux)
To install Managarr from Homebrew, install the Managarr tap and then you'll be able to install Managarr: To install Managarr from Homebrew, install the Managarr tap. Then you'll be able to install Managarr:
```shell ```shell
brew tap Dark-Alex-17/managarr brew tap Dark-Alex-17/managarr
@@ -119,16 +119,16 @@ Binaries are available on the [releases](https://github.com/Dark-Alex-17/managar
#### Windows Instructions #### Windows Instructions
To use a binary from the releases page on Windows, do the following: To use a binary from the releases page on Windows, do the following:
1. Download the latest binary [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS. 1. Download the latest [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS.
2. Use 7-Zip or TarTool to unpack the Tar file. 2. Use 7-Zip or TarTool to unpack the Tar file.
3. Run the executable `managarr.exe`! 3. Run the executable `managarr.exe`!
#### Linux/MacOS Instructions #### Linux/MacOS Instructions
To use a binary from the releases page on Linux/MacOS, do the following: To use a binary from the releases page on Linux/MacOS, do the following:
1. Download the latest binary [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS. 1. Download the latest [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS.
2. `cd` to the directory where you downloaded the binary. 2. `cd` to the directory where you downloaded the binary.
3. Extract the binary with `tar -C /usr/local/bin -xzf managarr-<arch>.tar.gz` (NB: This may require `sudo`) 3. Extract the binary with `tar -C /usr/local/bin -xzf managarr-<arch>.tar.gz` (Note: This may require `sudo`)
4. Now you can run `managarr`! 4. Now you can run `managarr`!
## Features ## Features
@@ -166,21 +166,21 @@ Key:
| TUI | CLI | Feature | | TUI | CLI | Feature |
|-----|-----|--------------------------------------------------------------------------------------------------------------------| |-----|-----|--------------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, blocklist, episodes | | ✅ | ✅ | View your library, downloads, blocklist, episodes |
| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits | | ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings | | 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings |
| ✅ | ✅ | Search your library | | ✅ | ✅ | Search your library |
| ✅ | ✅ | Add series to your library | | ✅ | ✅ | Add series to your library |
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files | | ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes | | ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads | | ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
| ✅ | ✅ | Manually search for series, seasons, or episodes | | ✅ | ✅ | Manually search for series, seasons, or episodes |
| ✅ | ✅ | Edit your series and indexers | | ✅ | ✅ | Edit your series and indexers |
| ✅ | ✅ | Manage your tags | | ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders | | ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist | | ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates | | ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks | | ✅ | ✅ | Manually trigger scheduled tasks |
### Readarr ### Readarr
@@ -206,6 +206,19 @@ Key:
- [ ] Support for Tautulli - [ ] Support for Tautulli
### Themes
Managarr ships with a few themes out of the box. Here's a few examples:
#### Default
![default](themes/default/manual_episode_search.png)
#### Dracula
![dracula](themes/dracula/manual_episode_search.png)
#### Watermelon Dark
![watermelon-dark](themes/watermelon-dark/manual_episode_search.png)
You can also create your own custom themes as well. To learn more about what themes are built-in to Managarr and how
to create your own custom themes, check out the [Themes README](themes/README.md).
### The Managarr CLI ### The Managarr CLI
Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs. Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs.
@@ -218,7 +231,7 @@ To see all available commands, simply run `managarr --help`:
```shell ```shell
$ managarr --help $ managarr --help
managarr 0.5.0 managarr 0.5.1
Alex Clarke <alex.j.tusa@gmail.com> Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs A TUI and CLI to manage your Servarrs
@@ -235,6 +248,8 @@ Commands:
Options: Options:
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=] --disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
--config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=] --config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
--themes-file <THEMES_FILE> The Managarr themes file to use [env: MANAGARR_THEMES_FILE=]
--theme <THEME> The name of the Managarr theme to use [env: MANAGARR_THEME=]
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use. --servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
This is useful when you have multiple instances of the same Servarr defined in your config file. This is useful when you have multiple instances of the same Servarr defined in your config file.
By default, if left empty, the first configured Servarr instance listed in the config file will be used. By default, if left empty, the first configured Servarr instance listed in the config file will be used.
@@ -315,6 +330,7 @@ managarr --config-file /path/to/config.yml
### Example Configuration: ### Example Configuration:
```yaml ```yaml
theme: default
radarr: radarr:
- host: 192.168.0.78 - host: 192.168.0.78
port: 7878 port: 7878
@@ -341,6 +357,9 @@ whisparr:
port: 6969 port: 6969
api_token: someApiToken1234567890 api_token: someApiToken1234567890
ssl_cert_path: /path/to/whisparr.crt ssl_cert_path: /path/to/whisparr.crt
custom_headers: # Example of adding custom headers to all requests to the Servarr instance
traefik-auth-bypass-key: someBypassKey1234567890
SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE}
bazarr: bazarr:
- host: 192.168.0.67 - host: 192.168.0.67
port: 6767 port: 6767
@@ -357,6 +376,7 @@ tautulli:
### Example Multi-Instance Configuration: ### Example Multi-Instance Configuration:
```yaml ```yaml
theme: default
radarr: radarr:
- host: 192.168.0.78 # No name specified, so this instance's name will default to 'Radarr 1' - host: 192.168.0.78 # No name specified, so this instance's name will default to 'Radarr 1'
port: 7878 port: 7878
@@ -4,7 +4,8 @@ use crate::macro_models::DisplayStyleArgs;
use darling::FromVariant; use darling::FromVariant;
use quote::quote; use quote::quote;
use syn::{Data, DeriveInput, parse_macro_input}; use syn::{Data, DeriveInput, parse_macro_input};
/// Derive macro for the EnumDisplayStyle trait.
/// Derive macro for generating a `to_display_str` method for an enum.
/// ///
/// # Example /// # Example
/// ///
@@ -15,13 +16,12 @@ use syn::{Data, DeriveInput, parse_macro_input};
/// ///
/// #[derive(EnumDisplayStyle)] /// #[derive(EnumDisplayStyle)]
/// enum Weekend { /// enum Weekend {
/// Saturday, /// Saturday,
/// Sunday, /// Sunday,
/// } /// }
/// ///
/// assert_eq!(Weekend::Saturday.to_display_str(), "Saturday"); /// assert_eq!(Weekend::Saturday.to_display_str(), "Saturday");
/// assert_eq!(Weekend::Sunday.to_display_str(), "Sunday"); /// assert_eq!(Weekend::Sunday.to_display_str(), "Sunday");
///
/// ``` /// ```
/// ///
/// Using custom values for the display style: /// Using custom values for the display style:
@@ -31,10 +31,10 @@ use syn::{Data, DeriveInput, parse_macro_input};
/// ///
/// #[derive(EnumDisplayStyle)] /// #[derive(EnumDisplayStyle)]
/// enum MonitorStatus { /// enum MonitorStatus {
/// #[display_style(name = "Monitor Transactions")] /// #[display_style(name = "Monitor Transactions")]
/// Active, /// Active,
/// #[display_style(name = "Don't Monitor Transactions")] /// #[display_style(name = "Don't Monitor Transactions")]
/// None, /// None,
/// } /// }
/// ///
/// assert_eq!(MonitorStatus::Active.to_display_str(), "Monitor Transactions"); /// assert_eq!(MonitorStatus::Active.to_display_str(), "Monitor Transactions");
@@ -0,0 +1,14 @@
[package]
name = "validate_theme_derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.39"
syn = "2.0.99"
[dev-dependencies]
log = "0.4.17"
@@ -0,0 +1,106 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, parse_macro_input};
/// Derive macro for generating a `validate` method for a Theme struct.
/// The `validate` method ensures that all values with the `validate` attribute are not `None`.
/// Otherwise, an error message it output to both the log file and stdout and the program exits.
///
/// # Example
///
/// Valid themes pass through the program transitively without any messages being output.
///
/// ```
/// use validate_theme_derive::ValidateTheme;
///
/// #[derive(ValidateTheme, Default)]
/// struct Theme {
/// pub name: String,
/// #[validate]
/// pub good: Option<Style>,
/// #[validate]
/// pub bad: Option<Style>,
/// pub ugly: Option<Style>,
/// }
///
/// struct Style {
/// color: String,
/// }
///
/// let theme = Theme {
/// good: Some(Style { color: "Green".to_owned() }),
/// bad: Some(Style { color: "Red".to_owned() }),
/// ..Theme::default()
/// };
///
/// // Since only `good` and `bad` have the `validate` attribute, the `validate` method will only check those fields.
/// theme.validate();
/// // Since both `good` and `bad` have values, the program will not exit and no message is output.
/// ```
///
/// Invalid themes will output an error message to both the log file and stdout and the program will exit.
///
/// ```should_panic
/// use validate_theme_derive::ValidateTheme;
///
/// #[derive(ValidateTheme, Default)]
/// struct Theme {
/// pub name: String,
/// #[validate]
/// pub good: Option<Style>,
/// #[validate]
/// pub bad: Option<Style>,
/// pub ugly: Option<Style>,
/// }
///
/// struct Style {
/// color: String,
/// }
///
/// let theme = Theme {
/// bad: Some(Style { color: "Red".to_owned() }),
/// ..Theme::default()
/// };
///
/// // Since `good` has the `validate` attribute and since `good` is `None`, the `validate` method will output an error message and exit the program.
/// theme.validate();
/// ```
#[proc_macro_derive(ValidateTheme, attributes(validate))]
pub fn derive_validate_theme(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let mut validation_checks = Vec::new();
if let Data::Struct(data_struct) = &input.data
&& let Fields::Named(fields) = &data_struct.fields
{
for field in &fields.named {
let field_name = &field.ident;
let has_validate_attr = field
.attrs
.iter()
.any(|attr| attr.path().is_ident("validate"));
if has_validate_attr {
validation_checks.push(quote! {
if self.#field_name.is_none() {
log::error!("{} is missing a color value.", stringify!(#field_name));
eprintln!("{} is missing a color value.", stringify!(#field_name));
std::process::exit(1);
}
})
}
}
}
quote! {
impl #struct_name {
pub fn validate(&self) {
#(#validation_checks)*
}
}
}
.into()
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 220 KiB

+107 -22
View File
@@ -2,10 +2,11 @@
mod tests { mod tests {
use anyhow::anyhow; use anyhow::anyhow;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde_json::Value;
use serial_test::serial; use serial_test::serial;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::app::{interpolate_env_vars, App, AppConfig, Data, ServarrConfig}; use crate::app::{interpolate_env_vars, App, AppConfig, Data, ServarrConfig};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
@@ -31,6 +32,7 @@ mod tests {
}; };
let sonarr_config_2 = ServarrConfig::default(); let sonarr_config_2 = ServarrConfig::default();
let config = AppConfig { let config = AppConfig {
theme: None,
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]), radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]), sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
}; };
@@ -38,40 +40,24 @@ mod tests {
TabRoute { TabRoute {
title: "Sonarr Test".to_owned(), title: "Sonarr Test".to_owned(),
route: ActiveSonarrBlock::default().into(), route: ActiveSonarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(sonarr_config_1), config: Some(sonarr_config_1),
}, },
TabRoute { TabRoute {
title: "Radarr 1".to_owned(), title: "Radarr 1".to_owned(),
route: ActiveRadarrBlock::default().into(), route: ActiveRadarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(radarr_config_2), config: Some(radarr_config_2),
}, },
TabRoute { TabRoute {
title: "Radarr Test".to_owned(), title: "Radarr Test".to_owned(),
route: ActiveRadarrBlock::default().into(), route: ActiveRadarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(radarr_config_1), config: Some(radarr_config_1),
}, },
TabRoute { TabRoute {
title: "Sonarr 1".to_owned(), title: "Sonarr 1".to_owned(),
route: ActiveSonarrBlock::default().into(), route: ActiveSonarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(sonarr_config_2), config: Some(sonarr_config_2),
}, },
@@ -97,7 +83,7 @@ mod tests {
assert!(!app.is_loading); assert!(!app.is_loading);
assert!(!app.is_routing); assert!(!app.is_routing);
assert!(!app.should_refresh); assert!(!app.should_refresh);
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app.cli_mode); assert!(!app.cli_mode);
} }
@@ -117,7 +103,7 @@ mod tests {
assert!(!app.is_loading); assert!(!app.is_loading);
assert!(!app.is_routing); assert!(!app.is_routing);
assert!(!app.should_refresh); assert!(!app.should_refresh);
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app.cli_mode); assert!(!app.cli_mode);
} }
@@ -283,7 +269,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads(500).into()
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
@@ -356,6 +342,43 @@ mod tests {
assert_eq!(servarr_config.api_token, Some(String::new())); assert_eq!(servarr_config.api_token, Some(String::new()));
assert_eq!(servarr_config.api_token_file, None); assert_eq!(servarr_config.api_token_file, None);
assert_eq!(servarr_config.ssl_cert_path, None); assert_eq!(servarr_config.ssl_cert_path, None);
assert_eq!(servarr_config.custom_headers, None);
}
#[test]
fn serialize_header_map_basic() {
let mut header_map = HeaderMap::new();
header_map.insert(
HeaderName::from_static("x-api-key"),
HeaderValue::from_static("abc123"),
);
header_map.insert(
HeaderName::from_static("header-1"),
HeaderValue::from_static("test"),
);
let config = ServarrConfig {
custom_headers: Some(header_map),
..ServarrConfig::default()
};
let v: Value = serde_json::to_value(&config).expect("serialize ok");
let custom = v.get("custom_headers").unwrap();
assert!(custom.is_object());
let obj = custom.as_object().unwrap();
assert_eq!(obj.get("x-api-key").unwrap(), "abc123");
assert_eq!(obj.get("header-1").unwrap(), "test");
assert!(obj.get("X-Api-Key").is_none());
assert!(obj.get("HEADER-1").is_none());
}
#[test]
fn serialize_header_map_none_is_null() {
let config = ServarrConfig::default();
let v: Value = serde_json::to_value(&config).expect("serialize ok");
assert!(v.get("custom_headers").unwrap().is_null());
} }
#[test] #[test]
@@ -399,6 +422,66 @@ mod tests {
assert_eq!(config.port, None); assert_eq!(config.port, None);
} }
#[test]
#[serial]
fn test_deserialize_optional_env_var_header_map_is_present() {
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_HEADER_OPTION", "localhost") };
let expected_custom_headers = {
let mut headers = HeaderMap::new();
headers.insert("X-Api-Host", "localhost".parse().unwrap());
headers.insert("api-token", "test123".parse().unwrap());
headers
};
let yaml_data = r#"
custom_headers:
X-Api-Host: ${TEST_VAR_DESERIALIZE_HEADER_OPTION}
api-token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.custom_headers, Some(expected_custom_headers));
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_HEADER_OPTION") };
}
#[test]
#[serial]
fn test_deserialize_optional_env_var_header_map_does_not_overwrite_non_env_value() {
unsafe {
std::env::set_var(
"TEST_VAR_DESERIALIZE_HEADER_OPTION_NO_OVERWRITE",
"localhost",
)
};
let expected_custom_headers = {
let mut headers = HeaderMap::new();
headers.insert("X-Api-Host", "www.example.com".parse().unwrap());
headers.insert("api-token", "test123".parse().unwrap());
headers
};
let yaml_data = r#"
custom_headers:
X-Api-Host: www.example.com
api-token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.custom_headers, Some(expected_custom_headers));
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_HEADER_OPTION_NO_OVERWRITE") };
}
#[test]
fn test_deserialize_optional_env_var_header_map_empty() {
let yaml_data = r#"
api_token: "test123"
"#;
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
assert_eq!(config.custom_headers, None);
}
#[test] #[test]
#[serial] #[serial]
fn test_deserialize_optional_u16_env_var_is_present() { fn test_deserialize_optional_u16_env_var_is_present() {
@@ -512,8 +595,9 @@ mod tests {
let api_token = "thisisatest".to_owned(); let api_token = "thisisatest".to_owned();
let api_token_file = "/root/.config/api_token".to_owned(); let api_token_file = "/root/.config/api_token".to_owned();
let ssl_cert_path = "/some/path".to_owned(); let ssl_cert_path = "/some/path".to_owned();
let expected_str = format!("ServarrConfig {{ name: Some(\"{}\"), host: Some(\"{}\"), port: Some({}), uri: Some(\"{}\"), weight: Some({}), api_token: Some(\"***********\"), api_token_file: Some(\"{}\"), ssl_cert_path: Some(\"{}\") }}", let mut custom_headers = HeaderMap::new();
name, host, port, uri, weight, api_token_file, ssl_cert_path); custom_headers.insert("X-Custom-Header", "value".parse().unwrap());
let expected_str = format!("ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}) }}");
let servarr_config = ServarrConfig { let servarr_config = ServarrConfig {
name: Some(name), name: Some(name),
host: Some(host), host: Some(host),
@@ -523,6 +607,7 @@ mod tests {
api_token: Some(api_token), api_token: Some(api_token),
api_token_file: Some(api_token_file), api_token_file: Some(api_token_file),
ssl_cert_path: Some(ssl_cert_path), ssl_cert_path: Some(ssl_cert_path),
custom_headers: Some(custom_headers),
}; };
assert_str_eq!(format!("{servarr_config:?}"), expected_str); assert_str_eq!(format!("{servarr_config:?}"), expected_str);
+30 -8
View File
@@ -1,20 +1,41 @@
use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS}; use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS};
use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider;
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
use crate::app::App;
use crate::models::Route;
#[cfg(test)] #[cfg(test)]
#[path = "context_clues_tests.rs"] #[path = "context_clues_tests.rs"]
mod context_clues_tests; mod context_clues_tests;
pub(in crate::app) type ContextClue = (KeyBinding, &'static str); pub type ContextClue = (KeyBinding, &'static str);
pub fn build_context_clue_string(context_clues: &[(KeyBinding, &str)]) -> String { pub trait ContextClueProvider {
context_clues fn get_context_clues(_app: &mut App<'_>) -> Option<&'static [ContextClue]>;
.iter()
.map(|(key_binding, desc)| format!("{} {desc}", key_binding.key))
.collect::<Vec<String>>()
.join(" | ")
} }
pub static SERVARR_CONTEXT_CLUES: [ContextClue; 3] = [ pub struct ServarrContextClueProvider;
impl ContextClueProvider for ServarrContextClueProvider {
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
match app.get_current_route() {
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
_ => None,
}
}
}
pub static SERVARR_CONTEXT_CLUES: [ContextClue; 10] = [
(DEFAULT_KEYBINDINGS.up, "scroll up"),
(DEFAULT_KEYBINDINGS.down, "scroll down"),
(DEFAULT_KEYBINDINGS.left, "previous tab"),
(DEFAULT_KEYBINDINGS.right, "next tab"),
(DEFAULT_KEYBINDINGS.pg_up, DEFAULT_KEYBINDINGS.pg_up.desc),
(
DEFAULT_KEYBINDINGS.pg_down,
DEFAULT_KEYBINDINGS.pg_down.desc,
),
( (
DEFAULT_KEYBINDINGS.next_servarr, DEFAULT_KEYBINDINGS.next_servarr,
DEFAULT_KEYBINDINGS.next_servarr.desc, DEFAULT_KEYBINDINGS.next_servarr.desc,
@@ -24,6 +45,7 @@ pub static SERVARR_CONTEXT_CLUES: [ContextClue; 3] = [
DEFAULT_KEYBINDINGS.previous_servarr.desc, DEFAULT_KEYBINDINGS.previous_servarr.desc,
), ),
(DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc), (DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc),
(DEFAULT_KEYBINDINGS.help, DEFAULT_KEYBINDINGS.help.desc),
]; ];
pub static BARE_POPUP_CONTEXT_CLUES: [ContextClue; 1] = pub static BARE_POPUP_CONTEXT_CLUES: [ContextClue; 1] =
+81 -17
View File
@@ -3,24 +3,15 @@ mod test {
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use crate::app::context_clues::{ use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider, ServarrContextClueProvider, BARE_POPUP_CONTEXT_CLUES,
DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES,
SYSTEM_CONTEXT_CLUES,
}; };
use crate::app::{context_clues::build_context_clue_string, key_binding::DEFAULT_KEYBINDINGS}; use crate::app::{key_binding::DEFAULT_KEYBINDINGS, App};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
#[test] use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
fn test_build_context_clue_string() { use crate::models::servarr_data::ActiveKeybindingBlock;
let test_context_clues_array = [
(DEFAULT_KEYBINDINGS.add, "add"),
(DEFAULT_KEYBINDINGS.delete, "delete"),
];
assert_str_eq!(
build_context_clue_string(&test_context_clues_array),
"<a> add | <del> delete"
);
}
#[test] #[test]
fn test_servarr_context_clues() { fn test_servarr_context_clues() {
@@ -28,6 +19,36 @@ mod test {
let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.up);
assert_str_eq!(*description, "scroll up");
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.down);
assert_str_eq!(*description, "scroll down");
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.left);
assert_str_eq!(*description, "previous tab");
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.right);
assert_str_eq!(*description, "next tab");
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.pg_up);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.pg_up.desc);
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.pg_down);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.pg_down.desc);
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.next_servarr); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.next_servarr);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.next_servarr.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.next_servarr.desc);
@@ -40,6 +61,11 @@ mod test {
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.quit); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.quit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.quit.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.quit.desc);
let (key_binding, description) = servarr_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.help);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.help.desc);
assert_eq!(servarr_context_clues_iter.next(), None); assert_eq!(servarr_context_clues_iter.next(), None);
} }
@@ -209,4 +235,42 @@ mod test {
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(system_context_clues_iter.next(), None); assert_eq!(system_context_clues_iter.next(), None);
} }
#[test]
fn test_servarr_context_clue_provider_delegates_to_radarr_provider() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(
&crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
context_clues.unwrap()
);
}
#[test]
fn test_servarr_context_clue_provider_delegates_to_sonarr_provider() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into());
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(
&crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
context_clues.unwrap()
);
}
#[test]
fn test_servarr_context_clue_provider_unsupported_route_returns_none() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveKeybindingBlock::Help.into());
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_none());
}
} }
+79 -1
View File
@@ -14,6 +14,8 @@ generate_keybindings! {
down, down,
left, left,
right, right,
pg_down,
pg_up,
backspace, backspace,
next_servarr, next_servarr,
previous_servarr, previous_servarr,
@@ -21,6 +23,7 @@ generate_keybindings! {
search, search,
auto_search, auto_search,
settings, settings,
help,
filter, filter,
sort, sort,
edit, edit,
@@ -44,128 +47,203 @@ generate_keybindings! {
#[derive(Clone, Copy, Eq, PartialEq, Debug)] #[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct KeyBinding { pub struct KeyBinding {
pub key: Key, pub key: Key,
pub alt: Option<Key>,
pub desc: &'static str, pub desc: &'static str,
} }
pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
add: KeyBinding { add: KeyBinding {
key: Key::Char('a'), key: Key::Char('a'),
alt: None,
desc: "add", desc: "add",
}, },
up: KeyBinding { up: KeyBinding {
key: Key::Up, key: Key::Up,
alt: Some(Key::Char('k')),
desc: "up", desc: "up",
}, },
down: KeyBinding { down: KeyBinding {
key: Key::Down, key: Key::Down,
alt: Some(Key::Char('j')),
desc: "down", desc: "down",
}, },
left: KeyBinding { left: KeyBinding {
key: Key::Left, key: Key::Left,
alt: Some(Key::Char('h')),
desc: "left", desc: "left",
}, },
right: KeyBinding { right: KeyBinding {
key: Key::Right, key: Key::Right,
alt: Some(Key::Char('l')),
desc: "right", desc: "right",
}, },
pg_down: KeyBinding {
key: Key::PgDown,
alt: Some(Key::Ctrl('d')),
desc: "page down",
},
pg_up: KeyBinding {
key: Key::PgUp,
alt: Some(Key::Ctrl('u')),
desc: "page up",
},
backspace: KeyBinding { backspace: KeyBinding {
key: Key::Backspace, key: Key::Backspace,
alt: Some(Key::Ctrl('h')),
desc: "backspace", desc: "backspace",
}, },
next_servarr: KeyBinding { next_servarr: KeyBinding {
key: Key::Tab, key: Key::Tab,
alt: None,
desc: "next servarr", desc: "next servarr",
}, },
previous_servarr: KeyBinding { previous_servarr: KeyBinding {
key: Key::BackTab, key: Key::BackTab,
alt: None,
desc: "previous servarr", desc: "previous servarr",
}, },
clear: KeyBinding { clear: KeyBinding {
key: Key::Char('c'), key: Key::Char('c'),
alt: None,
desc: "clear", desc: "clear",
}, },
auto_search: KeyBinding { auto_search: KeyBinding {
key: Key::Char('S'), key: Key::Char('S'),
alt: None,
desc: "auto search", desc: "auto search",
}, },
search: KeyBinding { search: KeyBinding {
key: Key::Char('s'), key: Key::Char('s'),
alt: None,
desc: "search", desc: "search",
}, },
settings: KeyBinding { settings: KeyBinding {
key: Key::Char('S'), key: Key::Char('S'),
alt: None,
desc: "settings", desc: "settings",
}, },
help: KeyBinding {
key: Key::Char('?'),
alt: None,
desc: "show/hide keybindings",
},
filter: KeyBinding { filter: KeyBinding {
key: Key::Char('f'), key: Key::Char('f'),
alt: None,
desc: "filter", desc: "filter",
}, },
sort: KeyBinding { sort: KeyBinding {
key: Key::Char('o'), key: Key::Char('o'),
alt: None,
desc: "sort", desc: "sort",
}, },
edit: KeyBinding { edit: KeyBinding {
key: Key::Char('e'), key: Key::Char('e'),
alt: None,
desc: "edit", desc: "edit",
}, },
events: KeyBinding { events: KeyBinding {
key: Key::Char('e'), key: Key::Char('e'),
alt: None,
desc: "events", desc: "events",
}, },
logs: KeyBinding { logs: KeyBinding {
key: Key::Char('l'), key: Key::Char('L'),
alt: None,
desc: "logs", desc: "logs",
}, },
tasks: KeyBinding { tasks: KeyBinding {
key: Key::Char('t'), key: Key::Char('t'),
alt: None,
desc: "tasks", desc: "tasks",
}, },
test: KeyBinding { test: KeyBinding {
key: Key::Char('t'), key: Key::Char('t'),
alt: None,
desc: "test", desc: "test",
}, },
test_all: KeyBinding { test_all: KeyBinding {
key: Key::Char('T'), key: Key::Char('T'),
alt: None,
desc: "test all", desc: "test all",
}, },
toggle_monitoring: KeyBinding { toggle_monitoring: KeyBinding {
key: Key::Char('m'), key: Key::Char('m'),
alt: None,
desc: "toggle monitoring", desc: "toggle monitoring",
}, },
refresh: KeyBinding { refresh: KeyBinding {
key: Key::Ctrl('r'), key: Key::Ctrl('r'),
alt: None,
desc: "refresh", desc: "refresh",
}, },
update: KeyBinding { update: KeyBinding {
key: Key::Char('u'), key: Key::Char('u'),
alt: None,
desc: "update", desc: "update",
}, },
home: KeyBinding { home: KeyBinding {
key: Key::Home, key: Key::Home,
alt: None,
desc: "home", desc: "home",
}, },
end: KeyBinding { end: KeyBinding {
key: Key::End, key: Key::End,
alt: None,
desc: "end", desc: "end",
}, },
delete: KeyBinding { delete: KeyBinding {
key: Key::Delete, key: Key::Delete,
alt: None,
desc: "delete", desc: "delete",
}, },
submit: KeyBinding { submit: KeyBinding {
key: Key::Enter, key: Key::Enter,
alt: None,
desc: "submit", desc: "submit",
}, },
confirm: KeyBinding { confirm: KeyBinding {
key: Key::Ctrl('s'), key: Key::Ctrl('s'),
alt: None,
desc: "submit", desc: "submit",
}, },
quit: KeyBinding { quit: KeyBinding {
key: Key::Char('q'), key: Key::Char('q'),
alt: None,
desc: "quit", desc: "quit",
}, },
esc: KeyBinding { esc: KeyBinding {
key: Key::Esc, key: Key::Esc,
alt: None,
desc: "close", desc: "close",
}, },
}; };
#[macro_export]
macro_rules! matches_key {
($binding:ident, $key:expr) => {
$crate::app::key_binding::DEFAULT_KEYBINDINGS.$binding.key == $key
|| ($crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.is_some()
&& $crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.unwrap()
== $key)
};
($binding:ident, $key:expr, $ignore_special_keys:expr) => {
$crate::app::key_binding::DEFAULT_KEYBINDINGS.$binding.key == $key
|| !$ignore_special_keys
&& ($crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.is_some()
&& $crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.unwrap()
== $key)
};
}
+74 -30
View File
@@ -5,44 +5,88 @@ mod test {
use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS}; use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS};
use crate::event::Key; use crate::event::Key;
use crate::matches_key;
#[rstest] #[rstest]
#[case(DEFAULT_KEYBINDINGS.add, Key::Char('a'), "add")] #[case(DEFAULT_KEYBINDINGS.add, Key::Char('a'), None, "add")]
#[case(DEFAULT_KEYBINDINGS.up, Key::Up, "up")] #[case(DEFAULT_KEYBINDINGS.up, Key::Up, Some(Key::Char('k')), "up")]
#[case(DEFAULT_KEYBINDINGS.down, Key::Down, "down")] #[case(DEFAULT_KEYBINDINGS.down, Key::Down, Some(Key::Char('j')), "down")]
#[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")] #[case(DEFAULT_KEYBINDINGS.left, Key::Left, Some(Key::Char('h')), "left")]
#[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")] #[case(DEFAULT_KEYBINDINGS.right, Key::Right, Some(Key::Char('l')), "right")]
#[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, "backspace")] #[case(DEFAULT_KEYBINDINGS.pg_down, Key::PgDown, Some(Key::Ctrl('d')), "page down")]
#[case(DEFAULT_KEYBINDINGS.next_servarr, Key::Tab, "next servarr")] #[case(DEFAULT_KEYBINDINGS.pg_up, Key::PgUp, Some(Key::Ctrl('u')), "page up")]
#[case(DEFAULT_KEYBINDINGS.previous_servarr, Key::BackTab, "previous servarr")] #[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, Some(Key::Ctrl('h')), "backspace")]
#[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), "clear")] #[case(DEFAULT_KEYBINDINGS.next_servarr, Key::Tab, None, "next servarr")]
#[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), "auto search")] #[case(DEFAULT_KEYBINDINGS.previous_servarr, Key::BackTab, None, "previous servarr")]
#[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), "search")] #[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), None, "clear")]
#[case(DEFAULT_KEYBINDINGS.settings, Key::Char('S'), "settings")] #[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), None, "auto search")]
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), "filter")] #[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), None, "search")]
#[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), "sort")] #[case(DEFAULT_KEYBINDINGS.settings, Key::Char('S'), None, "settings")]
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), "edit")] #[case(DEFAULT_KEYBINDINGS.help, Key::Char('?'), None, "show/hide keybindings")]
#[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), "events")] #[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), None, "filter")]
#[case(DEFAULT_KEYBINDINGS.logs, Key::Char('l'), "logs")] #[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), None, "sort")]
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), "tasks")] #[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), None, "edit")]
#[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), "test")] #[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), None, "events")]
#[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('T'), "test all")] #[case(DEFAULT_KEYBINDINGS.logs, Key::Char('L'), None, "logs")]
#[case(DEFAULT_KEYBINDINGS.toggle_monitoring, Key::Char('m'), "toggle monitoring")] #[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), None, "tasks")]
#[case(DEFAULT_KEYBINDINGS.refresh, Key::Ctrl('r'), "refresh")] #[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), None, "test")]
#[case(DEFAULT_KEYBINDINGS.update, Key::Char('u'), "update")] #[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('T'), None, "test all")]
#[case(DEFAULT_KEYBINDINGS.home, Key::Home, "home")] #[case(DEFAULT_KEYBINDINGS.toggle_monitoring, Key::Char('m'), None, "toggle monitoring")]
#[case(DEFAULT_KEYBINDINGS.end, Key::End, "end")] #[case(DEFAULT_KEYBINDINGS.refresh, Key::Ctrl('r'), None, "refresh")]
#[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, "delete")] #[case(DEFAULT_KEYBINDINGS.update, Key::Char('u'), None, "update")]
#[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, "submit")] #[case(DEFAULT_KEYBINDINGS.home, Key::Home, None, "home")]
#[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), "submit")] #[case(DEFAULT_KEYBINDINGS.end, Key::End, None, "end")]
#[case(DEFAULT_KEYBINDINGS.quit, Key::Char('q'), "quit")] #[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, None, "delete")]
#[case(DEFAULT_KEYBINDINGS.esc, Key::Esc, "close")] #[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, None, "submit")]
#[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), None, "submit")]
#[case(DEFAULT_KEYBINDINGS.quit, Key::Char('q'), None, "quit")]
#[case(DEFAULT_KEYBINDINGS.esc, Key::Esc, None, "close")]
fn test_default_key_bindings_and_descriptions( fn test_default_key_bindings_and_descriptions(
#[case] key_binding: KeyBinding, #[case] key_binding: KeyBinding,
#[case] expected_key: Key, #[case] expected_key: Key,
#[case] expected_alt_key: Option<Key>,
#[case] expected_desc: &str, #[case] expected_desc: &str,
) { ) {
assert_eq!(key_binding.key, expected_key); assert_eq!(key_binding.key, expected_key);
assert_eq!(key_binding.alt, expected_alt_key);
assert_str_eq!(key_binding.desc, expected_desc); assert_str_eq!(key_binding.desc, expected_desc);
} }
#[test]
fn test_matches_key_macro() {
let key = Key::Char('t');
assert!(matches_key!(test, key));
assert!(!matches_key!(test, Key::Char('T')));
}
#[test]
fn test_matches_key_macro_with_alt_keybinding() {
let alt_key = Key::Char('k');
let key = Key::Up;
assert!(matches_key!(up, key));
assert!(matches_key!(up, alt_key));
assert!(!matches_key!(up, Key::Char('t')));
}
#[test]
fn test_matches_key_macro_with_alt_keybinding_uses_alt_key_when_ignore_special_keys_is_false() {
let alt_key = Key::Char('k');
let key = Key::Up;
assert!(matches_key!(up, key, false));
assert!(matches_key!(up, alt_key, false));
assert!(!matches_key!(up, Key::Char('t'), false));
}
#[test]
fn test_matches_key_macro_with_alt_keybinding_ignores_alt_key_when_ignore_special_keys_is_true() {
let alt_key = Key::Char('k');
let key = Key::Up;
assert!(matches_key!(up, key, true));
assert!(!matches_key!(up, alt_key, true));
assert!(!matches_key!(up, Key::Char('t'), true));
}
} }
+64 -24
View File
@@ -3,17 +3,20 @@ use colored::Colorize;
use itertools::Itertools; use itertools::Itertools;
use log::{debug, error}; use log::{debug, error};
use regex::Regex; use regex::Regex;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::{fs, process}; use std::{fs, process};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use veil::Redact; use veil::Redact;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::cli::Command; use crate::cli::Command;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState}; use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState};
use crate::network::NetworkEvent; use crate::network::NetworkEvent;
@@ -32,6 +35,7 @@ pub struct App<'a> {
pub cancellation_token: CancellationToken, pub cancellation_token: CancellationToken,
pub is_first_render: bool, pub is_first_render: bool,
pub server_tabs: TabState, pub server_tabs: TabState,
pub keymapping_table: Option<StatefulTable<KeybindingItem>>,
pub error: HorizontallyScrollableText, pub error: HorizontallyScrollableText,
pub tick_until_poll: u64, pub tick_until_poll: u64,
pub ticks_until_scroll: u64, pub ticks_until_scroll: u64,
@@ -39,7 +43,7 @@ pub struct App<'a> {
pub is_routing: bool, pub is_routing: bool,
pub is_loading: bool, pub is_loading: bool,
pub should_refresh: bool, pub should_refresh: bool,
pub should_ignore_quit_key: bool, pub ignore_special_keys_for_textbox_input: bool,
pub cli_mode: bool, pub cli_mode: bool,
pub data: Data<'a>, pub data: Data<'a>,
} }
@@ -51,10 +55,6 @@ impl App<'_> {
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) -> Self { ) -> Self {
let mut server_tabs = Vec::new(); let mut server_tabs = Vec::new();
let help = format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
);
if let Some(radarr_configs) = config.radarr { if let Some(radarr_configs) = config.radarr {
let mut idx = 0; let mut idx = 0;
@@ -63,13 +63,12 @@ impl App<'_> {
name name
} else { } else {
idx += 1; idx += 1;
format!("Radarr {}", idx) format!("Radarr {idx}")
}; };
server_tabs.push(TabRoute { server_tabs.push(TabRoute {
title: name, title: name,
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: help.clone(),
contextual_help: None, contextual_help: None,
config: Some(radarr_config), config: Some(radarr_config),
}); });
@@ -84,13 +83,12 @@ impl App<'_> {
name name
} else { } else {
idx += 1; idx += 1;
format!("Sonarr {}", idx) format!("Sonarr {idx}")
}; };
server_tabs.push(TabRoute { server_tabs.push(TabRoute {
title: name, title: name,
route: ActiveSonarrBlock::Series.into(), route: ActiveSonarrBlock::Series.into(),
help: help.clone(),
contextual_help: None, contextual_help: None,
config: Some(sonarr_config), config: Some(sonarr_config),
}); });
@@ -215,6 +213,7 @@ impl Default for App<'_> {
navigation_stack: Vec::new(), navigation_stack: Vec::new(),
network_tx: None, network_tx: None,
cancellation_token: CancellationToken::new(), cancellation_token: CancellationToken::new(),
keymapping_table: None,
error: HorizontallyScrollableText::default(), error: HorizontallyScrollableText::default(),
is_first_render: true, is_first_render: true,
server_tabs: TabState::new(Vec::new()), server_tabs: TabState::new(Vec::new()),
@@ -224,7 +223,7 @@ impl Default for App<'_> {
is_loading: false, is_loading: false,
is_routing: false, is_routing: false,
should_refresh: false, should_refresh: false,
should_ignore_quit_key: false, ignore_special_keys_for_textbox_input: false,
cli_mode: false, cli_mode: false,
data: Data::default(), data: Data::default(),
} }
@@ -239,20 +238,12 @@ impl App<'_> {
TabRoute { TabRoute {
title: "Radarr".to_owned(), title: "Radarr".to_owned(),
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(ServarrConfig::default()), config: Some(ServarrConfig::default()),
}, },
TabRoute { TabRoute {
title: "Sonarr".to_owned(), title: "Sonarr".to_owned(),
route: ActiveSonarrBlock::Series.into(), route: ActiveSonarrBlock::Series.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(ServarrConfig::default()), config: Some(ServarrConfig::default()),
}, },
@@ -270,6 +261,7 @@ pub struct Data<'a> {
#[derive(Debug, Deserialize, Serialize, Default, Clone)] #[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct AppConfig { pub struct AppConfig {
pub theme: Option<String>,
pub radarr: Option<Vec<ServarrConfig>>, pub radarr: Option<Vec<ServarrConfig>>,
pub sonarr: Option<Vec<ServarrConfig>>, pub sonarr: Option<Vec<ServarrConfig>>,
} }
@@ -295,8 +287,7 @@ impl AppConfig {
pub fn verify_config_present_for_cli(&self, command: &Command) { pub fn verify_config_present_for_cli(&self, command: &Command) {
let msg = |servarr: &str| { let msg = |servarr: &str| {
log_and_print_error(format!( log_and_print_error(format!(
"{} configuration missing; Unable to run any {} commands.", "{servarr} configuration missing; Unable to run any {servarr} commands."
servarr, servarr
)) ))
}; };
match command { match command {
@@ -346,6 +337,12 @@ pub struct ServarrConfig {
pub api_token_file: Option<String>, pub api_token_file: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")] #[serde(default, deserialize_with = "deserialize_optional_env_var")]
pub ssl_cert_path: Option<String>, pub ssl_cert_path: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_optional_env_var_header_map",
serialize_with = "serialize_header_map"
)]
pub custom_headers: Option<HeaderMap>,
} }
impl ServarrConfig { impl ServarrConfig {
@@ -367,8 +364,7 @@ impl ServarrConfig {
if let Some(api_token_file) = self.api_token_file.as_ref() { if let Some(api_token_file) = self.api_token_file.as_ref() {
if !PathBuf::from(api_token_file).exists() { if !PathBuf::from(api_token_file).exists() {
log_and_print_error(format!( log_and_print_error(format!(
"The specified {} API token file does not exist", "The specified {api_token_file} API token file does not exist"
api_token_file
)); ));
process::exit(1); process::exit(1);
} }
@@ -392,15 +388,37 @@ impl Default for ServarrConfig {
api_token: Some(String::new()), api_token: Some(String::new()),
api_token_file: None, api_token_file: None,
ssl_cert_path: None, ssl_cert_path: None,
custom_headers: None,
} }
} }
} }
pub fn log_and_print_error(error: String) { pub fn log_and_print_error(error: String) {
error!("{}", error); error!("{error}");
eprintln!("error: {}", error.red()); eprintln!("error: {}", error.red());
} }
fn serialize_header_map<S>(headers: &Option<HeaderMap>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if let Some(headers) = headers {
let mut map = HashMap::new();
for (name, value) in headers.iter() {
let name_str = name.as_str().to_string();
let value_str = value
.to_str()
.map_err(serde::ser::Error::custom)?
.to_string();
map.insert(name_str, value_str);
}
map.serialize(serializer)
} else {
serializer.serialize_none()
}
}
fn deserialize_optional_env_var<'de, D>(deserializer: D) -> Result<Option<String>, D::Error> fn deserialize_optional_env_var<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
@@ -415,6 +433,28 @@ where
} }
} }
fn deserialize_optional_env_var_header_map<'de, D>(
deserializer: D,
) -> Result<Option<HeaderMap>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<HashMap<String, String>> = Option::deserialize(deserializer)?;
match opt {
Some(map) => {
let mut header_map = HeaderMap::new();
for (k, v) in map.iter() {
let name = HeaderName::from_bytes(k.as_bytes()).map_err(serde::de::Error::custom)?;
let value_str = interpolate_env_vars(v);
let value = HeaderValue::from_str(&value_str).map_err(serde::de::Error::custom)?;
header_map.insert(name, value);
}
Ok(Some(header_map))
}
None => Ok(None),
}
}
fn deserialize_u16_env_var<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error> fn deserialize_u16_env_var<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
+3 -3
View File
@@ -40,7 +40,7 @@ impl App<'_> {
} }
ActiveRadarrBlock::Downloads => { ActiveRadarrBlock::Downloads => {
self self
.dispatch_network_event(RadarrEvent::GetDownloads.into()) .dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await; .await;
} }
ActiveRadarrBlock::RootFolders => { ActiveRadarrBlock::RootFolders => {
@@ -59,7 +59,7 @@ impl App<'_> {
.dispatch_network_event(RadarrEvent::GetMovies.into()) .dispatch_network_event(RadarrEvent::GetMovies.into())
.await; .await;
self self
.dispatch_network_event(RadarrEvent::GetDownloads.into()) .dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await; .await;
} }
ActiveRadarrBlock::Indexers => { ActiveRadarrBlock::Indexers => {
@@ -201,7 +201,7 @@ impl App<'_> {
.dispatch_network_event(RadarrEvent::GetRootFolders.into()) .dispatch_network_event(RadarrEvent::GetRootFolders.into())
.await; .await;
self self
.dispatch_network_event(RadarrEvent::GetDownloads.into()) .dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await; .await;
self self
.dispatch_network_event(RadarrEvent::GetDiskSpace.into()) .dispatch_network_event(RadarrEvent::GetDiskSpace.into())
+67 -6
View File
@@ -1,13 +1,25 @@
use crate::app::context_clues::ContextClue; use crate::app::context_clues::{
ContextClue, ContextClueProvider, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, ADD_MOVIE_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_INDEXER_BLOCKS,
EDIT_MOVIE_BLOCKS, INDEXER_SETTINGS_BLOCKS, MOVIE_DETAILS_BLOCKS,
};
use crate::models::Route;
#[cfg(test)] #[cfg(test)]
#[path = "radarr_context_clues_tests.rs"] #[path = "radarr_context_clues_tests.rs"]
mod radarr_context_clues_tests; mod radarr_context_clues_tests;
pub static LIBRARY_CONTEXT_CLUES: [ContextClue; 10] = [ pub static LIBRARY_CONTEXT_CLUES: [ContextClue; 11] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
@@ -49,7 +61,7 @@ pub static MOVIE_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 6] = [ pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
@@ -61,12 +73,10 @@ pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 6] = [
DEFAULT_KEYBINDINGS.auto_search, DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
pub static MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 1] =
[(DEFAULT_KEYBINDINGS.submit, "details")];
pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "details"), (DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "edit search"), (DEFAULT_KEYBINDINGS.esc, "edit search"),
@@ -82,3 +92,54 @@ pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.edit, "edit collection"), (DEFAULT_KEYBINDINGS.edit, "edit collection"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
pub(in crate::app) struct RadarrContextClueProvider;
impl ContextClueProvider for RadarrContextClueProvider {
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
let Route::Radarr(active_radarr_block, context_option) = app.get_current_route() else {
panic!("RadarrContextClueProvider::get_context_clues called with non-Radarr route");
};
match active_radarr_block {
_ if MOVIE_DETAILS_BLOCKS.contains(&active_radarr_block) => app
.data
.radarr_data
.movie_info_tabs
.get_active_route_contextual_help(),
ActiveRadarrBlock::TestAllIndexers
| ActiveRadarrBlock::AddMovieSearchInput
| ActiveRadarrBlock::AddMovieEmptySearchResults
| ActiveRadarrBlock::SystemLogs
| ActiveRadarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES),
_ if context_option.unwrap_or(active_radarr_block)
== ActiveRadarrBlock::ViewMovieOverview =>
{
Some(&BARE_POPUP_CONTEXT_CLUES)
}
ActiveRadarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES),
_ if EDIT_COLLECTION_BLOCKS.contains(&active_radarr_block)
|| EDIT_INDEXER_BLOCKS.contains(&active_radarr_block)
|| INDEXER_SETTINGS_BLOCKS.contains(&active_radarr_block)
|| EDIT_MOVIE_BLOCKS.contains(&active_radarr_block) =>
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveRadarrBlock::AddMoviePrompt
| ActiveRadarrBlock::AddMovieSelectMonitor
| ActiveRadarrBlock::AddMovieSelectMinimumAvailability
| ActiveRadarrBlock::AddMovieSelectQualityProfile
| ActiveRadarrBlock::AddMovieSelectRootFolder
| ActiveRadarrBlock::AddMovieTagsInput
| ActiveRadarrBlock::SystemTaskStartConfirmPrompt => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
_ if ADD_MOVIE_BLOCKS.contains(&active_radarr_block) => {
Some(&ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES)
}
ActiveRadarrBlock::CollectionDetails => Some(&COLLECTION_DETAILS_CONTEXT_CLUES),
_ => app
.data
.radarr_data
.main_tabs
.get_active_route_contextual_help(),
}
}
}
+275 -23
View File
@@ -1,14 +1,21 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::{assert_eq, assert_str_eq}; use crate::app::context_clues::{
ContextClue, ContextClueProvider, BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES,
CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{ use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, RadarrContextClueProvider, ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MOVIE_DETAILS_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
}; };
use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
#[test] #[test]
fn test_library_context_clues() { fn test_library_context_clues() {
@@ -26,6 +33,11 @@ mod tests {
let (key_binding, description) = library_context_clues_iter.next().unwrap(); let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.toggle_monitoring);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.toggle_monitoring.desc);
let (key_binding, description) = library_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
@@ -174,26 +186,15 @@ mod tests {
let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap(); let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(manual_movie_search_context_clues_iter.next(), None);
}
#[test]
fn test_manual_movie_search_contextual_context_clues() {
let mut manual_movie_search_contextual_context_clues_iter =
MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES.iter();
let (key_binding, description) = manual_movie_search_contextual_context_clues_iter
.next()
.unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details"); assert_str_eq!(*description, "details");
assert_eq!(
manual_movie_search_contextual_context_clues_iter.next(), let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
None
); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(manual_movie_search_context_clues_iter.next(), None);
} }
#[test] #[test]
@@ -249,4 +250,255 @@ mod tests {
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(collection_details_context_clues_iter.next(), None); assert_eq!(collection_details_context_clues_iter.next(), None);
} }
#[test]
#[should_panic(
expected = "RadarrContextClueProvider::get_context_clues called with non-Radarr route"
)]
fn test_radarr_context_clue_provider_get_context_clues_non_radarr_route() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::default().into());
// This should panic because the route is not a Radarr route
RadarrContextClueProvider::get_context_clues(&mut app);
}
#[rstest]
#[case(ActiveRadarrBlock::TestAllIndexers, None)]
#[case(ActiveRadarrBlock::AddMovieSearchInput, None)]
#[case(ActiveRadarrBlock::AddMovieEmptySearchResults, None)]
#[case(ActiveRadarrBlock::SystemLogs, None)]
#[case(ActiveRadarrBlock::SystemUpdates, None)]
#[case(ActiveRadarrBlock::ViewMovieOverview, None)]
#[case(
ActiveRadarrBlock::CollectionDetails,
Some(ActiveRadarrBlock::ViewMovieOverview)
)]
fn test_radarr_context_clue_provider_bare_popup_context_clues(
#[case] active_radarr_block: ActiveRadarrBlock,
#[case] context_option: Option<ActiveRadarrBlock>,
) {
let mut app = App::test_default();
app.push_navigation_stack((active_radarr_block, context_option).into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(&BARE_POPUP_CONTEXT_CLUES, context_clues.unwrap());
}
#[rstest]
#[case(0, ActiveRadarrBlock::MovieDetails, &MOVIE_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveRadarrBlock::MovieHistory, &MOVIE_DETAILS_CONTEXT_CLUES)]
#[case(2, ActiveRadarrBlock::FileInfo, &MOVIE_DETAILS_CONTEXT_CLUES)]
#[case(3, ActiveRadarrBlock::Cast, &MOVIE_DETAILS_CONTEXT_CLUES)]
#[case(4, ActiveRadarrBlock::Crew, &MOVIE_DETAILS_CONTEXT_CLUES)]
#[case(5, ActiveRadarrBlock::ManualSearch, &MANUAL_MOVIE_SEARCH_CONTEXT_CLUES)]
fn test_radarr_context_clue_provider_movie_details_block_context_clues(
#[case] index: usize,
#[case] active_radarr_block: ActiveRadarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.data.radarr_data.movie_info_tabs.set_index(index);
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(expected_context_clues, context_clues.unwrap());
}
#[rstest]
fn test_radarr_context_clue_provider_confirmation_prompt_context_clues(
#[values(
ActiveRadarrBlock::AddMoviePrompt,
ActiveRadarrBlock::AddMovieSelectMonitor,
ActiveRadarrBlock::AddMovieSelectMinimumAvailability,
ActiveRadarrBlock::AddMovieSelectQualityProfile,
ActiveRadarrBlock::AddMovieSelectRootFolder,
ActiveRadarrBlock::AddMovieTagsInput,
ActiveRadarrBlock::SystemTaskStartConfirmPrompt
)]
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(&CONFIRMATION_PROMPT_CONTEXT_CLUES, context_clues.unwrap());
}
#[rstest]
fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_edit_collection_blocks(
#[values(
ActiveRadarrBlock::EditCollectionPrompt,
ActiveRadarrBlock::EditCollectionConfirmPrompt,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
ActiveRadarrBlock::EditCollectionSelectMinimumAvailability,
ActiveRadarrBlock::EditCollectionSelectQualityProfile,
ActiveRadarrBlock::EditCollectionToggleSearchOnAdd,
ActiveRadarrBlock::EditCollectionToggleMonitored
)]
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(&CONFIRMATION_PROMPT_CONTEXT_CLUES, context_clues.unwrap());
}
#[rstest]
fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_edit_indexer_blocks(
#[values(
ActiveRadarrBlock::EditIndexerPrompt,
ActiveRadarrBlock::EditIndexerConfirmPrompt,
ActiveRadarrBlock::EditIndexerApiKeyInput,
ActiveRadarrBlock::EditIndexerNameInput,
ActiveRadarrBlock::EditIndexerSeedRatioInput,
ActiveRadarrBlock::EditIndexerToggleEnableRss,
ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveRadarrBlock::EditIndexerPriorityInput,
ActiveRadarrBlock::EditIndexerUrlInput,
ActiveRadarrBlock::EditIndexerTagsInput
)]
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(&CONFIRMATION_PROMPT_CONTEXT_CLUES, context_clues.unwrap());
}
#[rstest]
fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_indexer_settings_blocks(
#[values(
ActiveRadarrBlock::AllIndexerSettingsPrompt,
ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
ActiveRadarrBlock::IndexerSettingsConfirmPrompt,
ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput,
ActiveRadarrBlock::IndexerSettingsRetentionInput,
ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput,
ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs,
ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput
)]
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(&CONFIRMATION_PROMPT_CONTEXT_CLUES, context_clues.unwrap());
}
#[rstest]
fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_edit_movie_blocks(
#[values(
ActiveRadarrBlock::EditMoviePrompt,
ActiveRadarrBlock::EditMovieConfirmPrompt,
ActiveRadarrBlock::EditMoviePathInput,
ActiveRadarrBlock::EditMovieSelectMinimumAvailability,
ActiveRadarrBlock::EditMovieSelectQualityProfile,
ActiveRadarrBlock::EditMovieTagsInput,
ActiveRadarrBlock::EditMovieToggleMonitored
)]
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(&CONFIRMATION_PROMPT_CONTEXT_CLUES, context_clues.unwrap());
}
#[rstest]
fn test_radarr_context_clue_provider_add_movie_search_results_context_clues(
#[values(
ActiveRadarrBlock::AddMovieSearchResults,
ActiveRadarrBlock::AddMovieAlreadyInLibrary
)]
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(
&ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES,
context_clues.unwrap()
);
}
#[test]
fn test_radarr_context_clue_provider_collection_details_context_clues() {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(&COLLECTION_DETAILS_CONTEXT_CLUES, context_clues.unwrap());
}
#[test]
fn test_radarr_context_clue_provider_system_tasks_context_clues() {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(&SYSTEM_TASKS_CONTEXT_CLUES, context_clues.unwrap());
}
#[rstest]
#[case(0, ActiveRadarrBlock::Movies, &LIBRARY_CONTEXT_CLUES)]
#[case(1, ActiveRadarrBlock::Collections, &COLLECTIONS_CONTEXT_CLUES)]
#[case(2, ActiveRadarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)]
#[case(3, ActiveRadarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)]
#[case(4, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)]
#[case(5, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)]
#[case(6, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)]
fn test_radarr_context_clue_provider_radarr_blocks_context_clues(
#[case] index: usize,
#[case] active_radarr_block: ActiveRadarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.data.radarr_data.main_tabs.set_index(index);
app.push_navigation_stack(active_radarr_block.into());
let context_clues = RadarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(expected_context_clues, context_clues.unwrap());
}
} }
+8 -8
View File
@@ -140,7 +140,7 @@ mod tests {
assert!(app.is_loading); assert!(app.is_loading);
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads(500).into()
); );
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
@@ -186,7 +186,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads(500).into()
); );
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
@@ -591,7 +591,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads(500).into()
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
@@ -625,7 +625,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads(500).into()
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
@@ -650,7 +650,7 @@ mod tests {
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads(500).into()
); );
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
} }
@@ -675,7 +675,7 @@ mod tests {
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads(500).into()
); );
assert!(app.should_refresh); assert!(app.should_refresh);
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
@@ -692,7 +692,7 @@ mod tests {
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads(500).into()
); );
assert!(app.is_loading); assert!(app.is_loading);
assert!(app.should_refresh); assert!(app.should_refresh);
@@ -722,7 +722,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into() RadarrEvent::GetDownloads(500).into()
); );
assert!(app.is_loading); assert!(app.is_loading);
} }
+3 -3
View File
@@ -53,7 +53,7 @@ impl App<'_> {
) )
.await; .await;
self self
.dispatch_network_event(SonarrEvent::GetDownloads.into()) .dispatch_network_event(SonarrEvent::GetDownloads(500).into())
.await; .await;
} }
ActiveSonarrBlock::SeasonHistory => { ActiveSonarrBlock::SeasonHistory => {
@@ -108,7 +108,7 @@ impl App<'_> {
} }
ActiveSonarrBlock::Downloads => { ActiveSonarrBlock::Downloads => {
self self
.dispatch_network_event(SonarrEvent::GetDownloads.into()) .dispatch_network_event(SonarrEvent::GetDownloads(500).into())
.await; .await;
} }
ActiveSonarrBlock::Blocklist => { ActiveSonarrBlock::Blocklist => {
@@ -234,7 +234,7 @@ impl App<'_> {
.dispatch_network_event(SonarrEvent::GetRootFolders.into()) .dispatch_network_event(SonarrEvent::GetRootFolders.into())
.await; .await;
self self
.dispatch_network_event(SonarrEvent::GetDownloads.into()) .dispatch_network_event(SonarrEvent::GetDownloads(500).into())
.await; .await;
self self
.dispatch_network_event(SonarrEvent::GetDiskSpace.into()) .dispatch_network_event(SonarrEvent::GetDiskSpace.into())
+100 -14
View File
@@ -1,4 +1,12 @@
use crate::app::{context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS}; use crate::app::context_clues::{
ContextClueProvider, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
};
use crate::app::{context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS, App};
use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, ADD_SERIES_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_SERIES_BLOCKS,
EPISODE_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS,
};
use crate::models::Route;
#[cfg(test)] #[cfg(test)]
#[path = "sonarr_context_clues_tests.rs"] #[path = "sonarr_context_clues_tests.rs"]
@@ -9,9 +17,13 @@ pub static ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.esc, "edit search"), (DEFAULT_KEYBINDINGS.esc, "edit search"),
]; ];
pub static SERIES_CONTEXT_CLUES: [ContextClue; 10] = [ pub static SERIES_CONTEXT_CLUES: [ContextClue; 11] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
@@ -75,12 +87,7 @@ pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
]; ];
pub static SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 2] = [ pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 7] = [
(DEFAULT_KEYBINDINGS.submit, "episode details"),
(DEFAULT_KEYBINDINGS.delete, "delete episode"),
];
pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
@@ -95,9 +102,11 @@ pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
(DEFAULT_KEYBINDINGS.submit, "episode details"),
(DEFAULT_KEYBINDINGS.delete, "delete episode"),
]; ];
pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [ pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
@@ -109,10 +118,11 @@ pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
DEFAULT_KEYBINDINGS.auto_search, DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
]; ];
pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [ pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
@@ -122,10 +132,11 @@ pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [ pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
@@ -135,12 +146,10 @@ pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
pub static DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 1] =
[(DEFAULT_KEYBINDINGS.submit, "details")];
pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [ pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
@@ -153,7 +162,84 @@ pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
pub static SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 4] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
];
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"), (DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
]; ];
pub(in crate::app) struct SonarrContextClueProvider;
impl ContextClueProvider for SonarrContextClueProvider {
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() else {
panic!("SonarrContextClueProvider::get_context_clues called with non-Sonarr route");
};
match active_sonarr_block {
_ if SERIES_DETAILS_BLOCKS.contains(&active_sonarr_block) => app
.data
.sonarr_data
.series_info_tabs
.get_active_route_contextual_help(),
_ if SEASON_DETAILS_BLOCKS.contains(&active_sonarr_block) => app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_details_tabs
.get_active_route_contextual_help(),
_ if EPISODE_DETAILS_BLOCKS.contains(&active_sonarr_block) => app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.episode_details_modal
.as_ref()
.unwrap()
.episode_details_tabs
.get_active_route_contextual_help(),
ActiveSonarrBlock::TestAllIndexers
| ActiveSonarrBlock::AddSeriesSearchInput
| ActiveSonarrBlock::AddSeriesEmptySearchResults
| ActiveSonarrBlock::SystemLogs
| ActiveSonarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES),
_ if EDIT_INDEXER_BLOCKS.contains(&active_sonarr_block)
|| INDEXER_SETTINGS_BLOCKS.contains(&active_sonarr_block)
|| EDIT_SERIES_BLOCKS.contains(&active_sonarr_block) =>
{
Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES)
}
ActiveSonarrBlock::AddSeriesPrompt
| ActiveSonarrBlock::AddSeriesSelectMonitor
| ActiveSonarrBlock::AddSeriesSelectSeriesType
| ActiveSonarrBlock::AddSeriesSelectQualityProfile
| ActiveSonarrBlock::AddSeriesSelectLanguageProfile
| ActiveSonarrBlock::AddSeriesSelectRootFolder
| ActiveSonarrBlock::AddSeriesTagsInput
| ActiveSonarrBlock::SystemTaskStartConfirmPrompt => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES),
_ if ADD_SERIES_BLOCKS.contains(&active_sonarr_block) => {
Some(&ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES)
}
ActiveSonarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES),
_ => app
.data
.sonarr_data
.main_tabs
.get_active_route_contextual_help(),
}
}
}
+323 -26
View File
@@ -1,17 +1,29 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::{assert_eq, assert_str_eq}; use crate::app::context_clues::{
ContextClue, ContextClueProvider, BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES,
CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
};
use crate::app::sonarr::sonarr_context_clues::{
SonarrContextClueProvider, SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES,
};
use crate::app::{ use crate::app::{
key_binding::DEFAULT_KEYBINDINGS, key_binding::DEFAULT_KEYBINDINGS,
sonarr::sonarr_context_clues::{ sonarr::sonarr_context_clues::{
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, DETAILS_CONTEXTUAL_CONTEXT_CLUES, ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES,
EPISODE_DETAILS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES,
MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES,
SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES,
SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
}, },
App,
}; };
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_data::sonarr::modals::{EpisodeDetailsModal, SeasonDetailsModal};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
#[test] #[test]
fn test_add_series_search_results_context_clues() { fn test_add_series_search_results_context_clues() {
@@ -46,6 +58,11 @@ mod tests {
let (key_binding, description) = series_context_clues_iter.next().unwrap(); let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.toggle_monitoring);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.toggle_monitoring.desc);
let (key_binding, description) = series_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
@@ -247,23 +264,18 @@ mod tests {
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(season_details_context_clues_iter.next(), None);
}
#[test] let (key_binding, description) = season_details_context_clues_iter.next().unwrap();
fn test_season_details_contextual_context_clues() {
let mut season_details_contextual_context_clues_iter =
SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES.iter();
let (key_binding, description) = season_details_contextual_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "episode details"); assert_str_eq!(*description, "episode details");
let (key_binding, description) = season_details_contextual_context_clues_iter.next().unwrap(); let (key_binding, description) = season_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, "delete episode"); assert_str_eq!(*description, "delete episode");
assert_eq!(season_details_contextual_context_clues_iter.next(), None);
assert_eq!(season_details_context_clues_iter.next(), None);
} }
#[test] #[test]
@@ -296,6 +308,11 @@ mod tests {
let (key_binding, description) = season_history_context_clues_iter.next().unwrap(); let (key_binding, description) = season_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = season_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel filter/close"); assert_str_eq!(*description, "cancel filter/close");
assert_eq!(season_history_context_clues_iter.next(), None); assert_eq!(season_history_context_clues_iter.next(), None);
@@ -322,6 +339,11 @@ mod tests {
let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap(); let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(manual_season_search_context_clues_iter.next(), None); assert_eq!(manual_season_search_context_clues_iter.next(), None);
@@ -348,21 +370,16 @@ mod tests {
let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap(); let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(manual_episode_search_context_clues_iter.next(), None); assert_eq!(manual_episode_search_context_clues_iter.next(), None);
} }
#[test]
fn details_contextual_context_clues() {
let mut manual_search_contextual_context_clues_iter = DETAILS_CONTEXTUAL_CONTEXT_CLUES.iter();
let (key_binding, description) = manual_search_contextual_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
assert_eq!(manual_search_contextual_context_clues_iter.next(), None);
}
#[test] #[test]
fn test_episode_details_context_clues() { fn test_episode_details_context_clues() {
let mut episode_details_context_clues_iter = EPISODE_DETAILS_CONTEXT_CLUES.iter(); let mut episode_details_context_clues_iter = EPISODE_DETAILS_CONTEXT_CLUES.iter();
@@ -384,6 +401,32 @@ mod tests {
assert_eq!(episode_details_context_clues_iter.next(), None); assert_eq!(episode_details_context_clues_iter.next(), None);
} }
#[test]
fn test_selectable_episode_details_context_clues() {
let mut episode_details_context_clues_iter = SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES.iter();
let (key_binding, description) = episode_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = episode_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = episode_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = episode_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(episode_details_context_clues_iter.next(), None);
}
#[test] #[test]
fn test_system_tasks_context_clues() { fn test_system_tasks_context_clues() {
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter(); let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
@@ -399,4 +442,258 @@ mod tests {
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(system_tasks_context_clues_iter.next(), None); assert_eq!(system_tasks_context_clues_iter.next(), None);
} }
#[test]
#[should_panic(
expected = "SonarrContextClueProvider::get_context_clues called with non-Sonarr route"
)]
fn test_sonarr_context_clue_provider_get_context_clues_non_sonarr_route() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::default().into());
// This should panic because the route is not a Sonarr route
SonarrContextClueProvider::get_context_clues(&mut app);
}
#[rstest]
#[case(0, ActiveSonarrBlock::SeriesDetails, &SERIES_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveSonarrBlock::SeriesHistory, &SERIES_HISTORY_CONTEXT_CLUES)]
fn test_sonarr_context_clue_provider_series_info_tabs(
#[case] index: usize,
#[case] active_sonarr_block: ActiveSonarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
app.data.sonarr_data = SonarrData::default();
app.data.sonarr_data.series_info_tabs.set_index(index);
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(expected_context_clues, context_clues.unwrap());
}
#[rstest]
#[case(0, ActiveSonarrBlock::SeasonDetails, &SEASON_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveSonarrBlock::SeasonHistory, &SEASON_HISTORY_CONTEXT_CLUES)]
#[case(2, ActiveSonarrBlock::ManualSeasonSearch, &MANUAL_SEASON_SEARCH_CONTEXT_CLUES)]
fn test_sonarr_context_clue_provider_season_details_tabs(
#[case] index: usize,
#[case] active_sonarr_block: ActiveSonarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
let mut season_details_modal = SeasonDetailsModal::default();
season_details_modal.season_details_tabs.set_index(index);
let sonarr_data = SonarrData {
season_details_modal: Some(season_details_modal),
..SonarrData::default()
};
app.data.sonarr_data = sonarr_data;
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(expected_context_clues, context_clues.unwrap());
}
#[rstest]
#[case(0, ActiveSonarrBlock::EpisodeDetails, &EPISODE_DETAILS_CONTEXT_CLUES)]
#[case(1, ActiveSonarrBlock::EpisodeHistory, &SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES)]
#[case(2, ActiveSonarrBlock::EpisodeFile, &EPISODE_DETAILS_CONTEXT_CLUES)]
#[case(3, ActiveSonarrBlock::ManualEpisodeSearch, &MANUAL_EPISODE_SEARCH_CONTEXT_CLUES)]
fn test_sonarr_context_clue_provider_episode_details_tabs(
#[case] index: usize,
#[case] active_sonarr_block: ActiveSonarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
let mut episode_details_modal = EpisodeDetailsModal::default();
episode_details_modal.episode_details_tabs.set_index(index);
let sonarr_data = SonarrData {
season_details_modal: Some(SeasonDetailsModal {
episode_details_modal: Some(episode_details_modal),
..SeasonDetailsModal::default()
}),
..SonarrData::default()
};
app.data.sonarr_data = sonarr_data;
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(expected_context_clues, context_clues.unwrap());
}
#[rstest]
fn test_sonarr_context_clue_provider_bare_popup_context_clues(
#[values(
ActiveSonarrBlock::TestAllIndexers,
ActiveSonarrBlock::AddSeriesSearchInput,
ActiveSonarrBlock::AddSeriesEmptySearchResults,
ActiveSonarrBlock::SystemLogs,
ActiveSonarrBlock::SystemUpdates
)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(context_clues.unwrap(), &BARE_POPUP_CONTEXT_CLUES);
}
#[rstest]
fn test_sonarr_context_clue_provider_confirmation_prompt_context_clues(
#[values(
ActiveSonarrBlock::AddSeriesPrompt,
ActiveSonarrBlock::AddSeriesSelectMonitor,
ActiveSonarrBlock::AddSeriesSelectSeriesType,
ActiveSonarrBlock::AddSeriesSelectQualityProfile,
ActiveSonarrBlock::AddSeriesSelectLanguageProfile,
ActiveSonarrBlock::AddSeriesSelectRootFolder,
ActiveSonarrBlock::AddSeriesTagsInput,
ActiveSonarrBlock::SystemTaskStartConfirmPrompt
)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(context_clues.unwrap(), &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_sonarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks(
#[values(
ActiveSonarrBlock::EditIndexerPrompt,
ActiveSonarrBlock::EditIndexerConfirmPrompt,
ActiveSonarrBlock::EditIndexerApiKeyInput,
ActiveSonarrBlock::EditIndexerNameInput,
ActiveSonarrBlock::EditIndexerSeedRatioInput,
ActiveSonarrBlock::EditIndexerToggleEnableRss,
ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveSonarrBlock::EditIndexerPriorityInput,
ActiveSonarrBlock::EditIndexerUrlInput,
ActiveSonarrBlock::EditIndexerTagsInput
)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(context_clues.unwrap(), &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_sonarr_context_clue_provider_confirmation_prompt_popup_clues_indexer_settings_blocks(
#[values(
ActiveSonarrBlock::AllIndexerSettingsPrompt,
ActiveSonarrBlock::IndexerSettingsConfirmPrompt,
ActiveSonarrBlock::IndexerSettingsMaximumSizeInput,
ActiveSonarrBlock::IndexerSettingsMinimumAgeInput,
ActiveSonarrBlock::IndexerSettingsRetentionInput,
ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput
)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(context_clues.unwrap(), &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_sonarr_context_clue_provider_confirmation_prompt_popup_clues_edit_series_blocks(
#[values(
ActiveSonarrBlock::EditSeriesPrompt,
ActiveSonarrBlock::EditSeriesConfirmPrompt,
ActiveSonarrBlock::EditSeriesPathInput,
ActiveSonarrBlock::EditSeriesSelectSeriesType,
ActiveSonarrBlock::EditSeriesSelectQualityProfile,
ActiveSonarrBlock::EditSeriesSelectLanguageProfile,
ActiveSonarrBlock::EditSeriesTagsInput,
ActiveSonarrBlock::EditSeriesToggleMonitored,
ActiveSonarrBlock::EditSeriesToggleSeasonFolder
)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(context_clues.unwrap(), &CONFIRMATION_PROMPT_CONTEXT_CLUES);
}
#[rstest]
fn test_sonarr_context_clue_provider_add_series_search_results_clues(
#[values(
ActiveSonarrBlock::AddSeriesAlreadyInLibrary,
ActiveSonarrBlock::AddSeriesSearchResults
)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::test_default();
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(
context_clues.unwrap(),
&ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES
);
}
#[test]
fn test_sonarr_context_clue_provider_system_tasks_clues() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(context_clues.unwrap(), &SYSTEM_TASKS_CONTEXT_CLUES);
}
#[rstest]
#[case(0, ActiveSonarrBlock::Series, &SERIES_CONTEXT_CLUES)]
#[case(1, ActiveSonarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)]
#[case(2, ActiveSonarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)]
#[case(3, ActiveSonarrBlock::History, &HISTORY_CONTEXT_CLUES)]
#[case(4, ActiveSonarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)]
#[case(5, ActiveSonarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)]
#[case(6, ActiveSonarrBlock::System, &SYSTEM_CONTEXT_CLUES)]
fn test_sonarr_context_clue_provider_sonarr_tabs(
#[case] index: usize,
#[case] active_sonarr_block: ActiveSonarrBlock,
#[case] expected_context_clues: &[ContextClue],
) {
let mut app = App::test_default();
app.data.sonarr_data = SonarrData::default();
app.data.sonarr_data.main_tabs.set_index(index);
app.push_navigation_stack(active_sonarr_block.into());
let context_clues = SonarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(expected_context_clues, context_clues.unwrap());
}
} }
+8 -8
View File
@@ -107,7 +107,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into() SonarrEvent::GetDownloads(500).into()
); );
assert!(!app.data.sonarr_data.prompt_confirm); assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
@@ -366,7 +366,7 @@ mod tests {
assert!(app.is_loading); assert!(app.is_loading);
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into() SonarrEvent::GetDownloads(500).into()
); );
assert!(!app.data.sonarr_data.prompt_confirm); assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0); assert_eq!(app.tick_count, 0);
@@ -604,7 +604,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into() SonarrEvent::GetDownloads(500).into()
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
@@ -642,7 +642,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into() SonarrEvent::GetDownloads(500).into()
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
@@ -667,7 +667,7 @@ mod tests {
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into() SonarrEvent::GetDownloads(500).into()
); );
assert!(!app.data.sonarr_data.prompt_confirm); assert!(!app.data.sonarr_data.prompt_confirm);
} }
@@ -692,7 +692,7 @@ mod tests {
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into() SonarrEvent::GetDownloads(500).into()
); );
assert!(app.should_refresh); assert!(app.should_refresh);
assert!(!app.data.sonarr_data.prompt_confirm); assert!(!app.data.sonarr_data.prompt_confirm);
@@ -709,7 +709,7 @@ mod tests {
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into() SonarrEvent::GetDownloads(500).into()
); );
assert!(app.is_loading); assert!(app.is_loading);
assert!(app.should_refresh); assert!(app.should_refresh);
@@ -743,7 +743,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
sync_network_rx.recv().await.unwrap(), sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into() SonarrEvent::GetDownloads(500).into()
); );
assert!(app.is_loading); assert!(app.is_loading);
} }
+6 -3
View File
@@ -23,7 +23,10 @@ pub enum RadarrListCommand {
#[command(about = "List all Radarr collections")] #[command(about = "List all Radarr collections")]
Collections, Collections,
#[command(about = "List all active downloads in Radarr")] #[command(about = "List all active downloads in Radarr")]
Downloads, Downloads {
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
count: u64,
},
#[command(about = "List disk space details for all provisioned root folders in Radarr")] #[command(about = "List disk space details for all provisioned root folders in Radarr")]
DiskSpace, DiskSpace,
#[command(about = "List all Radarr indexers")] #[command(about = "List all Radarr indexers")]
@@ -104,10 +107,10 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrListCommand::Downloads => { RadarrListCommand::Downloads { count } => {
let resp = self let resp = self
.network .network
.handle_network_event(RadarrEvent::GetDownloads.into()) .handle_network_event(RadarrEvent::GetDownloads(count).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+47 -2
View File
@@ -29,7 +29,6 @@ mod tests {
#[values( #[values(
"blocklist", "blocklist",
"collections", "collections",
"downloads",
"disk-space", "disk-space",
"indexers", "indexers",
"movies", "movies",
@@ -59,6 +58,15 @@ mod tests {
); );
} }
#[test]
fn test_list_downloads_count_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "list", "downloads", "--count"]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test] #[test]
fn test_list_logs_events_flag_requires_arguments() { fn test_list_logs_events_flag_requires_arguments() {
let result = let result =
@@ -87,6 +95,18 @@ mod tests {
} }
} }
#[test]
fn test_list_downloads_default_values() {
let expected_args = RadarrListCommand::Downloads { count: 500 };
let result = Cli::try_parse_from(["managarr", "radarr", "list", "downloads"]);
assert!(result.is_ok());
if let Some(Command::Radarr(RadarrCommand::List(refresh_command))) = result.unwrap().command {
assert_eq!(refresh_command, expected_args);
}
}
#[test] #[test]
fn test_list_logs_default_values() { fn test_list_logs_default_values() {
let expected_args = RadarrListCommand::Logs { let expected_args = RadarrListCommand::Logs {
@@ -122,7 +142,6 @@ mod tests {
#[rstest] #[rstest]
#[case(RadarrListCommand::Blocklist, RadarrEvent::GetBlocklist)] #[case(RadarrListCommand::Blocklist, RadarrEvent::GetBlocklist)]
#[case(RadarrListCommand::Collections, RadarrEvent::GetCollections)] #[case(RadarrListCommand::Collections, RadarrEvent::GetCollections)]
#[case(RadarrListCommand::Downloads, RadarrEvent::GetDownloads)]
#[case(RadarrListCommand::DiskSpace, RadarrEvent::GetDiskSpace)] #[case(RadarrListCommand::DiskSpace, RadarrEvent::GetDiskSpace)]
#[case(RadarrListCommand::Indexers, RadarrEvent::GetIndexers)] #[case(RadarrListCommand::Indexers, RadarrEvent::GetIndexers)]
#[case(RadarrListCommand::Movies, RadarrEvent::GetMovies)] #[case(RadarrListCommand::Movies, RadarrEvent::GetMovies)]
@@ -182,6 +201,32 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test]
async fn test_handle_list_downloads_command() {
let expected_count = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::GetDownloads(expected_count).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_downloads_command = RadarrListCommand::Downloads { count: 1000 };
let result =
RadarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test] #[tokio::test]
async fn test_handle_list_logs_command() { async fn test_handle_list_logs_command() {
let expected_events = 1000; let expected_events = 1000;
+18
View File
@@ -118,6 +118,17 @@ pub enum RadarrCommand {
}, },
#[command(about = "Test all Radarr indexers")] #[command(about = "Test all Radarr indexers")]
TestAllIndexers, TestAllIndexers,
#[command(
about = "Toggle monitoring for the specified movie corresponding to the given movie ID"
)]
ToggleMovieMonitoring {
#[arg(
long,
help = "The Radarr ID of the movie to toggle monitoring on",
required = true
)]
movie_id: i64,
},
#[command(about = "Trigger an automatic search for the movie with the specified ID")] #[command(about = "Trigger an automatic search for the movie with the specified ID")]
TriggerAutomaticSearch { TriggerAutomaticSearch {
#[arg( #[arg(
@@ -250,6 +261,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
RadarrCommand::ToggleMovieMonitoring { movie_id } => {
let resp = self
.network
.handle_network_event(RadarrEvent::ToggleMovieMonitoring(movie_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrCommand::TriggerAutomaticSearch { movie_id } => { RadarrCommand::TriggerAutomaticSearch { movie_id } => {
let resp = self let resp = self
.network .network
+51
View File
@@ -215,6 +215,31 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[test]
fn test_toggle_movie_monitoring_requires_movie_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "radarr", "toggle-movie-monitoring"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_movie_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"radarr",
"toggle-movie-monitoring",
"--movie-id",
"1",
]);
assert!(result.is_ok());
}
#[test] #[test]
fn test_trigger_automatic_search_requires_movie_id() { fn test_trigger_automatic_search_requires_movie_id() {
let result = let result =
@@ -461,6 +486,32 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test]
async fn test_toggle_movie_monitoring_command() {
let expected_movie_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
RadarrEvent::ToggleMovieMonitoring(expected_movie_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Radarr(RadarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let toggle_movie_monitoring_command = RadarrCommand::ToggleMovieMonitoring { movie_id: 1 };
let result =
RadarrCliHandler::with(&app_arc, toggle_movie_monitoring_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test] #[tokio::test]
async fn test_trigger_automatic_search_command() { async fn test_trigger_automatic_search_command() {
let expected_movie_id = 1; let expected_movie_id = 1;
+6 -3
View File
@@ -21,7 +21,10 @@ pub enum SonarrListCommand {
#[command(about = "List all items in the Sonarr blocklist")] #[command(about = "List all items in the Sonarr blocklist")]
Blocklist, Blocklist,
#[command(about = "List all active downloads in Sonarr")] #[command(about = "List all active downloads in Sonarr")]
Downloads, Downloads {
#[arg(long, help = "How many downloads to fetch", default_value_t = 500)]
count: u64,
},
#[command(about = "List disk space details for all provisioned root folders in Sonarr")] #[command(about = "List disk space details for all provisioned root folders in Sonarr")]
DiskSpace, DiskSpace,
#[command(about = "List the episodes for the series with the given ID")] #[command(about = "List the episodes for the series with the given ID")]
@@ -146,10 +149,10 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrListCommand::Downloads => { SonarrListCommand::Downloads { count } => {
let resp = self let resp = self
.network .network
.handle_network_event(SonarrEvent::GetDownloads.into()) .handle_network_event(SonarrEvent::GetDownloads(count).into())
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
+48 -2
View File
@@ -28,7 +28,6 @@ mod tests {
#[values( #[values(
"blocklist", "blocklist",
"series", "series",
"downloads",
"disk-space", "disk-space",
"quality-profiles", "quality-profiles",
"indexers", "indexers",
@@ -102,6 +101,28 @@ mod tests {
} }
} }
#[test]
fn test_list_downloads_count_flag_requires_arguments() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "downloads", "--count"]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue);
}
#[test]
fn test_list_downloads_default_values() {
let expected_args = SonarrListCommand::Downloads { count: 500 };
let result = Cli::try_parse_from(["managarr", "sonarr", "list", "downloads"]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::List(downloads_command))) = result.unwrap().command
{
assert_eq!(downloads_command, expected_args);
}
}
#[test] #[test]
fn test_list_history_events_flag_requires_arguments() { fn test_list_history_events_flag_requires_arguments() {
let result = let result =
@@ -287,7 +308,6 @@ mod tests {
#[rstest] #[rstest]
#[case(SonarrListCommand::Blocklist, SonarrEvent::GetBlocklist)] #[case(SonarrListCommand::Blocklist, SonarrEvent::GetBlocklist)]
#[case(SonarrListCommand::Downloads, SonarrEvent::GetDownloads)]
#[case(SonarrListCommand::DiskSpace, SonarrEvent::GetDiskSpace)] #[case(SonarrListCommand::DiskSpace, SonarrEvent::GetDiskSpace)]
#[case(SonarrListCommand::Indexers, SonarrEvent::GetIndexers)] #[case(SonarrListCommand::Indexers, SonarrEvent::GetIndexers)]
#[case(SonarrListCommand::QualityProfiles, SonarrEvent::GetQualityProfiles)] #[case(SonarrListCommand::QualityProfiles, SonarrEvent::GetQualityProfiles)]
@@ -374,6 +394,32 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test]
async fn test_handle_list_downloads_command() {
let expected_count = 1000;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::GetDownloads(expected_count).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let list_downloads_command = SonarrListCommand::Downloads { count: 1000 };
let result =
SonarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
#[tokio::test] #[tokio::test]
async fn test_handle_list_history_command() { async fn test_handle_list_history_command() {
let expected_events = 1000; let expected_events = 1000;
+18
View File
@@ -146,6 +146,17 @@ pub enum SonarrCommand {
)] )]
season_number: i64, season_number: i64,
}, },
#[command(
about = "Toggle monitoring for the specified series corresponding to the given series ID"
)]
ToggleSeriesMonitoring {
#[arg(
long,
help = "The Sonarr ID of the series to toggle monitoring on",
required = true
)]
series_id: i64,
},
} }
impl From<SonarrCommand> for Command { impl From<SonarrCommand> for Command {
@@ -290,6 +301,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
.await?; .await?;
serde_json::to_string_pretty(&resp)? serde_json::to_string_pretty(&resp)?
} }
SonarrCommand::ToggleSeriesMonitoring { series_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::ToggleSeriesMonitoring(series_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
}; };
Ok(result) Ok(result)
+56 -2
View File
@@ -216,6 +216,31 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[test]
fn test_toggle_series_monitoring_requires_series_id() {
let result =
Cli::command().try_get_matches_from(["managarr", "sonarr", "toggle-series-monitoring"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_toggle_series_monitoring_requirements_satisfied() {
let result = Cli::command().try_get_matches_from([
"managarr",
"sonarr",
"toggle-series-monitoring",
"--series-id",
"1",
]);
assert!(result.is_ok());
}
} }
mod handler { mod handler {
@@ -692,7 +717,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn test_list_toggle_episode_monitoring_command() { async fn test_toggle_episode_monitoring_command() {
let expected_episode_id = 1; let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
mock_network mock_network
@@ -722,7 +747,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn test_list_toggle_season_monitoring_command() { async fn test_toggle_season_monitoring_command() {
let expected_series_id = 1; let expected_series_id = 1;
let expected_season_number = 1; let expected_season_number = 1;
let mut mock_network = MockNetworkTrait::new(); let mut mock_network = MockNetworkTrait::new();
@@ -753,5 +778,34 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test]
async fn test_toggle_series_monitoring_command() {
let expected_series_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::ToggleSeriesMonitoring(expected_series_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::test_default()));
let toggle_series_monitoring_command = SonarrCommand::ToggleSeriesMonitoring { series_id: 1 };
let result = SonarrCliHandler::with(
&app_arc,
toggle_series_monitoring_command,
&mut mock_network,
)
.handle()
.await;
assert!(result.is_ok());
}
} }
} }
+7 -3
View File
@@ -4,7 +4,7 @@ use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crossterm::event; use crossterm::event;
use crossterm::event::Event as CrosstermEvent; use crossterm::event::{Event as CrosstermEvent, KeyEventKind};
use crate::event::Key; use crate::event::Key;
@@ -30,8 +30,12 @@ impl Events {
.unwrap_or_else(|| Duration::from_secs(0)); .unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout).unwrap() { if event::poll(timeout).unwrap() {
if let CrosstermEvent::Key(key_event) = event::read().unwrap() { if let CrosstermEvent::Key(key_event) = event::read().unwrap() {
let key = Key::from(key_event); // Only process the key event if it's a press event
tx.send(InputEvent::KeyEvent(key)).unwrap(); // Source: https://ratatui.rs/faq/ Why am I getting duplicate key events on Windows?
if key_event.kind == KeyEventKind::Press {
let key = Key::from(key_event);
tx.send(InputEvent::KeyEvent(key)).unwrap();
}
} }
} }
+27 -15
View File
@@ -13,6 +13,8 @@ pub enum Key {
Down, Down,
Left, Left,
Right, Right,
PgDown,
PgUp,
Enter, Enter,
Esc, Esc,
Backspace, Backspace,
@@ -29,21 +31,23 @@ pub enum Key {
impl Display for Key { impl Display for Key {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match *self {
Key::Char(c) => write!(f, "<{c}>"), Key::Char(c) => write!(f, "{c}"),
Key::Ctrl(c) => write!(f, "<ctrl-{c}>"), Key::Ctrl(c) => write!(f, "ctrl-{c}"),
Key::Up => write!(f, "<↑>"), Key::Up => write!(f, ""),
Key::Down => write!(f, "<↓>"), Key::Down => write!(f, ""),
Key::Left => write!(f, "<←>"), Key::Left => write!(f, ""),
Key::Right => write!(f, "<→>"), Key::Right => write!(f, ""),
Key::Enter => write!(f, "<enter>"), Key::PgDown => write!(f, "pgDown"),
Key::Esc => write!(f, "<esc>"), Key::PgUp => write!(f, "pgUp"),
Key::Backspace => write!(f, "<backspace>"), Key::Enter => write!(f, "enter"),
Key::Home => write!(f, "<home>"), Key::Esc => write!(f, "esc"),
Key::End => write!(f, "<end>"), Key::Backspace => write!(f, "backspace"),
Key::Tab => write!(f, "<tab>"), Key::Home => write!(f, "home"),
Key::BackTab => write!(f, "<shift-tab>"), Key::End => write!(f, "end"),
Key::Delete => write!(f, "<del>"), Key::Tab => write!(f, "tab"),
_ => write!(f, "<{self:?}>"), Key::BackTab => write!(f, "shift-tab"),
Key::Delete => write!(f, "del"),
_ => write!(f, "{self:?}"),
} }
} }
} }
@@ -66,6 +70,14 @@ impl From<KeyEvent> for Key {
code: KeyCode::Right, code: KeyCode::Right,
.. ..
} => Key::Right, } => Key::Right,
KeyEvent {
code: KeyCode::PageDown,
..
} => Key::PgDown,
KeyEvent {
code: KeyCode::PageUp,
..
} => Key::PgUp,
KeyEvent { KeyEvent {
code: KeyCode::Backspace, code: KeyCode::Backspace,
.. ..
+13 -1
View File
@@ -11,6 +11,8 @@ mod tests {
#[case(Key::Down, "")] #[case(Key::Down, "")]
#[case(Key::Left, "")] #[case(Key::Left, "")]
#[case(Key::Right, "")] #[case(Key::Right, "")]
#[case(Key::PgDown, "pgDown")]
#[case(Key::PgUp, "pgUp")]
#[case(Key::Enter, "enter")] #[case(Key::Enter, "enter")]
#[case(Key::Esc, "esc")] #[case(Key::Esc, "esc")]
#[case(Key::Backspace, "backspace")] #[case(Key::Backspace, "backspace")]
@@ -22,7 +24,7 @@ mod tests {
#[case(Key::Char('q'), "q")] #[case(Key::Char('q'), "q")]
#[case(Key::Ctrl('q'), "ctrl-q")] #[case(Key::Ctrl('q'), "ctrl-q")]
fn test_key_formatter(#[case] key: Key, #[case] expected_str: &str) { fn test_key_formatter(#[case] key: Key, #[case] expected_str: &str) {
assert_str_eq!(format!("{key}"), format!("<{expected_str}>")); assert_str_eq!(format!("{key}"), format!("{expected_str}"));
} }
#[test] #[test]
@@ -45,6 +47,16 @@ mod tests {
assert_eq!(Key::from(KeyEvent::from(KeyCode::Right)), Key::Right); assert_eq!(Key::from(KeyEvent::from(KeyCode::Right)), Key::Right);
} }
#[test]
fn test_key_from_page_down() {
assert_eq!(Key::from(KeyEvent::from(KeyCode::PageDown)), Key::PgDown);
}
#[test]
fn test_key_from_page_up() {
assert_eq!(Key::from(KeyEvent::from(KeyCode::PageUp)), Key::PgUp);
}
#[test] #[test]
fn test_key_from_backspace() { fn test_key_from_backspace() {
assert_eq!( assert_eq!(
+177 -3
View File
@@ -6,13 +6,20 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::context_clues::SERVARR_CONTEXT_CLUES;
use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS};
use crate::app::radarr::radarr_context_clues::{
LIBRARY_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES,
};
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::handle_events;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::handlers::{handle_events, populate_keymapping_table};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable;
use crate::models::HorizontallyScrollableText; use crate::models::HorizontallyScrollableText;
use crate::models::Route; use crate::models::Route;
@@ -82,6 +89,82 @@ mod tests {
assert!(app.cancellation_token.is_cancelled()); assert!(app.cancellation_token.is_cancelled());
} }
#[test]
fn test_handle_populate_keybindings_table_on_help_button_press() {
let mut app = App::test_default();
let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
app.push_navigation_stack(ActiveKeybindingBlock::Help.into());
handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app);
assert!(app.keymapping_table.is_some());
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
#[test]
fn test_handle_ignore_help_button_when_ignore_special_keys_for_textbox_input_is_true() {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveRadarrBlock::default().into());
handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app);
assert!(app.keymapping_table.is_none());
}
#[test]
fn test_handle_empties_keybindings_table_on_help_button_press_when_keybindings_table_is_already_populated(
) {
let mut app = App::test_default();
let keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(keybinding_items);
app.keymapping_table = Some(stateful_table);
app.push_navigation_stack(ActiveRadarrBlock::default().into());
handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app);
assert!(app.keymapping_table.is_none());
}
#[test]
fn test_handle_shows_keymapping_popup_when_keymapping_table_is_populated() {
let mut app = App::test_default();
let keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(keybinding_items);
app.keymapping_table = Some(stateful_table);
app.push_navigation_stack(ActiveRadarrBlock::default().into());
let expected_selection = KeybindingItem {
key: SERVARR_CONTEXT_CLUES[1].0.key.to_string(),
alt_key: SERVARR_CONTEXT_CLUES[1]
.0
.alt
.map_or(String::new(), |k| k.to_string()),
desc: SERVARR_CONTEXT_CLUES[1].1.to_string(),
};
handle_events(DEFAULT_KEYBINDINGS.down.key, &mut app);
assert!(app.keymapping_table.is_some());
assert_eq!(
&expected_selection,
app.keymapping_table.unwrap().current_selection()
);
}
#[rstest] #[rstest]
fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) { fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -113,4 +196,95 @@ mod tests {
assert!(!app.data.sonarr_data.prompt_confirm); assert!(!app.data.sonarr_data.prompt_confirm);
} }
#[test]
fn test_populate_keymapping_table_global_options() {
let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| {
let (key, alt_key) = if key.alt.is_some() {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
} else {
(key.key.to_string(), String::new())
};
KeybindingItem {
key,
alt_key,
desc: desc.to_string(),
}
})
.collect::<Vec<_>>();
let mut app = App::test_default();
app.push_navigation_stack(ActiveKeybindingBlock::Help.into());
populate_keymapping_table(&mut app);
assert!(app.keymapping_table.is_some());
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
#[test]
fn test_populate_keymapping_table_populates_servarr_specific_tab_info_before_global_options() {
let mut expected_keybinding_items = LIBRARY_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
expected_keybinding_items.extend(
SERVARR_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(ActiveRadarrBlock::default().into());
populate_keymapping_table(&mut app);
assert!(app.keymapping_table.is_some());
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
#[test]
fn test_populate_keymapping_table_populates_delegated_servarr_context_provider_options_before_global_options(
) {
let mut expected_keybinding_items = MOVIE_DETAILS_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
expected_keybinding_items.extend(
SERVARR_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into());
populate_keymapping_table(&mut app);
assert!(app.keymapping_table.is_some());
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
fn context_clue_to_keybinding_item(key: &KeyBinding, desc: &&str) -> KeybindingItem {
let (key, alt_key) = if key.alt.is_some() {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
} else {
(key.key.to_string(), String::new())
};
KeybindingItem {
key,
alt_key,
desc: desc.to_string(),
}
}
} }
+80
View File
@@ -0,0 +1,80 @@
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::KeyEventHandler;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_models::KeybindingItem;
#[cfg(test)]
#[path = "keybinding_handler_tests.rs"]
mod keybinding_handler_tests;
pub(super) struct KeybindingHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
}
impl KeybindingHandler<'_, '_> {
handle_table_events!(
self,
keybindings,
self.app.keymapping_table.as_mut().unwrap(),
KeybindingItem
);
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandler<'a, 'b> {
fn handle(&mut self) {
let keybinding_table_handling_config = TableHandlingConfig::new(self.app.get_current_route());
if !self.handle_keybindings_table_events(keybinding_table_handling_config) {
self.handle_key_event();
}
}
fn accepts(_active_block: ActiveKeybindingBlock) -> bool {
true
}
fn new(
key: Key,
app: &'a mut App<'b>,
_active_block: ActiveKeybindingBlock,
_context: Option<ActiveKeybindingBlock>,
) -> KeybindingHandler<'a, 'b> {
KeybindingHandler { key, app }
}
fn get_key(&self) -> Key {
self.key
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn is_ready(&self) -> bool {
self.app.keymapping_table.is_some()
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_down(&mut self) {}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {}
fn handle_submit(&mut self) {}
fn handle_esc(&mut self) {
self.app.keymapping_table = None;
}
fn handle_char_key_event(&mut self) {}
}
+69
View File
@@ -0,0 +1,69 @@
#[cfg(test)]
mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::KeybindingHandler;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::stateful_table::StatefulTable;
use rstest::rstest;
mod test_handle_esc {
use super::*;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use pretty_assertions::assert_eq;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_esc_empties_keymapping_table() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
KeybindingHandler::new(ESC_KEY, &mut app, ActiveKeybindingBlock::Help, None).handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
assert!(app.keymapping_table.is_none());
}
}
#[test]
fn test_keybinding_handler_accepts() {
assert!(KeybindingHandler::accepts(ActiveKeybindingBlock::Help));
}
#[test]
fn test_keybinding_handler_not_ready_when_keybinding_is_empty() {
let mut app = App::test_default();
app.is_loading = false;
let handler = KeybindingHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveKeybindingBlock::Help,
None,
);
assert!(!handler.is_ready());
}
#[rstest]
fn test_keybinding_handler_ready_when_keymapping_table_is_not_empty(
#[values(true, false)] is_loading: bool,
) {
let mut app = App::test_default();
app.keymapping_table = Some(StatefulTable::default());
app.is_loading = is_loading;
let handler = KeybindingHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveKeybindingBlock::Help,
None,
);
assert!(handler.is_ready());
}
}
+84 -23
View File
@@ -1,11 +1,20 @@
use radarr_handlers::RadarrHandler; use radarr_handlers::RadarrHandler;
use sonarr_handlers::SonarrHandler; use sonarr_handlers::SonarrHandler;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::context_clues::{
ContextClueProvider, ServarrContextClueProvider, SERVARR_CONTEXT_CLUES,
};
use crate::app::key_binding::KeyBinding;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::keybinding_handler::KeybindingHandler;
use crate::matches_key;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, Route}; use crate::models::{HorizontallyScrollableText, Route};
mod keybinding_handler;
mod radarr_handlers; mod radarr_handlers;
mod sonarr_handlers; mod sonarr_handlers;
@@ -22,40 +31,42 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
fn handle_key_event(&mut self) { fn handle_key_event(&mut self) {
let key = self.get_key(); let key = self.get_key();
match key { match key {
_ if key == DEFAULT_KEYBINDINGS.up.key => { _ if matches_key!(up, key, self.ignore_special_keys()) => {
if self.is_ready() { if self.is_ready() {
self.handle_scroll_up(); self.handle_scroll_up();
} }
} }
_ if key == DEFAULT_KEYBINDINGS.down.key => { _ if matches_key!(down, key, self.ignore_special_keys()) => {
if self.is_ready() { if self.is_ready() {
self.handle_scroll_down(); self.handle_scroll_down();
} }
} }
_ if key == DEFAULT_KEYBINDINGS.home.key => { _ if matches_key!(home, key) => {
if self.is_ready() { if self.is_ready() {
self.handle_home(); self.handle_home();
} }
} }
_ if key == DEFAULT_KEYBINDINGS.end.key => { _ if matches_key!(end, key) => {
if self.is_ready() { if self.is_ready() {
self.handle_end(); self.handle_end();
} }
} }
_ if key == DEFAULT_KEYBINDINGS.delete.key => { _ if matches_key!(delete, key) => {
if self.is_ready() { if self.is_ready() {
self.handle_delete(); self.handle_delete();
} }
} }
_ if key == DEFAULT_KEYBINDINGS.left.key || key == DEFAULT_KEYBINDINGS.right.key => { _ if matches_key!(left, key, self.ignore_special_keys())
|| matches_key!(right, key, self.ignore_special_keys()) =>
{
self.handle_left_right_action() self.handle_left_right_action()
} }
_ if key == DEFAULT_KEYBINDINGS.submit.key => { _ if matches_key!(submit, key) => {
if self.is_ready() { if self.is_ready() {
self.handle_submit(); self.handle_submit();
} }
} }
_ if key == DEFAULT_KEYBINDINGS.esc.key => self.handle_esc(), _ if matches_key!(esc, key) => self.handle_esc(),
_ => { _ => {
if self.is_ready() { if self.is_ready() {
self.handle_char_key_event(); self.handle_char_key_event();
@@ -71,6 +82,7 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
fn accepts(active_block: T) -> bool; fn accepts(active_block: T) -> bool;
fn new(key: Key, app: &'a mut App<'b>, active_block: T, context: Option<T>) -> Self; fn new(key: Key, app: &'a mut App<'b>, active_block: T, context: Option<T>) -> Self;
fn get_key(&self) -> Key; fn get_key(&self) -> Key;
fn ignore_special_keys(&self) -> bool;
fn is_ready(&self) -> bool; fn is_ready(&self) -> bool;
fn handle_scroll_up(&mut self); fn handle_scroll_up(&mut self);
fn handle_scroll_down(&mut self); fn handle_scroll_down(&mut self);
@@ -84,18 +96,27 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
} }
pub fn handle_events(key: Key, app: &mut App<'_>) { pub fn handle_events(key: Key, app: &mut App<'_>) {
if key == DEFAULT_KEYBINDINGS.next_servarr.key { if matches_key!(next_servarr, key) {
app.reset(); app.reset();
app.server_tabs.next(); app.server_tabs.next();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route()); app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
app.cancellation_token.cancel(); app.cancellation_token.cancel();
} else if key == DEFAULT_KEYBINDINGS.previous_servarr.key { } else if matches_key!(previous_servarr, key) {
app.reset(); app.reset();
app.server_tabs.previous(); app.server_tabs.previous();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route()); app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
app.cancellation_token.cancel(); app.cancellation_token.cancel();
} else if matches_key!(help, key) && !app.ignore_special_keys_for_textbox_input {
if app.keymapping_table.is_none() {
populate_keymapping_table(app);
} else {
app.keymapping_table = None;
}
} else { } else {
match app.get_current_route() { match app.get_current_route() {
_ if app.keymapping_table.is_some() => {
KeybindingHandler::new(key, app, ActiveKeybindingBlock::Help, None).handle();
}
Route::Radarr(active_radarr_block, context) => { Route::Radarr(active_radarr_block, context) => {
RadarrHandler::new(key, app, active_radarr_block, context).handle() RadarrHandler::new(key, app, active_radarr_block, context).handle()
} }
@@ -107,6 +128,48 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
} }
} }
fn populate_keymapping_table(app: &mut App<'_>) {
let context_clue_to_keybinding_item = |key: &KeyBinding, desc: &&str| {
let (key, alt_key) = if key.alt.is_some() {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
} else {
(key.key.to_string(), String::new())
};
KeybindingItem {
key,
alt_key,
desc: desc.to_string(),
}
};
let mut keybindings = Vec::new();
let global_keybindings = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
if let Some(contextual_help) = app.server_tabs.get_active_route_contextual_help() {
keybindings.extend(
contextual_help
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
}
if let Some(contextual_help) = ServarrContextClueProvider::get_context_clues(app) {
keybindings.extend(
contextual_help
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
}
keybindings.extend(global_keybindings);
let mut table = StatefulTable::default();
table.set_items(keybindings);
app.keymapping_table = Some(table);
}
fn handle_clear_errors(app: &mut App<'_>) { fn handle_clear_errors(app: &mut App<'_>) {
if !app.error.text.is_empty() { if !app.error.text.is_empty() {
app.error = HorizontallyScrollableText::default(); app.error = HorizontallyScrollableText::default();
@@ -115,17 +178,15 @@ fn handle_clear_errors(app: &mut App<'_>) {
fn handle_prompt_toggle(app: &mut App<'_>, key: Key) { fn handle_prompt_toggle(app: &mut App<'_>, key: Key) {
match key { match key {
_ if key == DEFAULT_KEYBINDINGS.left.key || key == DEFAULT_KEYBINDINGS.right.key => { _ if matches_key!(left, key) || matches_key!(right, key) => match app.get_current_route() {
match app.get_current_route() { Route::Radarr(_, _) => {
Route::Radarr(_, _) => { app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm
app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm
}
Route::Sonarr(_, _) => {
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
}
_ => (),
} }
} Route::Sonarr(_, _) => {
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
}
_ => (),
},
_ => (), _ => (),
} }
} }
@@ -149,7 +210,7 @@ macro_rules! handle_text_box_left_right_keys {
macro_rules! handle_text_box_keys { macro_rules! handle_text_box_keys {
($self:expr, $key:expr, $input:expr) => { ($self:expr, $key:expr, $input:expr) => {
match $self.key { match $self.key {
_ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.backspace.key => { _ if $crate::matches_key!(backspace, $key) => {
$input.pop(); $input.pop();
} }
Key::Char(character) => { Key::Char(character) => {
@@ -165,7 +226,7 @@ macro_rules! handle_prompt_left_right_keys {
($self:expr, $confirm_prompt:expr, $data:ident) => { ($self:expr, $confirm_prompt:expr, $data:ident) => {
if $self.app.data.$data.selected_block.get_active_block() == $confirm_prompt { if $self.app.data.$data.selected_block.get_active_block() == $confirm_prompt {
handle_prompt_toggle($self.app, $self.key); handle_prompt_toggle($self.app, $self.key);
} else if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.left.key { } else if $crate::matches_key!(left, $self.key) {
$self.app.data.$data.selected_block.left(); $self.app.data.$data.selected_block.left();
} else { } else {
$self.app.data.$data.selected_block.right(); $self.app.data.$data.selected_block.right();
@@ -4,6 +4,7 @@ mod tests {
use chrono::DateTime; use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -541,6 +542,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_blocklist_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = BlocklistHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_extract_blocklist_item_id() { fn test_extract_blocklist_item_id() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
@@ -9,6 +7,7 @@ use crate::models::radarr_models::BlocklistItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "blocklist_handler_tests.rs"] #[path = "blocklist_handler_tests.rs"]
@@ -51,6 +50,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
BLOCKLIST_BLOCKS.contains(&active_block) BLOCKLIST_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -143,10 +146,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
let key = self.key; let key = self.key;
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => match self.key { ActiveRadarrBlock::Blocklist => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ if key == DEFAULT_KEYBINDINGS.clear.key => { _ if matches_key!(clear, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into());
@@ -154,7 +157,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
_ => (), _ => (),
}, },
ActiveRadarrBlock::DeleteBlocklistItemPrompt => { ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem( self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem(
self.extract_blocklist_item_id(), self.extract_blocklist_item_id(),
@@ -164,7 +167,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
} }
} }
ActiveRadarrBlock::BlocklistClearAllItemsPrompt => { ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist); self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist);
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::KeyEventHandler; use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::CollectionMovie; use crate::models::radarr_models::CollectionMovie;
@@ -11,6 +9,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
}; };
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
use crate::models::BlockSelectionState; use crate::models::BlockSelectionState;
use crate::{handle_table_events, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "collection_details_handler_tests.rs"] #[path = "collection_details_handler_tests.rs"]
@@ -46,6 +45,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
COLLECTION_DETAILS_BLOCKS.contains(&active_block) COLLECTION_DETAILS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -130,7 +133,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
fn handle_char_key_event(&mut self) { fn handle_char_key_event(&mut self) {
if self.active_radarr_block == ActiveRadarrBlock::CollectionDetails if self.active_radarr_block == ActiveRadarrBlock::CollectionDetails
&& self.key == DEFAULT_KEYBINDINGS.edit.key && matches_key!(edit, self.key)
{ {
self.app.push_navigation_stack( self.app.push_navigation_stack(
( (
@@ -1,6 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_str_eq; use pretty_assertions::assert_str_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -278,6 +279,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_collection_details_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = CollectionDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_collection_details_handler_not_ready_when_loading() { fn test_collection_details_handler_not_ready_when_loading() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -589,6 +589,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_collections_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = CollectionsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_collections_handler_not_ready_when_loading() { fn test_collections_handler_not_ready_when_loading() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -644,7 +663,7 @@ mod tests {
Collection { Collection {
id: 3, id: 3,
title: "test 1".into(), title: "test 1".into(),
movies: Some(iter::repeat(CollectionMovie::default()).take(3).collect()), movies: Some(iter::repeat_n(CollectionMovie::default(), 3).collect()),
root_folder_path: Some("/nfs/movies".into()), root_folder_path: Some("/nfs/movies".into()),
quality_profile_id: 1, quality_profile_id: 1,
search_on_add: false, search_on_add: false,
@@ -654,7 +673,7 @@ mod tests {
Collection { Collection {
id: 2, id: 2,
title: "test 2".into(), title: "test 2".into(),
movies: Some(iter::repeat(CollectionMovie::default()).take(7).collect()), movies: Some(iter::repeat_n(CollectionMovie::default(), 7).collect()),
root_folder_path: Some("/htpc/movies".into()), root_folder_path: Some("/htpc/movies".into()),
quality_profile_id: 3, quality_profile_id: 3,
search_on_add: true, search_on_add: true,
@@ -664,7 +683,7 @@ mod tests {
Collection { Collection {
id: 1, id: 1,
title: "test 3".into(), title: "test 3".into(),
movies: Some(iter::repeat(CollectionMovie::default()).take(1).collect()), movies: Some(iter::repeat_n(CollectionMovie::default(), 1).collect()),
root_folder_path: Some("/nfs/some/stupidly/long/path/to/test/with".into()), root_folder_path: Some("/nfs/some/stupidly/long/path/to/test/with".into()),
quality_profile_id: 1, quality_profile_id: 1,
search_on_add: false, search_on_add: false,
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -7,7 +6,7 @@ use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS};
use crate::models::Scrollable; use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "edit_collection_handler_tests.rs"] #[path = "edit_collection_handler_tests.rs"]
@@ -69,6 +68,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
EDIT_COLLECTION_BLOCKS.contains(&active_block) EDIT_COLLECTION_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -262,7 +265,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
) )
.into(), .into(),
); );
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
ActiveRadarrBlock::EditCollectionToggleMonitored => { ActiveRadarrBlock::EditCollectionToggleMonitored => {
self self
@@ -311,7 +314,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
| ActiveRadarrBlock::EditCollectionSelectQualityProfile => self.app.pop_navigation_stack(), | ActiveRadarrBlock::EditCollectionSelectQualityProfile => self.app.pop_navigation_stack(),
ActiveRadarrBlock::EditCollectionRootFolderPathInput => { ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ => (), _ => (),
} }
@@ -321,7 +324,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::EditCollectionRootFolderPathInput => { ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
ActiveRadarrBlock::EditCollectionPrompt => { ActiveRadarrBlock::EditCollectionPrompt => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
@@ -354,7 +357,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
ActiveRadarrBlock::EditCollectionPrompt => { ActiveRadarrBlock::EditCollectionPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditCollectionConfirmPrompt == ActiveRadarrBlock::EditCollectionConfirmPrompt
&& key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, key)
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection( self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(
@@ -2,6 +2,7 @@
mod tests { mod tests {
use bimap::BiMap; use bimap::BiMap;
use pretty_assertions::assert_str_eq; use pretty_assertions::assert_str_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -457,7 +458,7 @@ mod tests {
#[test] #[test]
fn test_edit_collection_root_folder_path_input_submit() { fn test_edit_collection_root_folder_path_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal { app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal {
path: "Test Path".into(), path: "Test Path".into(),
..EditCollectionModal::default() ..EditCollectionModal::default()
@@ -473,7 +474,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.radarr_data .radarr_data
@@ -759,7 +760,7 @@ mod tests {
assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
if selected_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput { if selected_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput {
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
} }
} }
@@ -791,7 +792,7 @@ mod tests {
); );
if active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput { if active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput {
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
} }
} }
} }
@@ -811,7 +812,7 @@ mod tests {
let mut app = App::test_default(); let mut app = App::test_default();
app.data.radarr_data = create_test_radarr_data(); app.data.radarr_data = create_test_radarr_data();
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default()); app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::EditCollectionRootFolderPathInput.into()); app.push_navigation_stack(ActiveRadarrBlock::EditCollectionRootFolderPathInput.into());
@@ -823,7 +824,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::EditCollectionPrompt.into() ActiveRadarrBlock::EditCollectionPrompt.into()
@@ -926,7 +927,7 @@ mod tests {
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default()); app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
EditCollectionHandler::new( EditCollectionHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::EditCollectionRootFolderPathInput, ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None, None,
@@ -942,7 +943,7 @@ mod tests {
.unwrap() .unwrap()
.path .path
.text, .text,
"h" "a"
); );
} }
@@ -1019,6 +1020,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_edit_collection_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditCollectionHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_build_edit_collection_params() { fn test_build_edit_collection_params() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::collections::collection_details_handler::CollectionDetailsHandler; use crate::handlers::radarr_handlers::collections::collection_details_handler::CollectionDetailsHandler;
use crate::handlers::radarr_handlers::collections::edit_collection_handler::EditCollectionHandler; use crate::handlers::radarr_handlers::collections::edit_collection_handler::EditCollectionHandler;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
@@ -14,6 +12,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::models::BlockSelectionState; use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
mod collection_details_handler; mod collection_details_handler;
mod edit_collection_handler; mod edit_collection_handler;
@@ -73,6 +72,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
|| COLLECTIONS_BLOCKS.contains(&active_block) || COLLECTIONS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -145,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
let key = self.key; let key = self.key;
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::Collections => match self.key { ActiveRadarrBlock::Collections => match self.key {
_ if key == DEFAULT_KEYBINDINGS.edit.key => { _ if matches_key!(edit, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
@@ -154,18 +157,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
self.app.data.radarr_data.selected_block = self.app.data.radarr_data.selected_block =
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
} }
_ if key == DEFAULT_KEYBINDINGS.update.key => { _ if matches_key!(update, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into());
} }
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ => (), _ => (),
}, },
ActiveRadarrBlock::UpdateAllCollectionsPrompt => { ActiveRadarrBlock::UpdateAllCollectionsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections); self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections);
@@ -1,6 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -387,6 +388,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_downloads_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_extract_download_id() { fn test_extract_download_id() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,13 +1,12 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::DownloadRecord; use crate::models::radarr_models::DownloadRecord;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "downloads_handler_tests.rs"] #[path = "downloads_handler_tests.rs"]
@@ -47,6 +46,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
DOWNLOADS_BLOCKS.contains(&active_block) DOWNLOADS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -130,18 +133,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
let key = self.key; let key = self.key;
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::Downloads => match self.key { ActiveRadarrBlock::Downloads => match self.key {
_ if key == DEFAULT_KEYBINDINGS.update.key => { _ if matches_key!(update, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::UpdateDownloadsPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::UpdateDownloadsPrompt.into());
} }
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ => (), _ => (),
}, },
ActiveRadarrBlock::DeleteDownloadPrompt => { ActiveRadarrBlock::DeleteDownloadPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteDownload(self.extract_download_id())); Some(RadarrEvent::DeleteDownload(self.extract_download_id()));
@@ -150,7 +153,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
} }
} }
ActiveRadarrBlock::UpdateDownloadsPrompt => { ActiveRadarrBlock::UpdateDownloadsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads); self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads);
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -6,7 +5,9 @@ use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::servarr_models::EditIndexerParams; use crate::models::servarr_models::EditIndexerParams;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys}; use crate::{
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
#[cfg(test)] #[cfg(test)]
#[path = "edit_indexer_handler_tests.rs"] #[path = "edit_indexer_handler_tests.rs"]
@@ -65,6 +66,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
EDIT_INDEXER_BLOCKS.contains(&active_block) EDIT_INDEXER_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -356,7 +361,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
| ActiveRadarrBlock::EditIndexerSeedRatioInput | ActiveRadarrBlock::EditIndexerSeedRatioInput
| ActiveRadarrBlock::EditIndexerTagsInput => { | ActiveRadarrBlock::EditIndexerTagsInput => {
self.app.push_navigation_stack(selected_block.into()); self.app.push_navigation_stack(selected_block.into());
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
ActiveRadarrBlock::EditIndexerPriorityInput => self ActiveRadarrBlock::EditIndexerPriorityInput => self
.app .app
@@ -402,7 +407,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
| ActiveRadarrBlock::EditIndexerSeedRatioInput | ActiveRadarrBlock::EditIndexerSeedRatioInput
| ActiveRadarrBlock::EditIndexerTagsInput => { | ActiveRadarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
ActiveRadarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(), ActiveRadarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(),
_ => (), _ => (),
@@ -423,7 +428,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
| ActiveRadarrBlock::EditIndexerPriorityInput | ActiveRadarrBlock::EditIndexerPriorityInput
| ActiveRadarrBlock::EditIndexerTagsInput => { | ActiveRadarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ => self.app.pop_navigation_stack(), _ => self.app.pop_navigation_stack(),
} }
@@ -504,7 +509,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
ActiveRadarrBlock::EditIndexerPrompt => { ActiveRadarrBlock::EditIndexerPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditIndexerConfirmPrompt == ActiveRadarrBlock::EditIndexerConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, self.key)
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
@@ -10,6 +10,7 @@ mod tests {
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::servarr_models::EditIndexerParams; use crate::models::servarr_models::EditIndexerParams;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
mod test_handle_scroll_up_and_down { mod test_handle_scroll_up_and_down {
@@ -1002,7 +1003,7 @@ mod tests {
.handle(); .handle();
assert_eq!(app.get_current_route(), block.into()); assert_eq!(app.get_current_route(), block.into());
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
} }
#[test] #[test]
@@ -1027,7 +1028,7 @@ mod tests {
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::EditIndexerPriorityInput.into() ActiveRadarrBlock::EditIndexerPriorityInput.into()
); );
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
} }
#[test] #[test]
@@ -1193,7 +1194,7 @@ mod tests {
fn test_edit_indexer_name_input_submit() { fn test_edit_indexer_name_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal {
name: "Test".into(), name: "Test".into(),
..EditIndexerModal::default() ..EditIndexerModal::default()
@@ -1209,7 +1210,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.radarr_data .radarr_data
@@ -1229,7 +1230,7 @@ mod tests {
fn test_edit_indexer_url_input_submit() { fn test_edit_indexer_url_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal {
url: "Test".into(), url: "Test".into(),
..EditIndexerModal::default() ..EditIndexerModal::default()
@@ -1245,7 +1246,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.radarr_data .radarr_data
@@ -1265,7 +1266,7 @@ mod tests {
fn test_edit_indexer_api_key_input_submit() { fn test_edit_indexer_api_key_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal {
api_key: "Test".into(), api_key: "Test".into(),
..EditIndexerModal::default() ..EditIndexerModal::default()
@@ -1281,7 +1282,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.radarr_data .radarr_data
@@ -1301,7 +1302,7 @@ mod tests {
fn test_edit_indexer_seed_ratio_input_submit() { fn test_edit_indexer_seed_ratio_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal {
seed_ratio: "Test".into(), seed_ratio: "Test".into(),
..EditIndexerModal::default() ..EditIndexerModal::default()
@@ -1317,7 +1318,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.radarr_data .radarr_data
@@ -1337,7 +1338,7 @@ mod tests {
fn test_edit_indexer_tags_input_submit() { fn test_edit_indexer_tags_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal {
tags: "Test".into(), tags: "Test".into(),
..EditIndexerModal::default() ..EditIndexerModal::default()
@@ -1353,7 +1354,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.radarr_data .radarr_data
@@ -1417,12 +1418,12 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(active_radarr_block.into()); app.push_navigation_stack(active_radarr_block.into());
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
EditIndexerHandler::new(ESC_KEY, &mut app, active_radarr_block, None).handle(); EditIndexerHandler::new(ESC_KEY, &mut app, active_radarr_block, None).handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.data.radarr_data.edit_indexer_modal, app.data.radarr_data.edit_indexer_modal,
Some(EditIndexerModal::default()) Some(EditIndexerModal::default())
@@ -1597,7 +1598,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new( EditIndexerHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::EditIndexerNameInput, ActiveRadarrBlock::EditIndexerNameInput,
None, None,
@@ -1613,7 +1614,7 @@ mod tests {
.unwrap() .unwrap()
.name .name
.text, .text,
"h" "a"
); );
} }
@@ -1624,7 +1625,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new( EditIndexerHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::EditIndexerUrlInput, ActiveRadarrBlock::EditIndexerUrlInput,
None, None,
@@ -1640,7 +1641,7 @@ mod tests {
.unwrap() .unwrap()
.url .url
.text, .text,
"h" "a"
); );
} }
@@ -1651,7 +1652,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new( EditIndexerHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::EditIndexerApiKeyInput, ActiveRadarrBlock::EditIndexerApiKeyInput,
None, None,
@@ -1667,7 +1668,7 @@ mod tests {
.unwrap() .unwrap()
.api_key .api_key
.text, .text,
"h" "a"
); );
} }
@@ -1678,7 +1679,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new( EditIndexerHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::EditIndexerSeedRatioInput, ActiveRadarrBlock::EditIndexerSeedRatioInput,
None, None,
@@ -1694,7 +1695,7 @@ mod tests {
.unwrap() .unwrap()
.seed_ratio .seed_ratio
.text, .text,
"h" "a"
); );
} }
@@ -1705,7 +1706,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new( EditIndexerHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::EditIndexerTagsInput, ActiveRadarrBlock::EditIndexerTagsInput,
None, None,
@@ -1721,7 +1722,7 @@ mod tests {
.unwrap() .unwrap()
.tags .tags
.text, .text,
"h" "a"
); );
} }
@@ -1793,6 +1794,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_edit_indexer_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditIndexerHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_build_edit_indexer_params() { fn test_build_edit_indexer_params() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -7,7 +6,9 @@ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS, ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS,
}; };
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys}; use crate::{
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
#[cfg(test)] #[cfg(test)]
#[path = "edit_indexer_settings_handler_tests.rs"] #[path = "edit_indexer_settings_handler_tests.rs"]
@@ -37,6 +38,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
INDEXER_SETTINGS_BLOCKS.contains(&active_block) INDEXER_SETTINGS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -207,7 +212,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
self.app.push_navigation_stack( self.app.push_navigation_stack(
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into(), ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into(),
); );
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags => { ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags => {
let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap(); let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap();
@@ -224,7 +229,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
} }
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => { ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput ActiveRadarrBlock::IndexerSettingsMinimumAgeInput
| ActiveRadarrBlock::IndexerSettingsRetentionInput | ActiveRadarrBlock::IndexerSettingsRetentionInput
@@ -244,7 +249,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
} }
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => { ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ => self.app.pop_navigation_stack(), _ => self.app.pop_navigation_stack(),
} }
@@ -269,7 +274,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
ActiveRadarrBlock::AllIndexerSettingsPrompt => { ActiveRadarrBlock::AllIndexerSettingsPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::IndexerSettingsConfirmPrompt == ActiveRadarrBlock::IndexerSettingsConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, self.key)
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some( self.app.data.radarr_data.prompt_confirm_action = Some(
@@ -1,6 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -602,7 +603,7 @@ mod tests {
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into() ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into()
); );
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
} }
#[test] #[test]
@@ -716,7 +717,7 @@ mod tests {
#[test] #[test]
fn test_edit_indexer_settings_whitelisted_subtitle_tags_input_submit() { fn test_edit_indexer_settings_whitelisted_subtitle_tags_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.indexer_settings = Some(IndexerSettings { app.data.radarr_data.indexer_settings = Some(IndexerSettings {
whitelisted_hardcoded_subs: "Test tags".into(), whitelisted_hardcoded_subs: "Test tags".into(),
..IndexerSettings::default() ..IndexerSettings::default()
@@ -734,7 +735,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.radarr_data .radarr_data
@@ -814,7 +815,7 @@ mod tests {
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into(), ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into(),
); );
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
IndexerSettingsHandler::new( IndexerSettingsHandler::new(
ESC_KEY, ESC_KEY,
@@ -825,7 +826,7 @@ mod tests {
.handle(); .handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into());
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.data.radarr_data.indexer_settings, app.data.radarr_data.indexer_settings,
Some(IndexerSettings::default()) Some(IndexerSettings::default())
@@ -907,7 +908,7 @@ mod tests {
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::new( IndexerSettingsHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
None, None,
@@ -923,7 +924,7 @@ mod tests {
.unwrap() .unwrap()
.whitelisted_hardcoded_subs .whitelisted_hardcoded_subs
.text, .text,
"h" "a"
); );
} }
@@ -970,6 +971,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_indexer_settings_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_build_edit_indexer_settings_body() { fn test_build_edit_indexer_settings_body() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -633,6 +633,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_indexers_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_extract_indexer_id() { fn test_extract_indexer_id() {
let mut app = App::test_default(); let mut app = App::test_default();
+10 -7
View File
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
use crate::handlers::radarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; use crate::handlers::radarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler;
@@ -15,6 +13,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
use crate::models::servarr_models::Indexer; use crate::models::servarr_models::Indexer;
use crate::models::BlockSelectionState; use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
mod edit_indexer_handler; mod edit_indexer_handler;
mod edit_indexer_settings_handler; mod edit_indexer_settings_handler;
@@ -70,6 +69,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
|| INDEXERS_BLOCKS.contains(&active_block) || INDEXERS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -169,20 +172,20 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
let key = self.key; let key = self.key;
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::Indexers => match self.key { ActiveRadarrBlock::Indexers => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ if key == DEFAULT_KEYBINDINGS.test.key => { _ if matches_key!(test, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::TestIndexer.into()); .push_navigation_stack(ActiveRadarrBlock::TestIndexer.into());
} }
_ if key == DEFAULT_KEYBINDINGS.test_all.key => { _ if matches_key!(test_all, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::TestAllIndexers.into()); .push_navigation_stack(ActiveRadarrBlock::TestAllIndexers.into());
} }
_ if key == DEFAULT_KEYBINDINGS.settings.key => { _ if matches_key!(settings, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
@@ -192,7 +195,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
_ => (), _ => (),
}, },
ActiveRadarrBlock::DeleteIndexerPrompt => { ActiveRadarrBlock::DeleteIndexerPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteIndexer(self.extract_indexer_id())); Some(RadarrEvent::DeleteIndexer(self.extract_indexer_id()));
@@ -48,6 +48,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl
active_block == ActiveRadarrBlock::TestAllIndexers active_block == ActiveRadarrBlock::TestAllIndexers
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -7,6 +7,7 @@ mod tests {
use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
mod test_handle_esc { mod test_handle_esc {
@@ -48,6 +49,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_test_all_indexers_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = TestAllIndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_test_all_indexers_handler_is_not_ready_when_loading() { fn test_test_all_indexers_handler_is_not_ready_when_loading() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::{ use crate::models::radarr_models::{
@@ -11,7 +10,9 @@ use crate::models::servarr_data::radarr::radarr_data::{
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
use crate::models::{BlockSelectionState, Scrollable}; use crate::models::{BlockSelectionState, Scrollable};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, App, Key}; use crate::{
handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, matches_key, App, Key,
};
#[cfg(test)] #[cfg(test)]
#[path = "add_movie_handler_tests.rs"] #[path = "add_movie_handler_tests.rs"]
@@ -132,6 +133,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
ADD_MOVIE_BLOCKS.contains(&active_block) ADD_MOVIE_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -404,7 +409,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchResults.into()); .push_navigation_stack(ActiveRadarrBlock::AddMovieSearchResults.into());
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ if self.active_radarr_block == ActiveRadarrBlock::AddMovieSearchResults _ if self.active_radarr_block == ActiveRadarrBlock::AddMovieSearchResults
&& self.app.data.radarr_data.add_searched_movies.is_some() => && self.app.data.radarr_data.add_searched_movies.is_some() =>
@@ -468,7 +473,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
) )
.into(), .into(),
); );
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
_ => (), _ => (),
} }
@@ -479,7 +484,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
| ActiveRadarrBlock::AddMovieSelectRootFolder => self.app.pop_navigation_stack(), | ActiveRadarrBlock::AddMovieSelectRootFolder => self.app.pop_navigation_stack(),
ActiveRadarrBlock::AddMovieTagsInput => { ActiveRadarrBlock::AddMovieTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ => (), _ => (),
} }
@@ -490,12 +495,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
ActiveRadarrBlock::AddMovieSearchInput => { ActiveRadarrBlock::AddMovieSearchInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.data.radarr_data.add_movie_search = None; self.app.data.radarr_data.add_movie_search = None;
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
ActiveRadarrBlock::AddMovieSearchResults | ActiveRadarrBlock::AddMovieEmptySearchResults => { ActiveRadarrBlock::AddMovieSearchResults | ActiveRadarrBlock::AddMovieEmptySearchResults => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.data.radarr_data.add_searched_movies = None; self.app.data.radarr_data.add_searched_movies = None;
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
ActiveRadarrBlock::AddMoviePrompt => { ActiveRadarrBlock::AddMoviePrompt => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
@@ -509,7 +514,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
| ActiveRadarrBlock::AddMovieSelectRootFolder => self.app.pop_navigation_stack(), | ActiveRadarrBlock::AddMovieSelectRootFolder => self.app.pop_navigation_stack(),
ActiveRadarrBlock::AddMovieTagsInput => { ActiveRadarrBlock::AddMovieTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ => (), _ => (),
} }
@@ -542,7 +547,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
ActiveRadarrBlock::AddMoviePrompt => { ActiveRadarrBlock::AddMoviePrompt => {
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::AddMovieConfirmPrompt == ActiveRadarrBlock::AddMovieConfirmPrompt
&& key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, key)
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
@@ -782,7 +782,7 @@ mod tests {
#[test] #[test]
fn test_add_movie_search_input_submit() { fn test_add_movie_search_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.add_movie_search = Some("test".into()); app.data.radarr_data.add_movie_search = Some("test".into());
AddMovieHandler::new( AddMovieHandler::new(
@@ -793,7 +793,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::AddMovieSearchResults.into() ActiveRadarrBlock::AddMovieSearchResults.into()
@@ -805,7 +805,7 @@ mod tests {
let mut app = App::test_default(); let mut app = App::test_default();
app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default()); app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default());
app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
AddMovieHandler::new( AddMovieHandler::new(
SUBMIT_KEY, SUBMIT_KEY,
@@ -815,7 +815,7 @@ mod tests {
) )
.handle(); .handle();
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::AddMovieSearchInput.into() ActiveRadarrBlock::AddMovieSearchInput.into()
@@ -1093,7 +1093,7 @@ mod tests {
assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
if selected_block == ActiveRadarrBlock::AddMovieTagsInput { if selected_block == ActiveRadarrBlock::AddMovieTagsInput {
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
} }
} }
@@ -1126,7 +1126,7 @@ mod tests {
); );
if active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput { if active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput {
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
} }
} }
} }
@@ -1149,7 +1149,7 @@ mod tests {
let mut app = App::test_default(); let mut app = App::test_default();
app.is_loading = is_ready; app.is_loading = is_ready;
app.data.radarr_data = create_test_radarr_data(); app.data.radarr_data = create_test_radarr_data();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into());
AddMovieHandler::new( AddMovieHandler::new(
@@ -1160,7 +1160,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
assert_eq!(app.data.radarr_data.add_movie_search, None); assert_eq!(app.data.radarr_data.add_movie_search, None);
} }
@@ -1169,7 +1169,7 @@ mod tests {
fn test_add_movie_input_esc() { fn test_add_movie_input_esc() {
let mut app = App::test_default(); let mut app = App::test_default();
app.data.radarr_data = create_test_radarr_data(); app.data.radarr_data = create_test_radarr_data();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AddMovieTagsInput.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMovieTagsInput.into());
@@ -1181,7 +1181,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::AddMoviePrompt.into() ActiveRadarrBlock::AddMoviePrompt.into()
@@ -1213,7 +1213,7 @@ mod tests {
ActiveRadarrBlock::AddMovieSearchInput.into() ActiveRadarrBlock::AddMovieSearchInput.into()
); );
assert!(app.data.radarr_data.add_searched_movies.is_none()); assert!(app.data.radarr_data.add_searched_movies.is_none());
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
} }
#[test] #[test]
@@ -1259,7 +1259,7 @@ mod tests {
fn test_add_movie_tags_input_esc() { fn test_add_movie_tags_input_esc() {
let mut app = App::test_default(); let mut app = App::test_default();
app.data.radarr_data = create_test_radarr_data(); app.data.radarr_data = create_test_radarr_data();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::AddMovieTagsInput.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMovieTagsInput.into());
@@ -1271,7 +1271,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::AddMoviePrompt.into() ActiveRadarrBlock::AddMoviePrompt.into()
@@ -1395,7 +1395,7 @@ mod tests {
app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default()); app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default());
AddMovieHandler::new( AddMovieHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::AddMovieSearchInput, ActiveRadarrBlock::AddMovieSearchInput,
None, None,
@@ -1404,7 +1404,7 @@ mod tests {
assert_str_eq!( assert_str_eq!(
app.data.radarr_data.add_movie_search.as_ref().unwrap().text, app.data.radarr_data.add_movie_search.as_ref().unwrap().text,
"h" "a"
); );
} }
@@ -1414,7 +1414,7 @@ mod tests {
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default()); app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
AddMovieHandler::new( AddMovieHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::AddMovieTagsInput, ActiveRadarrBlock::AddMovieTagsInput,
None, None,
@@ -1430,7 +1430,7 @@ mod tests {
.unwrap() .unwrap()
.tags .tags
.text, .text,
"h" "a"
); );
} }
@@ -1523,6 +1523,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_add_movie_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = AddMovieHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_add_movie_search_no_panic_on_none_search_result() { fn test_add_movie_search_no_panic_on_none_search_result() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,7 +1,7 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::matches_key;
use crate::models::radarr_models::DeleteMovieParams; use crate::models::radarr_models::DeleteMovieParams;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
@@ -37,6 +37,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
DELETE_MOVIE_BLOCKS.contains(&active_block) DELETE_MOVIE_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -122,7 +126,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
if self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt if self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt
&& self.app.data.radarr_data.selected_block.get_active_block() && self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::DeleteMovieConfirmPrompt == ActiveRadarrBlock::DeleteMovieConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, self.key)
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
@@ -1,6 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -313,6 +314,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_delete_movie_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DeleteMovieHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_build_delete_movie_params() { fn test_build_delete_movie_params() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -7,7 +6,7 @@ use crate::models::servarr_data::radarr::modals::EditMovieModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS};
use crate::models::Scrollable; use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "edit_movie_handler_tests.rs"] #[path = "edit_movie_handler_tests.rs"]
@@ -68,6 +67,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
EDIT_MOVIE_BLOCKS.contains(&active_block) EDIT_MOVIE_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -290,7 +293,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
) )
.into(), .into(),
); );
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
ActiveRadarrBlock::EditMovieToggleMonitored => { ActiveRadarrBlock::EditMovieToggleMonitored => {
self self
@@ -319,7 +322,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
| ActiveRadarrBlock::EditMovieSelectQualityProfile => self.app.pop_navigation_stack(), | ActiveRadarrBlock::EditMovieSelectQualityProfile => self.app.pop_navigation_stack(),
ActiveRadarrBlock::EditMoviePathInput | ActiveRadarrBlock::EditMovieTagsInput => { ActiveRadarrBlock::EditMoviePathInput | ActiveRadarrBlock::EditMovieTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ => (), _ => (),
} }
@@ -329,7 +332,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::EditMovieTagsInput | ActiveRadarrBlock::EditMoviePathInput => { ActiveRadarrBlock::EditMovieTagsInput | ActiveRadarrBlock::EditMoviePathInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
ActiveRadarrBlock::EditMoviePrompt => { ActiveRadarrBlock::EditMoviePrompt => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
@@ -376,7 +379,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
ActiveRadarrBlock::EditMoviePrompt => { ActiveRadarrBlock::EditMoviePrompt => {
if self.app.data.radarr_data.selected_block.get_active_block() if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditMovieConfirmPrompt == ActiveRadarrBlock::EditMovieConfirmPrompt
&& key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, key)
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
@@ -2,6 +2,7 @@
mod tests { mod tests {
use bimap::BiMap; use bimap::BiMap;
use pretty_assertions::assert_str_eq; use pretty_assertions::assert_str_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -548,7 +549,7 @@ mod tests {
#[test] #[test]
fn test_edit_movie_path_input_submit() { fn test_edit_movie_path_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_movie_modal = Some(EditMovieModal { app.data.radarr_data.edit_movie_modal = Some(EditMovieModal {
path: "Test Path".into(), path: "Test Path".into(),
..EditMovieModal::default() ..EditMovieModal::default()
@@ -564,7 +565,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.radarr_data .radarr_data
@@ -583,7 +584,7 @@ mod tests {
#[test] #[test]
fn test_edit_movie_tags_input_submit() { fn test_edit_movie_tags_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.radarr_data.edit_movie_modal = Some(EditMovieModal { app.data.radarr_data.edit_movie_modal = Some(EditMovieModal {
tags: "Test Tags".into(), tags: "Test Tags".into(),
..EditMovieModal::default() ..EditMovieModal::default()
@@ -599,7 +600,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.radarr_data .radarr_data
@@ -813,7 +814,7 @@ mod tests {
if selected_block == ActiveRadarrBlock::EditMoviePathInput if selected_block == ActiveRadarrBlock::EditMoviePathInput
|| selected_block == ActiveRadarrBlock::EditMovieTagsInput || selected_block == ActiveRadarrBlock::EditMovieTagsInput
{ {
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
} }
} }
@@ -851,7 +852,7 @@ mod tests {
.into() .into()
); );
assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
} }
#[rstest] #[rstest]
@@ -885,7 +886,7 @@ mod tests {
if active_radarr_block == ActiveRadarrBlock::EditMoviePathInput if active_radarr_block == ActiveRadarrBlock::EditMoviePathInput
|| active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput || active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput
{ {
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
} }
} }
} }
@@ -911,13 +912,13 @@ mod tests {
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.data.radarr_data = create_test_radarr_data(); app.data.radarr_data = create_test_radarr_data();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into());
app.push_navigation_stack(active_radarr_block.into()); app.push_navigation_stack(active_radarr_block.into());
EditMovieHandler::new(ESC_KEY, &mut app, active_radarr_block, None).handle(); EditMovieHandler::new(ESC_KEY, &mut app, active_radarr_block, None).handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::EditMoviePrompt.into() ActiveRadarrBlock::EditMoviePrompt.into()
@@ -1033,7 +1034,7 @@ mod tests {
app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default());
EditMovieHandler::new( EditMovieHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::EditMoviePathInput, ActiveRadarrBlock::EditMoviePathInput,
None, None,
@@ -1049,7 +1050,7 @@ mod tests {
.unwrap() .unwrap()
.path .path
.text, .text,
"h" "a"
); );
} }
@@ -1059,7 +1060,7 @@ mod tests {
app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default());
EditMovieHandler::new( EditMovieHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::EditMovieTagsInput, ActiveRadarrBlock::EditMovieTagsInput,
None, None,
@@ -1075,7 +1076,7 @@ mod tests {
.unwrap() .unwrap()
.tags .tags
.text, .text,
"h" "a"
); );
} }
@@ -1148,6 +1149,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_edit_movie_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditMovieHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_build_edit_movie_params() { fn test_build_edit_movie_params() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -9,6 +9,7 @@ mod tests {
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::radarr_handlers::library::{movies_sorting_options, LibraryHandler}; use crate::handlers::radarr_handlers::library::{movies_sorting_options, LibraryHandler};
use crate::handlers::radarr_handlers::radarr_handler_test_utils::utils::movie;
use crate::handlers::KeyEventHandler; use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::Movie; use crate::models::radarr_models::Movie;
use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::servarr_data::radarr::radarr_data::{
@@ -331,7 +332,7 @@ mod tests {
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::AddMovieSearchInput.into() ActiveRadarrBlock::AddMovieSearchInput.into()
); );
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
assert!(app.data.radarr_data.add_movie_search.is_some()); assert!(app.data.radarr_data.add_movie_search.is_some());
} }
@@ -355,7 +356,7 @@ mod tests {
.handle(); .handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(app.data.radarr_data.add_movie_search.is_none()); assert!(app.data.radarr_data.add_movie_search.is_none());
} }
@@ -391,6 +392,51 @@ mod tests {
assert!(app.data.radarr_data.edit_movie_modal.is_none()); assert!(app.data.radarr_data.edit_movie_modal.is_none());
} }
#[test]
fn test_toggle_monitoring_key() {
let mut app = App::test_default();
app.data.radarr_data = create_test_radarr_data();
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.is_routing = false;
LibraryHandler::new(
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
&mut app,
ActiveRadarrBlock::Movies,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
assert!(app.data.radarr_data.prompt_confirm);
assert!(app.is_routing);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::ToggleMovieMonitoring(0))
);
}
#[test]
fn test_toggle_monitoring_key_no_op_when_not_ready() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.is_routing = false;
LibraryHandler::new(
DEFAULT_KEYBINDINGS.toggle_monitoring.key,
&mut app,
ActiveRadarrBlock::Movies,
None,
)
.handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
assert!(!app.data.radarr_data.prompt_confirm);
assert!(app.data.radarr_data.prompt_confirm_action.is_none());
assert!(!app.is_routing);
}
#[test] #[test]
fn test_update_all_movies_key() { fn test_update_all_movies_key() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -580,6 +626,22 @@ mod tests {
); );
} }
#[test]
fn test_extract_movie_id() {
let mut app = App::test_default();
app.data.radarr_data.movies.set_items(vec![movie()]);
let movie_id = LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::Movies,
None,
)
.extract_movie_id();
assert_eq!(movie_id, 1);
}
#[test] #[test]
fn test_movies_sorting_options_title() { fn test_movies_sorting_options_title() {
let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| { let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| {
@@ -615,8 +677,13 @@ mod tests {
#[test] #[test]
fn test_movies_sorting_options_studio() { fn test_movies_sorting_options_studio() {
let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| {
|a, b| a.studio.to_lowercase().cmp(&b.studio.to_lowercase()); a.studio
.as_ref()
.unwrap_or(&String::new())
.to_lowercase()
.cmp(&b.studio.as_ref().unwrap_or(&String::new()).to_lowercase())
};
let mut expected_movies_vec = movies_vec(); let mut expected_movies_vec = movies_vec();
expected_movies_vec.sort_by(expected_cmp_fn); expected_movies_vec.sort_by(expected_cmp_fn);
@@ -777,6 +844,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_library_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_library_handler_not_ready_when_loading() { fn test_library_handler_not_ready_when_loading() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -837,7 +923,7 @@ mod tests {
name: "English".to_owned(), name: "English".to_owned(),
}, },
size_on_disk: 1024, size_on_disk: 1024,
studio: "Studio 1".to_owned(), studio: Some("Studio 1".to_owned()),
year: 2024, year: 2024,
monitored: false, monitored: false,
runtime: 12.into(), runtime: 12.into(),
@@ -854,7 +940,7 @@ mod tests {
name: "Chinese".to_owned(), name: "Chinese".to_owned(),
}, },
size_on_disk: 2048, size_on_disk: 2048,
studio: "Studio 2".to_owned(), studio: Some("Studio 2".to_owned()),
year: 1998, year: 1998,
monitored: false, monitored: false,
runtime: 60.into(), runtime: 60.into(),
@@ -871,7 +957,7 @@ mod tests {
name: "Japanese".to_owned(), name: "Japanese".to_owned(),
}, },
size_on_disk: 512, size_on_disk: 512,
studio: "studio 3".to_owned(), studio: Some("studio 3".to_owned()),
year: 1954, year: 1954,
monitored: true, monitored: true,
runtime: 120.into(), runtime: 120.into(),
+30 -9
View File
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
@@ -8,7 +7,6 @@ use crate::handlers::radarr_handlers::library::edit_movie_handler::EditMovieHand
use crate::handlers::radarr_handlers::library::movie_details_handler::MovieDetailsHandler; use crate::handlers::radarr_handlers::library::movie_details_handler::MovieDetailsHandler;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::handle_table_events;
use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::TableHandlingConfig;
use crate::models::radarr_models::Movie; use crate::models::radarr_models::Movie;
use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::servarr_data::radarr::radarr_data::{
@@ -17,6 +15,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, HorizontallyScrollableText}; use crate::models::{BlockSelectionState, HorizontallyScrollableText};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
mod add_movie_handler; mod add_movie_handler;
mod delete_movie_handler; mod delete_movie_handler;
@@ -36,6 +35,9 @@ pub(super) struct LibraryHandler<'a, 'b> {
impl LibraryHandler<'_, '_> { impl LibraryHandler<'_, '_> {
handle_table_events!(self, movies, self.app.data.radarr_data.movies, Movie); handle_table_events!(self, movies, self.app.data.radarr_data.movies, Movie);
fn extract_movie_id(&self) -> i64 {
self.app.data.radarr_data.movies.current_selection().id
}
} }
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, 'b> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, 'b> {
@@ -81,6 +83,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
|| LIBRARY_BLOCKS.contains(&active_block) || LIBRARY_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -161,7 +167,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
let key = self.key; let key = self.key;
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::Movies => match self.key { ActiveRadarrBlock::Movies => match self.key {
_ if key == DEFAULT_KEYBINDINGS.edit.key => { _ if matches_key!(edit, key) => {
self.app.push_navigation_stack( self.app.push_navigation_stack(
( (
ActiveRadarrBlock::EditMoviePrompt, ActiveRadarrBlock::EditMoviePrompt,
@@ -173,25 +179,34 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
self.app.data.radarr_data.selected_block = self.app.data.radarr_data.selected_block =
BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS);
} }
_ if key == DEFAULT_KEYBINDINGS.add.key => { _ if matches_key!(add, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); .push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into());
self.app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default()); self.app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default());
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
_ if key == DEFAULT_KEYBINDINGS.update.key => { _ if matches_key!(toggle_monitoring, key) => {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::ToggleMovieMonitoring(self.extract_movie_id()));
self
.app
.pop_and_push_navigation_stack(self.active_radarr_block.into());
}
_ if matches_key!(update, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into());
} }
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ => (), _ => (),
}, },
ActiveRadarrBlock::UpdateAllMoviesPrompt => { ActiveRadarrBlock::UpdateAllMoviesPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies); self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies);
@@ -220,7 +235,13 @@ fn movies_sorting_options() -> Vec<SortOption<Movie>> {
}, },
SortOption { SortOption {
name: "Studio", name: "Studio",
cmp_fn: Some(|a, b| a.studio.to_lowercase().cmp(&b.studio.to_lowercase())), cmp_fn: Some(|a, b| {
a.studio
.as_ref()
.unwrap_or(&String::new())
.to_lowercase()
.cmp(&b.studio.as_ref().unwrap_or(&String::new()).to_lowercase())
}),
}, },
SortOption { SortOption {
name: "Runtime", name: "Runtime",
@@ -1,9 +1,7 @@
use serde_json::Number; use serde_json::Number;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::{ use crate::models::radarr_models::{
@@ -16,6 +14,7 @@ use crate::models::servarr_models::Language;
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, Scrollable}; use crate::models::{BlockSelectionState, Scrollable};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "movie_details_handler_tests.rs"] #[path = "movie_details_handler_tests.rs"]
@@ -136,6 +135,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
MOVIE_DETAILS_BLOCKS.contains(&active_block) MOVIE_DETAILS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -245,13 +248,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
| ActiveRadarrBlock::Cast | ActiveRadarrBlock::Cast
| ActiveRadarrBlock::Crew | ActiveRadarrBlock::Crew
| ActiveRadarrBlock::ManualSearch => match self.key { | ActiveRadarrBlock::ManualSearch => match self.key {
_ if self.key == DEFAULT_KEYBINDINGS.left.key => { _ if matches_key!(left, self.key) => {
self.app.data.radarr_data.movie_info_tabs.previous(); self.app.data.radarr_data.movie_info_tabs.previous();
self.app.pop_and_push_navigation_stack( self.app.pop_and_push_navigation_stack(
self.app.data.radarr_data.movie_info_tabs.get_active_route(), self.app.data.radarr_data.movie_info_tabs.get_active_route(),
); );
} }
_ if self.key == DEFAULT_KEYBINDINGS.right.key => { _ if matches_key!(right, self.key) => {
self.app.data.radarr_data.movie_info_tabs.next(); self.app.data.radarr_data.movie_info_tabs.next();
self.app.pop_and_push_navigation_stack( self.app.pop_and_push_navigation_stack(
self.app.data.radarr_data.movie_info_tabs.get_active_route(), self.app.data.radarr_data.movie_info_tabs.get_active_route(),
@@ -332,12 +335,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
| ActiveRadarrBlock::Cast | ActiveRadarrBlock::Cast
| ActiveRadarrBlock::Crew | ActiveRadarrBlock::Crew
| ActiveRadarrBlock::ManualSearch => match self.key { | ActiveRadarrBlock::ManualSearch => match self.key {
_ if key == DEFAULT_KEYBINDINGS.auto_search.key => { _ if matches_key!(auto_search, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::AutomaticallySearchMoviePrompt.into()); .push_navigation_stack(ActiveRadarrBlock::AutomaticallySearchMoviePrompt.into());
} }
_ if key == DEFAULT_KEYBINDINGS.edit.key => { _ if matches_key!(edit, key) => {
self.app.push_navigation_stack( self.app.push_navigation_stack(
( (
ActiveRadarrBlock::EditMoviePrompt, ActiveRadarrBlock::EditMoviePrompt,
@@ -349,35 +352,33 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
self.app.data.radarr_data.selected_block = self.app.data.radarr_data.selected_block =
BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS);
} }
_ if key == DEFAULT_KEYBINDINGS.update.key => { _ if matches_key!(update, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::UpdateAndScanPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::UpdateAndScanPrompt.into());
} }
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self self
.app .app
.pop_and_push_navigation_stack(self.active_radarr_block.into()); .pop_and_push_navigation_stack(self.active_radarr_block.into());
} }
_ => (), _ => (),
}, },
ActiveRadarrBlock::AutomaticallySearchMoviePrompt ActiveRadarrBlock::AutomaticallySearchMoviePrompt if matches_key!(confirm, key) => {
if key == DEFAULT_KEYBINDINGS.confirm.key =>
{
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::TriggerAutomaticSearch(self.extract_movie_id())); Some(RadarrEvent::TriggerAutomaticSearch(self.extract_movie_id()));
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
} }
ActiveRadarrBlock::UpdateAndScanPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => { ActiveRadarrBlock::UpdateAndScanPrompt if matches_key!(confirm, key) => {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::UpdateAndScan(self.extract_movie_id())); Some(RadarrEvent::UpdateAndScan(self.extract_movie_id()));
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
} }
ActiveRadarrBlock::ManualSearchConfirmPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => { ActiveRadarrBlock::ManualSearchConfirmPrompt if matches_key!(confirm, key) => {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease( self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease(
self.build_radarr_release_download_body(), self.build_radarr_release_download_body(),
@@ -1042,6 +1042,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_movie_details_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = MovieDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[rstest] #[rstest]
fn test_movie_details_handler_is_not_ready_when_loading( fn test_movie_details_handler_is_not_ready_when_loading(
#[values( #[values(
+9 -6
View File
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::radarr_handlers::blocklist::BlocklistHandler; use crate::handlers::radarr_handlers::blocklist::BlocklistHandler;
use crate::handlers::radarr_handlers::collections::CollectionsHandler; use crate::handlers::radarr_handlers::collections::CollectionsHandler;
use crate::handlers::radarr_handlers::downloads::DownloadsHandler; use crate::handlers::radarr_handlers::downloads::DownloadsHandler;
@@ -8,7 +7,7 @@ use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
use crate::handlers::radarr_handlers::system::SystemHandler; use crate::handlers::radarr_handlers::system::SystemHandler;
use crate::handlers::KeyEventHandler; use crate::handlers::KeyEventHandler;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::{App, Key}; use crate::{matches_key, App, Key};
mod blocklist; mod blocklist;
mod collections; mod collections;
@@ -65,6 +64,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
true true
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -109,11 +112,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
pub fn handle_change_tab_left_right_keys(app: &mut App<'_>, key: Key) { pub fn handle_change_tab_left_right_keys(app: &mut App<'_>, key: Key) {
let key_ref = key; let key_ref = key;
match key_ref { match key_ref {
_ if key == DEFAULT_KEYBINDINGS.left.key => { _ if matches_key!(left, key, app.ignore_special_keys_for_textbox_input) => {
app.data.radarr_data.main_tabs.previous(); app.data.radarr_data.main_tabs.previous();
app.pop_and_push_navigation_stack(app.data.radarr_data.main_tabs.get_active_route()); app.pop_and_push_navigation_stack(app.data.radarr_data.main_tabs.get_active_route());
} }
_ if key == DEFAULT_KEYBINDINGS.right.key => { _ if matches_key!(right, key, app.ignore_special_keys_for_textbox_input) => {
app.data.radarr_data.main_tabs.next(); app.data.radarr_data.main_tabs.next();
app.pop_and_push_navigation_stack(app.data.radarr_data.main_tabs.get_active_route()); app.pop_and_push_navigation_stack(app.data.radarr_data.main_tabs.get_active_route());
} }
@@ -139,7 +142,7 @@ macro_rules! search_table {
}; };
$app.data.radarr_data.is_searching = false; $app.data.radarr_data.is_searching = false;
$app.should_ignore_quit_key = false; $app.ignore_special_keys_for_textbox_input = false;
if search_index.is_some() { if search_index.is_some() {
$app.pop_navigation_stack(); $app.pop_navigation_stack();
@@ -166,7 +169,7 @@ macro_rules! search_table {
}; };
$app.data.radarr_data.is_searching = false; $app.data.radarr_data.is_searching = false;
$app.should_ignore_quit_key = false; $app.ignore_special_keys_for_textbox_input = false;
if search_index.is_some() { if search_index.is_some() {
$app.pop_navigation_stack(); $app.pop_navigation_stack();
@@ -275,7 +275,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
audio_stream_count: 1, audio_stream_count: 1,
video_bit_depth: 10, video_bit_depth: 10,
video_bitrate: 0, video_bitrate: 0,
video_codec: "x265".to_owned(), video_codec: Some("x265".to_owned()),
video_fps: Number::from_f64(23.976).unwrap(), video_fps: Number::from_f64(23.976).unwrap(),
resolution: "1920x804".to_owned(), resolution: "1920x804".to_owned(),
run_time: "2:00:00".to_owned(), run_time: "2:00:00".to_owned(),
@@ -327,7 +327,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
status: "Downloaded".to_owned(), status: "Downloaded".to_owned(),
overview: "Blah blah blah".to_owned(), overview: "Blah blah blah".to_owned(),
path: "/nfs/movies".to_owned(), path: "/nfs/movies".to_owned(),
studio: "21st Century Alex".to_owned(), studio: Some("21st Century Alex".to_owned()),
genres: genres(), genres: genres(),
year: 2023, year: 2023,
monitored: true, monitored: true,
@@ -46,6 +46,76 @@ mod tests {
assert_eq!(app.get_current_route(), right_block.into()); assert_eq!(app.get_current_route(), right_block.into());
} }
#[rstest]
#[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)]
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)]
#[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)]
#[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)]
#[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)]
#[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)]
#[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)]
fn test_radarr_handler_change_tab_left_right_keys_alt_navigation(
#[case] index: usize,
#[case] left_block: ActiveRadarrBlock,
#[case] right_block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = false;
app.data.radarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.alt.unwrap());
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
left_block.into()
);
assert_eq!(app.get_current_route(), left_block.into());
app.data.radarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.alt.unwrap());
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
right_block.into()
);
assert_eq!(app.get_current_route(), right_block.into());
}
#[rstest]
#[case(0, ActiveRadarrBlock::Movies)]
#[case(1, ActiveRadarrBlock::Collections)]
#[case(2, ActiveRadarrBlock::Downloads)]
#[case(3, ActiveRadarrBlock::Blocklist)]
#[case(4, ActiveRadarrBlock::RootFolders)]
#[case(5, ActiveRadarrBlock::Indexers)]
#[case(6, ActiveRadarrBlock::System)]
fn test_radarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key(
#[case] index: usize,
#[case] block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(block.into());
app.data.radarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.alt.unwrap());
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
block.into()
);
assert_eq!(app.get_current_route(), block.into());
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.alt.unwrap());
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
block.into()
);
assert_eq!(app.get_current_route(), block.into());
}
#[rstest] #[rstest]
fn test_delegates_system_blocks_to_system_handler( fn test_delegates_system_blocks_to_system_handler(
#[values( #[values(
@@ -217,6 +287,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_radarr_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = RadarrHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::Movies,
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_radarr_handler_is_ready() { fn test_radarr_handler_is_ready() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
@@ -8,7 +7,9 @@ use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_F
use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; use crate::models::servarr_models::{AddRootFolderBody, RootFolder};
use crate::models::HorizontallyScrollableText; use crate::models::HorizontallyScrollableText;
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys}; use crate::{
handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
#[cfg(test)] #[cfg(test)]
#[path = "root_folders_handler_tests.rs"] #[path = "root_folders_handler_tests.rs"]
@@ -68,6 +69,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
ROOT_FOLDERS_BLOCKS.contains(&active_block) ROOT_FOLDERS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -168,7 +173,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
self.build_add_root_folder_body(), self.build_add_root_folder_body(),
)); ));
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
} }
_ => (), _ => (),
@@ -181,7 +186,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.data.radarr_data.edit_root_folder = None; self.app.data.radarr_data.edit_root_folder = None;
self.app.data.radarr_data.prompt_confirm = false; self.app.data.radarr_data.prompt_confirm = false;
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
ActiveRadarrBlock::DeleteRootFolderPrompt => { ActiveRadarrBlock::DeleteRootFolderPrompt => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
@@ -195,15 +200,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
let key = self.key; let key = self.key;
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::RootFolders => match self.key { ActiveRadarrBlock::RootFolders => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ if key == DEFAULT_KEYBINDINGS.add.key => { _ if matches_key!(add, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into()); .push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into());
self.app.data.radarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); self.app.data.radarr_data.edit_root_folder = Some(HorizontallyScrollableText::default());
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
_ => (), _ => (),
}, },
@@ -215,7 +220,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
) )
} }
ActiveRadarrBlock::DeleteRootFolderPrompt => { ActiveRadarrBlock::DeleteRootFolderPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteRootFolder(self.extract_root_folder_id())); Some(RadarrEvent::DeleteRootFolder(self.extract_root_folder_id()));
@@ -1,6 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -262,7 +263,7 @@ mod tests {
.set_items(vec![RootFolder::default()]); .set_items(vec![RootFolder::default()]);
app.data.radarr_data.edit_root_folder = Some("Test".into()); app.data.radarr_data.edit_root_folder = Some("Test".into());
app.data.radarr_data.prompt_confirm = true; app.data.radarr_data.prompt_confirm = true;
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into()); app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into()); app.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into());
@@ -275,7 +276,7 @@ mod tests {
.handle(); .handle();
assert!(app.data.radarr_data.prompt_confirm); assert!(app.data.radarr_data.prompt_confirm);
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.data.radarr_data.prompt_confirm_action, app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::AddRootFolder(expected_add_root_folder_body)) Some(RadarrEvent::AddRootFolder(expected_add_root_folder_body))
@@ -291,7 +292,7 @@ mod tests {
let mut app = App::test_default(); let mut app = App::test_default();
app.data.radarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); app.data.radarr_data.edit_root_folder = Some(HorizontallyScrollableText::default());
app.data.radarr_data.prompt_confirm = false; app.data.radarr_data.prompt_confirm = false;
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into()); app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into()); app.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into());
@@ -304,7 +305,7 @@ mod tests {
.handle(); .handle();
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
assert!(app.data.radarr_data.prompt_confirm_action.is_none()); assert!(app.data.radarr_data.prompt_confirm_action.is_none());
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
@@ -406,7 +407,7 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into()); app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into()); app.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into());
app.data.radarr_data.edit_root_folder = Some("/nfs/test".into()); app.data.radarr_data.edit_root_folder = Some("/nfs/test".into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
RootFoldersHandler::new( RootFoldersHandler::new(
ESC_KEY, ESC_KEY,
@@ -423,7 +424,7 @@ mod tests {
assert!(app.data.radarr_data.edit_root_folder.is_none()); assert!(app.data.radarr_data.edit_root_folder.is_none());
assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.prompt_confirm);
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
} }
#[rstest] #[rstest]
@@ -472,7 +473,7 @@ mod tests {
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::AddRootFolderPrompt.into() ActiveRadarrBlock::AddRootFolderPrompt.into()
); );
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
assert!(app.data.radarr_data.edit_root_folder.is_some()); assert!(app.data.radarr_data.edit_root_folder.is_some());
} }
@@ -499,7 +500,7 @@ mod tests {
app.get_current_route(), app.get_current_route(),
ActiveRadarrBlock::RootFolders.into() ActiveRadarrBlock::RootFolders.into()
); );
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(app.data.radarr_data.edit_root_folder.is_none()); assert!(app.data.radarr_data.edit_root_folder.is_none());
} }
@@ -589,7 +590,7 @@ mod tests {
app.data.radarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); app.data.radarr_data.edit_root_folder = Some(HorizontallyScrollableText::default());
RootFoldersHandler::new( RootFoldersHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveRadarrBlock::AddRootFolderPrompt, ActiveRadarrBlock::AddRootFolderPrompt,
None, None,
@@ -598,7 +599,7 @@ mod tests {
assert_str_eq!( assert_str_eq!(
app.data.radarr_data.edit_root_folder.as_ref().unwrap().text, app.data.radarr_data.edit_root_folder.as_ref().unwrap().text,
"h" "a"
); );
} }
@@ -644,6 +645,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_root_folders_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_build_add_root_folder_body() { fn test_build_add_root_folder_body() {
let mut app = App::test_default(); let mut app = App::test_default();
+10 -6
View File
@@ -1,9 +1,9 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler; use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler;
use crate::handlers::{handle_clear_errors, KeyEventHandler}; use crate::handlers::{handle_clear_errors, KeyEventHandler};
use crate::matches_key;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::Scrollable; use crate::models::Scrollable;
@@ -35,6 +35,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b
SystemDetailsHandler::accepts(active_block) || active_block == ActiveRadarrBlock::System SystemDetailsHandler::accepts(active_block) || active_block == ActiveRadarrBlock::System
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -86,15 +90,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b
if self.active_radarr_block == ActiveRadarrBlock::System { if self.active_radarr_block == ActiveRadarrBlock::System {
let key = self.key; let key = self.key;
match self.key { match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ if key == DEFAULT_KEYBINDINGS.events.key => { _ if matches_key!(events, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::SystemQueuedEvents.into()); .push_navigation_stack(ActiveRadarrBlock::SystemQueuedEvents.into());
} }
_ if key == DEFAULT_KEYBINDINGS.logs.key => { _ if matches_key!(logs, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::SystemLogs.into()); .push_navigation_stack(ActiveRadarrBlock::SystemLogs.into());
@@ -106,12 +110,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b
.set_items(self.app.data.radarr_data.logs.items.to_vec()); .set_items(self.app.data.radarr_data.logs.items.to_vec());
self.app.data.radarr_data.log_details.scroll_to_bottom(); self.app.data.radarr_data.log_details.scroll_to_bottom();
} }
_ if key == DEFAULT_KEYBINDINGS.tasks.key => { _ if matches_key!(tasks, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); .push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
} }
_ if key == DEFAULT_KEYBINDINGS.update.key => { _ if matches_key!(update, key) => {
self self
.app .app
.push_navigation_stack(ActiveRadarrBlock::SystemUpdates.into()); .push_navigation_stack(ActiveRadarrBlock::SystemUpdates.into());
@@ -1,7 +1,7 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::matches_key;
use crate::models::radarr_models::RadarrTaskName; use crate::models::radarr_models::RadarrTaskName;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS};
use crate::models::stateful_list::StatefulList; use crate::models::stateful_list::StatefulList;
@@ -36,6 +36,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
SYSTEM_DETAILS_BLOCKS.contains(&active_block) SYSTEM_DETAILS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -114,7 +118,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::SystemLogs => match self.key { ActiveRadarrBlock::SystemLogs => match self.key {
_ if key == DEFAULT_KEYBINDINGS.left.key => { _ if matches_key!(left, key) => {
self self
.app .app
.data .data
@@ -124,7 +128,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
.iter() .iter()
.for_each(|log| log.scroll_right()); .for_each(|log| log.scroll_right());
} }
_ if key == DEFAULT_KEYBINDINGS.right.key => { _ if matches_key!(right, key) => {
self self
.app .app
.data .data
@@ -178,14 +182,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
} }
fn handle_char_key_event(&mut self) { fn handle_char_key_event(&mut self) {
if SYSTEM_DETAILS_BLOCKS.contains(&self.active_radarr_block) if SYSTEM_DETAILS_BLOCKS.contains(&self.active_radarr_block) && matches_key!(refresh, self.key)
&& self.key == DEFAULT_KEYBINDINGS.refresh.key
{ {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
if self.active_radarr_block == ActiveRadarrBlock::SystemTaskStartConfirmPrompt if self.active_radarr_block == ActiveRadarrBlock::SystemTaskStartConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, self.key)
{ {
self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = self.app.data.radarr_data.prompt_confirm_action =
@@ -1,6 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -938,6 +939,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_system_details_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = SystemDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_extract_task_name() { fn test_extract_task_name() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -450,6 +450,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_system_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = SystemHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_system_handler_is_not_ready_when_loading() { fn test_system_handler_is_not_ready_when_loading() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -4,6 +4,7 @@ mod tests {
use chrono::DateTime; use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -513,6 +514,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_blocklist_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = BlocklistHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_extract_blocklist_item_id() { fn test_extract_blocklist_item_id() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
@@ -9,6 +7,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKL
use crate::models::sonarr_models::BlocklistItem; use crate::models::sonarr_models::BlocklistItem;
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "blocklist_handler_tests.rs"] #[path = "blocklist_handler_tests.rs"]
@@ -51,6 +50,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
BLOCKLIST_BLOCKS.contains(&active_block) BLOCKLIST_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -143,10 +146,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
let key = self.key; let key = self.key;
match self.active_sonarr_block { match self.active_sonarr_block {
ActiveSonarrBlock::Blocklist => match self.key { ActiveSonarrBlock::Blocklist => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ if key == DEFAULT_KEYBINDINGS.clear.key => { _ if matches_key!(clear, key) => {
self self
.app .app
.push_navigation_stack(ActiveSonarrBlock::BlocklistClearAllItemsPrompt.into()); .push_navigation_stack(ActiveSonarrBlock::BlocklistClearAllItemsPrompt.into());
@@ -154,7 +157,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
_ => (), _ => (),
}, },
ActiveSonarrBlock::DeleteBlocklistItemPrompt => { ActiveSonarrBlock::DeleteBlocklistItemPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteBlocklistItem( self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteBlocklistItem(
self.extract_blocklist_item_id(), self.extract_blocklist_item_id(),
@@ -164,7 +167,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
} }
} }
ActiveSonarrBlock::BlocklistClearAllItemsPrompt => { ActiveSonarrBlock::BlocklistClearAllItemsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::ClearBlocklist); self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::ClearBlocklist);
@@ -1,6 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -389,6 +390,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_downloads_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_extract_download_id() { fn test_extract_download_id() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,13 +1,12 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS};
use crate::models::sonarr_models::DownloadRecord; use crate::models::sonarr_models::DownloadRecord;
use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "downloads_handler_tests.rs"] #[path = "downloads_handler_tests.rs"]
@@ -47,6 +46,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
DOWNLOADS_BLOCKS.contains(&active_block) DOWNLOADS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -130,18 +133,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
let key = self.key; let key = self.key;
match self.active_sonarr_block { match self.active_sonarr_block {
ActiveSonarrBlock::Downloads => match self.key { ActiveSonarrBlock::Downloads => match self.key {
_ if key == DEFAULT_KEYBINDINGS.update.key => { _ if matches_key!(update, key) => {
self self
.app .app
.push_navigation_stack(ActiveSonarrBlock::UpdateDownloadsPrompt.into()); .push_navigation_stack(ActiveSonarrBlock::UpdateDownloadsPrompt.into());
} }
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ => (), _ => (),
}, },
ActiveSonarrBlock::DeleteDownloadPrompt => { ActiveSonarrBlock::DeleteDownloadPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::DeleteDownload(self.extract_download_id())); Some(SonarrEvent::DeleteDownload(self.extract_download_id()));
@@ -150,7 +153,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
} }
} }
ActiveSonarrBlock::UpdateDownloadsPrompt => { ActiveSonarrBlock::UpdateDownloadsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::UpdateDownloads); self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::UpdateDownloads);
@@ -4,6 +4,7 @@ mod tests {
use chrono::DateTime; use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -306,6 +307,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_history_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = HistoryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_history_handler_not_ready_when_loading() { fn test_history_handler_not_ready_when_loading() {
let mut app = App::test_default(); let mut app = App::test_default();
+6 -3
View File
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, KeyEventHandler}; use crate::handlers::{handle_clear_errors, KeyEventHandler};
@@ -9,6 +7,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTOR
use crate::models::servarr_models::Language; use crate::models::servarr_models::Language;
use crate::models::sonarr_models::SonarrHistoryItem; use crate::models::sonarr_models::SonarrHistoryItem;
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::{handle_table_events, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "history_handler_tests.rs"] #[path = "history_handler_tests.rs"]
@@ -52,6 +51,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, '
HISTORY_BLOCKS.contains(&active_block) HISTORY_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -110,7 +113,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, '
let key = self.key; let key = self.key;
if self.active_sonarr_block == ActiveSonarrBlock::History { if self.active_sonarr_block == ActiveSonarrBlock::History {
match self.key { match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ => (), _ => (),
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -6,7 +5,9 @@ use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::servarr_models::EditIndexerParams; use crate::models::servarr_models::EditIndexerParams;
use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys}; use crate::{
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
#[cfg(test)] #[cfg(test)]
#[path = "edit_indexer_handler_tests.rs"] #[path = "edit_indexer_handler_tests.rs"]
@@ -64,6 +65,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
EDIT_INDEXER_BLOCKS.contains(&active_block) EDIT_INDEXER_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -355,7 +360,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
| ActiveSonarrBlock::EditIndexerSeedRatioInput | ActiveSonarrBlock::EditIndexerSeedRatioInput
| ActiveSonarrBlock::EditIndexerTagsInput => { | ActiveSonarrBlock::EditIndexerTagsInput => {
self.app.push_navigation_stack(selected_block.into()); self.app.push_navigation_stack(selected_block.into());
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
ActiveSonarrBlock::EditIndexerPriorityInput => self ActiveSonarrBlock::EditIndexerPriorityInput => self
.app .app
@@ -401,7 +406,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
| ActiveSonarrBlock::EditIndexerSeedRatioInput | ActiveSonarrBlock::EditIndexerSeedRatioInput
| ActiveSonarrBlock::EditIndexerTagsInput => { | ActiveSonarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
ActiveSonarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(), ActiveSonarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(),
_ => (), _ => (),
@@ -422,7 +427,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
| ActiveSonarrBlock::EditIndexerPriorityInput | ActiveSonarrBlock::EditIndexerPriorityInput
| ActiveSonarrBlock::EditIndexerTagsInput => { | ActiveSonarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ => self.app.pop_navigation_stack(), _ => self.app.pop_navigation_stack(),
} }
@@ -503,7 +508,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
ActiveSonarrBlock::EditIndexerPrompt => { ActiveSonarrBlock::EditIndexerPrompt => {
if self.app.data.sonarr_data.selected_block.get_active_block() if self.app.data.sonarr_data.selected_block.get_active_block()
== ActiveSonarrBlock::EditIndexerConfirmPrompt == ActiveSonarrBlock::EditIndexerConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, self.key)
{ {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = self.app.data.sonarr_data.prompt_confirm_action =
@@ -10,6 +10,7 @@ mod tests {
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::servarr_models::EditIndexerParams; use crate::models::servarr_models::EditIndexerParams;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
mod test_handle_scroll_up_and_down { mod test_handle_scroll_up_and_down {
@@ -1002,7 +1003,7 @@ mod tests {
.handle(); .handle();
assert_eq!(app.get_current_route(), block.into()); assert_eq!(app.get_current_route(), block.into());
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
} }
#[test] #[test]
@@ -1027,7 +1028,7 @@ mod tests {
app.get_current_route(), app.get_current_route(),
ActiveSonarrBlock::EditIndexerPriorityInput.into() ActiveSonarrBlock::EditIndexerPriorityInput.into()
); );
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
} }
#[test] #[test]
@@ -1193,7 +1194,7 @@ mod tests {
fn test_edit_indexer_name_input_submit() { fn test_edit_indexer_name_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); app.push_navigation_stack(ActiveSonarrBlock::Indexers.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal {
name: "Test".into(), name: "Test".into(),
..EditIndexerModal::default() ..EditIndexerModal::default()
@@ -1209,7 +1210,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.sonarr_data .sonarr_data
@@ -1229,7 +1230,7 @@ mod tests {
fn test_edit_indexer_url_input_submit() { fn test_edit_indexer_url_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); app.push_navigation_stack(ActiveSonarrBlock::Indexers.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal {
url: "Test".into(), url: "Test".into(),
..EditIndexerModal::default() ..EditIndexerModal::default()
@@ -1245,7 +1246,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.sonarr_data .sonarr_data
@@ -1265,7 +1266,7 @@ mod tests {
fn test_edit_indexer_api_key_input_submit() { fn test_edit_indexer_api_key_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); app.push_navigation_stack(ActiveSonarrBlock::Indexers.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal {
api_key: "Test".into(), api_key: "Test".into(),
..EditIndexerModal::default() ..EditIndexerModal::default()
@@ -1281,7 +1282,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.sonarr_data .sonarr_data
@@ -1301,7 +1302,7 @@ mod tests {
fn test_edit_indexer_seed_ratio_input_submit() { fn test_edit_indexer_seed_ratio_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); app.push_navigation_stack(ActiveSonarrBlock::Indexers.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal {
seed_ratio: "Test".into(), seed_ratio: "Test".into(),
..EditIndexerModal::default() ..EditIndexerModal::default()
@@ -1317,7 +1318,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.sonarr_data .sonarr_data
@@ -1337,7 +1338,7 @@ mod tests {
fn test_edit_indexer_tags_input_submit() { fn test_edit_indexer_tags_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); app.push_navigation_stack(ActiveSonarrBlock::Indexers.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal {
tags: "Test".into(), tags: "Test".into(),
..EditIndexerModal::default() ..EditIndexerModal::default()
@@ -1353,7 +1354,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app assert!(!app
.data .data
.sonarr_data .sonarr_data
@@ -1417,12 +1418,12 @@ mod tests {
app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); app.push_navigation_stack(ActiveSonarrBlock::Indexers.into());
app.push_navigation_stack(active_sonarr_block.into()); app.push_navigation_stack(active_sonarr_block.into());
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
EditIndexerHandler::new(ESC_KEY, &mut app, active_sonarr_block, None).handle(); EditIndexerHandler::new(ESC_KEY, &mut app, active_sonarr_block, None).handle();
assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into());
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.data.sonarr_data.edit_indexer_modal, app.data.sonarr_data.edit_indexer_modal,
Some(EditIndexerModal::default()) Some(EditIndexerModal::default())
@@ -1597,7 +1598,7 @@ mod tests {
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new( EditIndexerHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveSonarrBlock::EditIndexerNameInput, ActiveSonarrBlock::EditIndexerNameInput,
None, None,
@@ -1613,7 +1614,7 @@ mod tests {
.unwrap() .unwrap()
.name .name
.text, .text,
"h" "a"
); );
} }
@@ -1624,7 +1625,7 @@ mod tests {
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new( EditIndexerHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveSonarrBlock::EditIndexerUrlInput, ActiveSonarrBlock::EditIndexerUrlInput,
None, None,
@@ -1640,7 +1641,7 @@ mod tests {
.unwrap() .unwrap()
.url .url
.text, .text,
"h" "a"
); );
} }
@@ -1651,7 +1652,7 @@ mod tests {
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new( EditIndexerHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveSonarrBlock::EditIndexerApiKeyInput, ActiveSonarrBlock::EditIndexerApiKeyInput,
None, None,
@@ -1667,7 +1668,7 @@ mod tests {
.unwrap() .unwrap()
.api_key .api_key
.text, .text,
"h" "a"
); );
} }
@@ -1678,7 +1679,7 @@ mod tests {
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new( EditIndexerHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveSonarrBlock::EditIndexerSeedRatioInput, ActiveSonarrBlock::EditIndexerSeedRatioInput,
None, None,
@@ -1694,7 +1695,7 @@ mod tests {
.unwrap() .unwrap()
.seed_ratio .seed_ratio
.text, .text,
"h" "a"
); );
} }
@@ -1705,7 +1706,7 @@ mod tests {
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new( EditIndexerHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveSonarrBlock::EditIndexerTagsInput, ActiveSonarrBlock::EditIndexerTagsInput,
None, None,
@@ -1721,7 +1722,7 @@ mod tests {
.unwrap() .unwrap()
.tags .tags
.text, .text,
"h" "a"
); );
} }
@@ -1793,6 +1794,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_edit_indexer_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditIndexerHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_build_edit_indexer_params() { fn test_build_edit_indexer_params() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,13 +1,12 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_prompt_left_right_keys;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::sonarr::sonarr_data::{ use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS,
}; };
use crate::models::sonarr_models::IndexerSettings; use crate::models::sonarr_models::IndexerSettings;
use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_prompt_left_right_keys, matches_key};
#[cfg(test)] #[cfg(test)]
#[path = "edit_indexer_settings_handler_tests.rs"] #[path = "edit_indexer_settings_handler_tests.rs"]
@@ -37,6 +36,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
INDEXER_SETTINGS_BLOCKS.contains(&active_block) INDEXER_SETTINGS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -183,7 +186,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
if self.active_sonarr_block == ActiveSonarrBlock::AllIndexerSettingsPrompt if self.active_sonarr_block == ActiveSonarrBlock::AllIndexerSettingsPrompt
&& self.app.data.sonarr_data.selected_block.get_active_block() && self.app.data.sonarr_data.selected_block.get_active_block()
== ActiveSonarrBlock::IndexerSettingsConfirmPrompt == ActiveSonarrBlock::IndexerSettingsConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, self.key)
{ {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditAllIndexerSettings( self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditAllIndexerSettings(
@@ -1,6 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -521,6 +522,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_indexer_settings_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_build_edit_indexer_settings_params() { fn test_build_edit_indexer_settings_params() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -643,6 +643,25 @@ mod tests {
}) })
} }
#[rstest]
fn test_indexers_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_extract_indexer_id() { fn test_extract_indexer_id() {
let mut app = App::test_default(); let mut app = App::test_default();
+10 -7
View File
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::sonarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; use crate::handlers::sonarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
use crate::handlers::sonarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; use crate::handlers::sonarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler;
@@ -15,6 +13,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{
use crate::models::servarr_models::Indexer; use crate::models::servarr_models::Indexer;
use crate::models::BlockSelectionState; use crate::models::BlockSelectionState;
use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_table_events, matches_key};
mod edit_indexer_handler; mod edit_indexer_handler;
mod edit_indexer_settings_handler; mod edit_indexer_settings_handler;
@@ -70,6 +69,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
|| INDEXERS_BLOCKS.contains(&active_block) || INDEXERS_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -168,20 +171,20 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
let key = self.key; let key = self.key;
match self.active_sonarr_block { match self.active_sonarr_block {
ActiveSonarrBlock::Indexers => match self.key { ActiveSonarrBlock::Indexers => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if matches_key!(refresh, key) => {
self.app.should_refresh = true; self.app.should_refresh = true;
} }
_ if key == DEFAULT_KEYBINDINGS.test.key => { _ if matches_key!(test, key) => {
self self
.app .app
.push_navigation_stack(ActiveSonarrBlock::TestIndexer.into()); .push_navigation_stack(ActiveSonarrBlock::TestIndexer.into());
} }
_ if key == DEFAULT_KEYBINDINGS.test_all.key => { _ if matches_key!(test_all, key) => {
self self
.app .app
.push_navigation_stack(ActiveSonarrBlock::TestAllIndexers.into()); .push_navigation_stack(ActiveSonarrBlock::TestAllIndexers.into());
} }
_ if key == DEFAULT_KEYBINDINGS.settings.key => { _ if matches_key!(settings, key) => {
self self
.app .app
.push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into()); .push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into());
@@ -191,7 +194,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
_ => (), _ => (),
}, },
ActiveSonarrBlock::DeleteIndexerPrompt => { ActiveSonarrBlock::DeleteIndexerPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key { if matches_key!(confirm, key) {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::DeleteIndexer(self.extract_indexer_id())); Some(SonarrEvent::DeleteIndexer(self.extract_indexer_id()));
@@ -48,6 +48,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for TestAllIndexersHandl
active_block == ActiveSonarrBlock::TestAllIndexers active_block == ActiveSonarrBlock::TestAllIndexers
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -7,6 +7,7 @@ mod tests {
use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
mod test_handle_esc { mod test_handle_esc {
@@ -48,6 +49,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_test_all_indexers_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = TestAllIndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_test_all_indexers_handler_is_not_ready_when_loading() { fn test_test_all_indexers_handler_is_not_ready_when_loading() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::sonarr::modals::AddSeriesModal; use crate::models::servarr_data::sonarr::modals::AddSeriesModal;
@@ -9,7 +8,9 @@ use crate::models::sonarr_models::{AddSeriesBody, AddSeriesOptions, AddSeriesSea
use crate::models::stateful_table::StatefulTable; use crate::models::stateful_table::StatefulTable;
use crate::models::{BlockSelectionState, Scrollable}; use crate::models::{BlockSelectionState, Scrollable};
use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, App, Key}; use crate::{
handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, matches_key, App, Key,
};
#[cfg(test)] #[cfg(test)]
#[path = "add_series_handler_tests.rs"] #[path = "add_series_handler_tests.rs"]
@@ -126,6 +127,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
ADD_SERIES_BLOCKS.contains(&active_block) ADD_SERIES_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -440,7 +445,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
self self
.app .app
.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); .push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into());
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchResults _ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchResults
&& self.app.data.sonarr_data.add_searched_series.is_some() => && self.app.data.sonarr_data.add_searched_series.is_some() =>
@@ -509,7 +514,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
.get_active_block() .get_active_block()
.into(), .into(),
); );
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder => { ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder => {
self self
@@ -538,7 +543,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
| ActiveSonarrBlock::AddSeriesSelectRootFolder => self.app.pop_navigation_stack(), | ActiveSonarrBlock::AddSeriesSelectRootFolder => self.app.pop_navigation_stack(),
ActiveSonarrBlock::AddSeriesTagsInput => { ActiveSonarrBlock::AddSeriesTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ => (), _ => (),
} }
@@ -549,13 +554,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
ActiveSonarrBlock::AddSeriesSearchInput => { ActiveSonarrBlock::AddSeriesSearchInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.data.sonarr_data.add_series_search = None; self.app.data.sonarr_data.add_series_search = None;
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
ActiveSonarrBlock::AddSeriesSearchResults ActiveSonarrBlock::AddSeriesSearchResults
| ActiveSonarrBlock::AddSeriesEmptySearchResults => { | ActiveSonarrBlock::AddSeriesEmptySearchResults => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.data.sonarr_data.add_searched_series = None; self.app.data.sonarr_data.add_searched_series = None;
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
ActiveSonarrBlock::AddSeriesPrompt => { ActiveSonarrBlock::AddSeriesPrompt => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
@@ -570,7 +575,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
| ActiveSonarrBlock::AddSeriesSelectRootFolder => self.app.pop_navigation_stack(), | ActiveSonarrBlock::AddSeriesSelectRootFolder => self.app.pop_navigation_stack(),
ActiveSonarrBlock::AddSeriesTagsInput => { ActiveSonarrBlock::AddSeriesTagsInput => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false; self.app.ignore_special_keys_for_textbox_input = false;
} }
_ => (), _ => (),
} }
@@ -609,7 +614,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
ActiveSonarrBlock::AddSeriesPrompt => { ActiveSonarrBlock::AddSeriesPrompt => {
if self.app.data.sonarr_data.selected_block.get_active_block() if self.app.data.sonarr_data.selected_block.get_active_block()
== ActiveSonarrBlock::AddSeriesConfirmPrompt == ActiveSonarrBlock::AddSeriesConfirmPrompt
&& key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, key)
{ {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = self.app.data.sonarr_data.prompt_confirm_action =
@@ -2,6 +2,7 @@
mod tests { mod tests {
use bimap::BiMap; use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -912,7 +913,7 @@ mod tests {
fn test_add_series_search_input_submit() { fn test_add_series_search_input_submit() {
let mut app = App::test_default(); let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.data.sonarr_data.add_series_search = Some("test".into()); app.data.sonarr_data.add_series_search = Some("test".into());
AddSeriesHandler::new( AddSeriesHandler::new(
@@ -923,7 +924,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
ActiveSonarrBlock::AddSeriesSearchResults.into() ActiveSonarrBlock::AddSeriesSearchResults.into()
@@ -936,7 +937,7 @@ mod tests {
app.data.sonarr_data.add_series_search = Some(HorizontallyScrollableText::default()); app.data.sonarr_data.add_series_search = Some(HorizontallyScrollableText::default());
app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchInput.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchInput.into());
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
AddSeriesHandler::new( AddSeriesHandler::new(
SUBMIT_KEY, SUBMIT_KEY,
@@ -946,7 +947,7 @@ mod tests {
) )
.handle(); .handle();
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
ActiveSonarrBlock::AddSeriesSearchInput.into() ActiveSonarrBlock::AddSeriesSearchInput.into()
@@ -1230,7 +1231,7 @@ mod tests {
assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); assert_eq!(app.data.sonarr_data.prompt_confirm_action, None);
if selected_block == ActiveSonarrBlock::AddSeriesTagsInput { if selected_block == ActiveSonarrBlock::AddSeriesTagsInput {
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
} }
} }
@@ -1259,7 +1260,7 @@ mod tests {
); );
if active_sonarr_block == ActiveSonarrBlock::AddSeriesTagsInput { if active_sonarr_block == ActiveSonarrBlock::AddSeriesTagsInput {
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
} }
} }
@@ -1336,7 +1337,7 @@ mod tests {
let mut app = App::test_default(); let mut app = App::test_default();
app.is_loading = is_ready; app.is_loading = is_ready;
app.data.sonarr_data = create_test_sonarr_data(); app.data.sonarr_data = create_test_sonarr_data();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchInput.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchInput.into());
@@ -1348,7 +1349,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into());
assert_eq!(app.data.sonarr_data.add_series_search, None); assert_eq!(app.data.sonarr_data.add_series_search, None);
} }
@@ -1357,7 +1358,7 @@ mod tests {
fn test_add_series_input_esc() { fn test_add_series_input_esc() {
let mut app = App::test_default(); let mut app = App::test_default();
app.data.sonarr_data = create_test_sonarr_data(); app.data.sonarr_data = create_test_sonarr_data();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into());
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesTagsInput.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesTagsInput.into());
@@ -1370,7 +1371,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
ActiveSonarrBlock::AddSeriesPrompt.into() ActiveSonarrBlock::AddSeriesPrompt.into()
@@ -1403,7 +1404,7 @@ mod tests {
ActiveSonarrBlock::AddSeriesSearchInput.into() ActiveSonarrBlock::AddSeriesSearchInput.into()
); );
assert!(app.data.sonarr_data.add_searched_series.is_none()); assert!(app.data.sonarr_data.add_searched_series.is_none());
assert!(app.should_ignore_quit_key); assert!(app.ignore_special_keys_for_textbox_input);
} }
#[test] #[test]
@@ -1451,7 +1452,7 @@ mod tests {
fn test_add_series_tags_input_esc() { fn test_add_series_tags_input_esc() {
let mut app = App::test_default(); let mut app = App::test_default();
app.data.sonarr_data = create_test_sonarr_data(); app.data.sonarr_data = create_test_sonarr_data();
app.should_ignore_quit_key = true; app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into());
app.push_navigation_stack(ActiveSonarrBlock::AddSeriesTagsInput.into()); app.push_navigation_stack(ActiveSonarrBlock::AddSeriesTagsInput.into());
@@ -1464,7 +1465,7 @@ mod tests {
) )
.handle(); .handle();
assert!(!app.should_ignore_quit_key); assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
ActiveSonarrBlock::AddSeriesPrompt.into() ActiveSonarrBlock::AddSeriesPrompt.into()
@@ -1570,7 +1571,7 @@ mod tests {
app.data.sonarr_data.add_series_search = Some(HorizontallyScrollableText::default()); app.data.sonarr_data.add_series_search = Some(HorizontallyScrollableText::default());
AddSeriesHandler::new( AddSeriesHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveSonarrBlock::AddSeriesSearchInput, ActiveSonarrBlock::AddSeriesSearchInput,
None, None,
@@ -1585,7 +1586,7 @@ mod tests {
.as_ref() .as_ref()
.unwrap() .unwrap()
.text, .text,
"h" "a"
); );
} }
@@ -1596,7 +1597,7 @@ mod tests {
app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default());
AddSeriesHandler::new( AddSeriesHandler::new(
Key::Char('h'), Key::Char('a'),
&mut app, &mut app,
ActiveSonarrBlock::AddSeriesTagsInput, ActiveSonarrBlock::AddSeriesTagsInput,
None, None,
@@ -1612,7 +1613,7 @@ mod tests {
.unwrap() .unwrap()
.tags .tags
.text, .text,
"h" "a"
); );
} }
@@ -1714,6 +1715,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_add_series_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = AddSeriesHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_add_series_search_no_panic_on_none_search_result() { fn test_add_series_search_no_panic_on_none_search_result() {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -1,9 +1,10 @@
use crate::models::sonarr_models::DeleteSeriesParams; use crate::models::sonarr_models::DeleteSeriesParams;
use crate::network::sonarr_network::SonarrEvent; use crate::network::sonarr_network::SonarrEvent;
use crate::{ use crate::{
app::{key_binding::DEFAULT_KEYBINDINGS, App}, app::App,
event::Key, event::Key,
handlers::{handle_prompt_toggle, KeyEventHandler}, handlers::{handle_prompt_toggle, KeyEventHandler},
matches_key,
models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS}, models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS},
}; };
@@ -38,6 +39,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler<
DELETE_SERIES_BLOCKS.contains(&active_block) DELETE_SERIES_BLOCKS.contains(&active_block)
} }
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new( fn new(
key: Key, key: Key,
app: &'a mut App<'b>, app: &'a mut App<'b>,
@@ -123,7 +128,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler<
if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt
&& self.app.data.sonarr_data.selected_block.get_active_block() && self.app.data.sonarr_data.selected_block.get_active_block()
== ActiveSonarrBlock::DeleteSeriesConfirmPrompt == ActiveSonarrBlock::DeleteSeriesConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key && matches_key!(confirm, self.key)
{ {
self.app.data.sonarr_data.prompt_confirm = true; self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = self.app.data.sonarr_data.prompt_confirm_action =
@@ -1,6 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -320,6 +321,25 @@ mod tests {
}); });
} }
#[rstest]
fn test_delete_series_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DeleteSeriesHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test] #[test]
fn test_build_delete_series_params() { fn test_build_delete_series_params() {
let mut app = App::test_default(); let mut app = App::test_default();

Some files were not shown because too many files have changed in this diff Show More