Compare commits

..

49 Commits

Author SHA1 Message Date
Dark-Alex-17 c4e8d64710 ci: Fixed changelog generation for releases 2025-09-14 18:46:28 -06:00
Dark-Alex-17 ca4319001c docs: Updated the Homebrew tap description [skip-ci] 2025-09-12 11:25:20 -06:00
Dark-Alex-17 ebc58b831d ci: Modified the CI/CD pipeline to bump the procedural macros as well 2025-09-02 17:27:12 -06:00
github-actions[bot] b1572c903c chore: bump Cargo.toml to 0.6.1 2025-09-02 22:58:28 +00:00
github-actions[bot] 5b73924e2a bump: version 0.6.0 → 0.6.1 [skip ci] 2025-09-02 22:58:25 +00:00
Dark-Alex-17 97ce258a8d fix: Fixed UI bugs introduced as part of the hotkey refactor 2025-09-02 15:49:36 -06:00
Dark-Alex-17 fedec79a95 refactor: Updated crate to publish properly with the procedural macros 2025-08-29 17:31:14 -06:00
Dark-Alex-17 8e74709b9c chore(release): Add metadata to enum_display_style_derive 2025-08-29 17:22:15 -06:00
github-actions[bot] be09e1d932 bump: version 0.5.1 → 0.6.0 [skip ci] 2025-08-29 22:33:29 +00:00
Alex Clarke d95417d075 Merge pull request #49 from Dark-Alex-17/develop
chore: Pre-release merge develop into main
2025-08-29 16:14:22 -06:00
Dark-Alex-17 2e3e511e3b ci: Improved the release workflow to be more compatible with Act local testing 2025-08-29 15:41:39 -06:00
Dark-Alex-17 8555052cc4 build: Modified the Dockerfile to be able to build the procedural macros as well 2025-08-29 15:09:22 -06:00
Dark-Alex-17 80bc6793c7 ci: Updated the release flow to also update the Cargo.toml version automatically 2025-08-29 13:45:14 -06:00
Dark-Alex-17 049a0c5d49 docs: Updated CONTRIBUTING.md to explain how to use act 2025-08-29 13:27:36 -06:00
Dark-Alex-17 ae9cb77e6d ci: Added step to release flow to update the Cargo.toml version 2025-08-29 11:13:32 -06:00
Dark-Alex-17 126ed5ed72 build: Updated dependencies 2025-08-29 10:59:29 -06:00
Dark-Alex-17 63ae64cebd ci: Modified CI/CD workflows so they can be tested locally with Act 2025-08-29 10:58:39 -06:00
Dark-Alex-17 0b29351366 feat: Support for custom headers to be added to every request to each server to support alternative authentication mechanisms [#47] 2025-08-29 10:23:17 -06:00
Dark-Alex-17 4ea39f74fe Updated the Rust version to 1.89 in the dockerfile to match the crate definition 2025-08-26 16:43:15 -06:00
Dark-Alex-17 76fcf5e67f docs: Fixed a typo in the README 2025-08-15 14:39:47 -06:00
Dark-Alex-17 11457736e6 refactor: Network module is now broken out into similar directory structures for each servarr to mimic the rest of the project to make it easier to develop and maintain 2025-08-14 13:14:23 -06:00
Dark-Alex-17 e2a6af1cbd refactor: Refactored the IndexerTestResut model into the general Servarr models 2025-08-12 17:05:17 -06:00
Dark-Alex-17 20ea15009d build: Updated dependencies and upgraded to Rust 1.89.0 2025-08-12 16:56:45 -06:00
Dark-Alex-17 00ab0f27f7 feat: Refactor all keybinding tips into a dynamically changing menu that can be invoked via '?' [#32] 2025-08-12 16:27:34 -06:00
Dark-Alex-17 1f4870d082 feat: Display total disk usage for series in the Library view to mirror Radarr functionality [#44] 2025-08-11 10:24:00 -06:00
Dark-Alex-17 e96af7410e feat: Pagination support for jumping 20 items at a time in all table views [#45] 2025-08-08 17:04:28 -06:00
Dark-Alex-17 345bb8ce03 refactor: Renamed 'ctrl-*' keyboard shortcuts to 'C-*' to simplify and shrink the on-screen help 2025-08-08 15:09:39 -06:00
Dark-Alex-17 dbcfc77ad4 feat: Support toggling Movie monitoring directly from the library view [#43] 2025-08-08 14:54:28 -06:00
Dark-Alex-17 e653532212 feat: Support toggling Movie monitoring from the CLI 2025-08-08 14:49:15 -06:00
Dark-Alex-17 8e7e31f64d feat: Support toggling Series monitoring directly from the Sonarr library view [#43] 2025-08-08 14:48:27 -06:00
Dark-Alex-17 bff3795cc6 feat: Support toggling Series monitoring from the CLI 2025-08-08 14:46:35 -06:00
Dark-Alex-17 8782f1353d docs: Formatted and cleaned up README a bit 2025-08-08 14:10:33 -06:00
Dark-Alex-17 f08f255a46 fix: Marked Radarr studios as nullable to prevent crashes 2025-08-07 20:05:05 -06:00
Dark-Alex-17 02870043ec style: Addressed updated linter complaints 2025-08-07 19:49:11 -06:00
Dark-Alex-17 154e491922 fix: Fixed a bug where the Sonarr API was returning empty values for seeders when searching for season releases 2025-08-07 16:00:10 -06:00
Dark-Alex-17 ef5e702255 feat: Fixed the Radarr downloads tab to display more than 10 downloads at a time and added a new --count flag to the CLI for specifying the number of downloads to return 2025-07-13 15:04:39 -06:00
Dark-Alex-17 cb4cd93bcd feat: Fetch more than 10 downloads when listing Sonarr downloads, and add a --count flag to the CLI to specify how many downloads to fetch 2025-07-13 14:48:15 -06:00
Alex Clarke ee034c9caf Update README.md to remove the cheeky "try before you buy" heading since some users reported it as misleading 2025-05-29 23:48:16 -06:00
Dark-Alex-17 c133a4ecd2 fix: Improve fault tolerance for tag associations in Radarr and Sonarr 2025-05-18 13:49:52 -06:00
Dark-Alex-17 f09a2efa5e Updated README to have labels for the theme examples for better readability 2025-04-08 17:31:29 -06:00
Dark-Alex-17 49983f4173 fix: Upgraded to the most recent version of Tokio to mitigate CWE-664 2025-04-08 11:05:32 -06:00
Dark-Alex-17 0f9894e1be fix: Updated all dependencies and updated openssl to the most recent version to mitigate CWE-416 2025-04-07 15:22:56 -06:00
Alex Clarke 8c0dffec31 Merge pull request #41 from cwelsys/fix-key-event-handling
fix: Ensure key events are only processed on key press to avoid dupli…
2025-04-07 12:01:14 -06:00
Alex Clarke 0085d944ea Merge branch 'develop' into fix-key-event-handling 2025-04-07 11:58:06 -06:00
Connor Welsh 89cf0e66a6 fix: Ensure key events are only processed on key press to avoid duplicates 2025-04-06 17:29:06 -04:00
Alex Clarke baef436f78 Merge pull request #39 from Dark-Alex-17/alternative-keymappings
Alternative keymappings
2025-04-01 10:11:22 -06:00
Dark-Alex-17 cf00d7992e fix: Updated the name of the should_ignore_quit_key to ignore_special_keys_for_textbox_input to give a better idea of what the flag is used for; also added alt keybinding for backspace 2025-03-27 15:21:44 -06:00
Dark-Alex-17 f25829f3c1 Merge remote-tracking branch 'refs/remotes/origin/develop' 2025-03-21 11:56:12 -06:00
Dark-Alex-17 8c783bc405 fix: Marked videoCodecs as Option to resolve #38 2025-03-21 11:55:54 -06:00
226 changed files with 19690 additions and 16316 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
+202 -19
View File
@@ -8,9 +8,9 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
bump_type: bump_type:
description: "Specify the type of version bump" description: 'Specify the type of version bump'
required: true required: true
default: "patch" default: 'patch'
type: choice type: choice
options: options:
- patch - patch
@@ -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,15 +31,22 @@ 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:
python-version: "3.10" python-version: '3.10'
- name: Install Commitizen - name: Install Commitizen
run: | run: |
@@ -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,14 +86,133 @@ 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: Bump validate_theme_derive/Cargo.toml version
shell: bash
working-directory: ${{ github.workspace }}/proc_macros/validate_theme_derive
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 || true
# Only commit if one of these files actually changed
if ! git diff --quiet -- Cargo.toml; then
# Stage only modifications of already tracked files (won't pick up artifacts/)
git add -u -- Cargo.toml
git commit -m "chore: bump validate_theme_derive Cargo.toml to $VERSION"
else
echo "No changes to commit (already at $VERSION)"
fi
- name: Bump enum_display_style_derive/Cargo.toml version
shell: bash
working-directory: ${{ github.workspace }}/proc_macros/enum_display_style_derive
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 || true
# Only commit if one of these files actually changed
if ! git diff --quiet -- Cargo.toml; then
# Stage only modifications of already tracked files (won't pick up artifacts/)
git add -u -- Cargo.toml
git commit -m "chore: bump enum_display_style_derive 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: |
changelog=$(conventional-changelog -p angular -i CHANGELOG.md -s --from ${{ env.prev_version }} --to ${{ env.version }}) conventional-changelog -p angular -i CHANGELOG.md --from ${{ env.prev_version }} --to v${{ env.version }} > artifacts/changelog.md
echo "$changelog" > artifacts/changelog.md
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 +223,17 @@ 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
proc_macros/validate_theme_derive/Cargo.toml
proc_macros/enum_display_style_derive/Cargo.toml
build-release-artifacts: build-release-artifacts:
name: build-release name: build-release
needs: [bump-version] needs: [bump-version]
@@ -129,7 +261,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 +272,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 +386,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 +404,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
@@ -266,10 +413,14 @@ jobs:
run: | run: |
release_version="$(cat ./artifacts/release-version)" release_version="$(cat ./artifacts/release-version)"
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
changelog_body="$(cat ./artifacts/changelog.md)"
echo "changelog_body=$(cat artifacts/changelog.md)" >> $GITHUB_ENV - name: Validate release environment variables
run: |
echo "Release version: ${{ env.RELEASE_VERSION }}"
echo "Changelog body: $(cat artifacts/changelog.md)"
- 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 }}
@@ -300,8 +451,8 @@ jobs:
artifacts/managarr-armv7-musl.tar.gz artifacts/managarr-armv7-musl.tar.gz
artifacts/managarr-armv7-musl.sha256 artifacts/managarr-armv7-musl.sha256
tag_name: v${{ env.RELEASE_VERSION }} tag_name: v${{ env.RELEASE_VERSION }}
name: "v${{ env.RELEASE_VERSION }}" name: 'v${{ env.RELEASE_VERSION }}'
body: ${{ env.changelog_body }} body_path: artifacts/changelog.md
draft: false draft: false
prerelease: false prerelease: false
@@ -316,6 +467,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 +501,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 +521,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 +559,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 +585,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 +603,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 +624,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 +636,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 +645,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 +655,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 +683,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/
+57
View File
@@ -5,6 +5,63 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## v0.6.1 (2025-09-02)
### Fix
- Fixed UI bugs introduced as part of the hotkey refactor
### Refactor
- Updated crate to publish properly with the procedural macros
## v0.6.0 (2025-08-29)
### Feat
- Support for custom headers to be added to every request to each server to support alternative authentication mechanisms [#47]
- Refactor all keybinding tips into a dynamically changing menu that can be invoked via '?' [#32]
- Display total disk usage for series in the Library view to mirror Radarr functionality [#44]
- Pagination support for jumping 20 items at a time in all table views [#45]
- Support toggling Movie monitoring directly from the library view [#43]
- Support toggling Movie monitoring from the CLI
- Support toggling Series monitoring directly from the Sonarr library view [#43]
- Support toggling Series monitoring from the CLI
- Fixed the Radarr downloads tab to display more than 10 downloads at a time and added a new --count flag to the CLI for specifying the number of downloads to return
- Fetch more than 10 downloads when listing Sonarr downloads, and add a --count flag to the CLI to specify how many downloads to fetch
- Support alternative keymappings for all keys, featuring hjkl movements
- Added the Eldritch theme and updated documentation
- Write built in themes to the themes file on first run so users can define custom themes
- Created a theme validation macro to verify theme configurations before allowing the TUI to start
- Initial support for custom user-defined themes
### Fix
- Marked Radarr studios as nullable to prevent crashes
- Fixed a bug where the Sonarr API was returning empty values for seeders when searching for season releases
- Improve fault tolerance for tag associations in Radarr and Sonarr
- Upgraded to the most recent version of Tokio to mitigate CWE-664
- Updated all dependencies and updated openssl to the most recent version to mitigate CWE-416
- Updated the name of the should_ignore_quit_key to ignore_special_keys_for_textbox_input to give a better idea of what the flag is used for; also added alt keybinding for backspace
- Marked videoCodecs as Option to resolve #38
- Marked the `Season.statistics` field as `Option` so that a panic does not happen for outdated Sonarr data. This resolves #35
- When adding a film from the Collection Details modal, the render order was wrong: Radarr Library -> Collection Table -> Add Movie Prompt (missing the Collection details prompt too). Correct order is: Collection Table -> Collection Details Modal -> Add Movie Modal
- Fixed a bug that was rendering encompassing blocks after other widgets were rendered, thus overwriting the custom styles on each previously rendered widget
- change the name of the theme configuration file to 'themes'
- Ensure key events are only processed on key press to avoid duplicates
- Updated ring dependency to mitigate CWE-770
- Modified the Sonarr DownloadRecord so that the episode_id is optional to prevent crashes for weird downloads
### Refactor
- Network module is now broken out into similar directory structures for each servarr to mimic the rest of the project to make it easier to develop and maintain
- Refactored the IndexerTestResut model into the general Servarr models
- Renamed 'ctrl-*' keyboard shortcuts to 'C-*' to simplify and shrink the on-screen help
- Formatted files using rustfmt
- Reformatted code to make the format checks pass
- Created a derive macro for defining the display style of Enum models and removed the use of the EnumDisplayStyle trait
- Expanded the serde_enum_from macro to further reduce code duplication
## v0.5.1 (2025-03-01) ## v0.5.1 (2025-03-01)
### Feat ### Feat
+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
+613 -448
View File
File diff suppressed because it is too large Load Diff
+9 -6
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "managarr" name = "managarr"
version = "0.5.1" version = "0.6.1"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"] authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs" description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"] keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
@@ -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", "proc_macros/validate_theme_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,8 +66,8 @@ 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" } validate_theme_derive = "0.1.0"
enum_display_style_derive = { path = "proc_macros/enum_display_style_derive" } enum_display_style_derive = "0.1.0"
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.16" assert_cmd = "2.0.16"
+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
+25 -19
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
@@ -209,8 +209,11 @@ Key:
### Themes ### Themes
Managarr ships with a few themes out of the box. Here's a few examples: Managarr ships with a few themes out of the box. Here's a few examples:
#### Default
![default](themes/default/manual_episode_search.png) ![default](themes/default/manual_episode_search.png)
#### Dracula
![dracula](themes/dracula/manual_episode_search.png) ![dracula](themes/dracula/manual_episode_search.png)
#### Watermelon Dark
![watermelon-dark](themes/watermelon-dark/manual_episode_search.png) ![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 You can also create your own custom themes as well. To learn more about what themes are built-in to Managarr and how
@@ -354,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
+1 -1
View File
@@ -1,7 +1,7 @@
# Documentation: https://docs.brew.sh/Formula-Cookbook # Documentation: https://docs.brew.sh/Formula-Cookbook
# https://rubydoc.brew.sh/Formula # https://rubydoc.brew.sh/Formula
class Managarr < Formula class Managarr < Formula
desc "A fast and simple dashboard for Kubernetes written in Rust" desc "Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC)"
homepage "https://github.com/Dark-Alex-17/managarr" homepage "https://github.com/Dark-Alex-17/managarr"
if OS.mac? and Hardware::CPU.arm? if OS.mac? and Hardware::CPU.arm?
url "https://github.com/Dark-Alex-17/managarr/releases/download/v$version/managarr-macos-arm64.tar.gz" url "https://github.com/Dark-Alex-17/managarr/releases/download/v$version/managarr-macos-arm64.tar.gz"
@@ -1,7 +1,13 @@
[package] [package]
name = "enum_display_style_derive" name = "enum_display_style_derive"
version = "0.1.0" version = "0.6.1"
edition = "2024" edition = "2024"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A proc-macro to derive a `Display` and `FromStr` implementation for enums with a `style` attribute."
license = "MIT"
documentation = "https://github.com/Dark-Alex-17/managarr"
repository = "https://github.com/Dark-Alex-17/managarr"
homepage = "https://github.com/Dark-Alex-17/managarr"
[lib] [lib]
proc-macro = true proc-macro = true
+7 -1
View File
@@ -1,7 +1,13 @@
[package] [package]
name = "validate_theme_derive" name = "validate_theme_derive"
version = "0.1.0" version = "0.6.1"
edition = "2024" edition = "2024"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A proc-macro to validate a theme."
license = "MIT"
documentation = "https://github.com/Dark-Alex-17/managarr"
repository = "https://github.com/Dark-Alex-17/managarr"
homepage = "https://github.com/Dark-Alex-17/managarr"
[lib] [lib]
proc-macro = true proc-macro = true
+17 -17
View File
@@ -72,25 +72,25 @@ pub fn derive_validate_theme(input: TokenStream) -> TokenStream {
let mut validation_checks = Vec::new(); let mut validation_checks = Vec::new();
if let Data::Struct(data_struct) = &input.data { if let Data::Struct(data_struct) = &input.data
if let Fields::Named(fields) = &data_struct.fields { && let Fields::Named(fields) = &data_struct.fields
for field in &fields.named { {
let field_name = &field.ident; for field in &fields.named {
let field_name = &field.ident;
let has_validate_attr = field let has_validate_attr = field
.attrs .attrs
.iter() .iter()
.any(|attr| attr.path().is_ident("validate")); .any(|attr| attr.path().is_ident("validate"));
if has_validate_attr { if has_validate_attr {
validation_checks.push(quote! { validation_checks.push(quote! {
if self.#field_name.is_none() { if self.#field_name.is_none() {
log::error!("{} is missing a color value.", stringify!(#field_name)); log::error!("{} is missing a color value.", stringify!(#field_name));
eprintln!("{} is missing a color value.", stringify!(#field_name)); eprintln!("{} is missing a color value.", stringify!(#field_name));
std::process::exit(1); std::process::exit(1);
} }
}) })
}
} }
} }
} }
+106 -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};
@@ -39,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),
}, },
@@ -98,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);
} }
@@ -118,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);
} }
@@ -284,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(),
@@ -357,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]
@@ -400,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() {
@@ -513,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),
@@ -524,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());
}
} }
+21 -3
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,
@@ -74,9 +77,19 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
alt: Some(Key::Char('l')), 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: None, alt: Some(Key::Ctrl('h')),
desc: "backspace", desc: "backspace",
}, },
next_servarr: KeyBinding { next_servarr: KeyBinding {
@@ -109,6 +122,11 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
alt: None, 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, alt: None,
@@ -215,9 +233,9 @@ macro_rules! matches_key {
.unwrap() .unwrap()
== $key) == $key)
}; };
($binding:ident, $key:expr, $ignore_alt_navigation:expr) => { ($binding:ident, $key:expr, $ignore_special_keys:expr) => {
$crate::app::key_binding::DEFAULT_KEYBINDINGS.$binding.key == $key $crate::app::key_binding::DEFAULT_KEYBINDINGS.$binding.key == $key
|| !$ignore_alt_navigation || !$ignore_special_keys
&& ($crate::app::key_binding::DEFAULT_KEYBINDINGS && ($crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding .$binding
.alt .alt
+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));
}
} }
+63 -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()),
}, },
@@ -296,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 {
@@ -347,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 {
@@ -368,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);
} }
@@ -393,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>,
@@ -416,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());
}
}
+65 -5
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::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::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,12 +31,12 @@ 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 matches_key!(up, key, self.ignore_alt_navigation()) => { _ 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 matches_key!(down, key, self.ignore_alt_navigation()) => { _ 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();
} }
@@ -47,8 +56,8 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
self.handle_delete(); self.handle_delete();
} }
} }
_ if matches_key!(left, key, self.ignore_alt_navigation()) _ if matches_key!(left, key, self.ignore_special_keys())
|| matches_key!(right, key, self.ignore_alt_navigation()) => || matches_key!(right, key, self.ignore_special_keys()) =>
{ {
self.handle_left_right_action() self.handle_left_right_action()
} }
@@ -73,7 +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_alt_navigation(&self) -> bool; 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);
@@ -97,8 +106,17 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
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()
} }
@@ -110,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();
@@ -543,11 +543,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_blocklist_handler_ignore_alt_navigation( fn test_blocklist_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = BlocklistHandler::new( let handler = BlocklistHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -555,7 +555,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -50,8 +50,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
BLOCKLIST_BLOCKS.contains(&active_block) BLOCKLIST_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -45,8 +45,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
COLLECTION_DETAILS_BLOCKS.contains(&active_block) COLLECTION_DETAILS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -280,11 +280,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_collection_details_handler_ignore_alt_navigation( fn test_collection_details_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = CollectionDetailsHandler::new( let handler = CollectionDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -292,7 +292,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -590,11 +590,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_collections_handler_ignore_alt_navigation( fn test_collections_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = CollectionsHandler::new( let handler = CollectionsHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -602,7 +602,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -660,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,
@@ -670,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,
@@ -680,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,
@@ -68,8 +68,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
EDIT_COLLECTION_BLOCKS.contains(&active_block) EDIT_COLLECTION_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -265,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
@@ -314,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;
} }
_ => (), _ => (),
} }
@@ -324,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();
@@ -458,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()
@@ -474,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
@@ -760,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);
} }
} }
@@ -792,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);
} }
} }
} }
@@ -812,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());
@@ -824,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()
@@ -1021,11 +1021,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_edit_collection_handler_ignore_alt_navigation( fn test_edit_collection_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditCollectionHandler::new( let handler = EditCollectionHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -1033,7 +1033,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -72,8 +72,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
|| COLLECTIONS_BLOCKS.contains(&active_block) || COLLECTIONS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -389,11 +389,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_downloads_handler_ignore_alt_navigation( fn test_downloads_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DownloadsHandler::new( let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -401,7 +401,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -46,8 +46,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
DOWNLOADS_BLOCKS.contains(&active_block) DOWNLOADS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -66,8 +66,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
EDIT_INDEXER_BLOCKS.contains(&active_block) EDIT_INDEXER_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -361,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
@@ -407,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(),
_ => (), _ => (),
@@ -428,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(),
} }
@@ -1003,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]
@@ -1028,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]
@@ -1194,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()
@@ -1210,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
@@ -1230,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()
@@ -1246,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
@@ -1266,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()
@@ -1282,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
@@ -1302,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()
@@ -1318,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
@@ -1338,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()
@@ -1354,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
@@ -1418,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())
@@ -1795,11 +1795,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_edit_indexer_handler_ignore_alt_navigation( fn test_edit_indexer_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditIndexerHandler::new( let handler = EditIndexerHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -1807,7 +1807,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -38,8 +38,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
INDEXER_SETTINGS_BLOCKS.contains(&active_block) INDEXER_SETTINGS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -212,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();
@@ -229,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
@@ -249,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(),
} }
@@ -603,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]
@@ -717,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()
@@ -735,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
@@ -815,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,
@@ -826,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())
@@ -972,11 +972,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_indexer_settings_handler_ignore_alt_navigation( fn test_indexer_settings_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexerSettingsHandler::new( let handler = IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -984,7 +984,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -634,11 +634,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_indexers_handler_ignore_alt_navigation( fn test_indexers_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexersHandler::new( let handler = IndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -646,7 +646,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
+2 -2
View File
@@ -69,8 +69,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
|| INDEXERS_BLOCKS.contains(&active_block) || INDEXERS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -48,8 +48,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl
active_block == ActiveRadarrBlock::TestAllIndexers active_block == ActiveRadarrBlock::TestAllIndexers
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -50,11 +50,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_test_all_indexers_handler_ignore_alt_navigation( fn test_test_all_indexers_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = TestAllIndexersHandler::new( let handler = TestAllIndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -62,7 +62,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -133,8 +133,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
ADD_MOVIE_BLOCKS.contains(&active_block) ADD_MOVIE_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -409,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() =>
@@ -473,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;
} }
_ => (), _ => (),
} }
@@ -484,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;
} }
_ => (), _ => (),
} }
@@ -495,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();
@@ -514,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;
} }
_ => (), _ => (),
} }
@@ -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()
@@ -1524,11 +1524,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_add_movie_handler_ignore_alt_navigation( fn test_add_movie_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = AddMovieHandler::new( let handler = AddMovieHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -1536,7 +1536,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -37,8 +37,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
DELETE_MOVIE_BLOCKS.contains(&active_block) DELETE_MOVIE_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -315,11 +315,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_delete_movie_handler_ignore_alt_navigation( fn test_delete_movie_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DeleteMovieHandler::new( let handler = DeleteMovieHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -327,7 +327,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -67,8 +67,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
EDIT_MOVIE_BLOCKS.contains(&active_block) EDIT_MOVIE_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -293,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
@@ -322,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;
} }
_ => (), _ => (),
} }
@@ -332,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();
@@ -549,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()
@@ -565,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
@@ -584,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()
@@ -600,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
@@ -814,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);
} }
} }
@@ -852,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]
@@ -886,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);
} }
} }
} }
@@ -912,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()
@@ -1150,11 +1150,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_edit_movie_handler_ignore_alt_navigation( fn test_edit_movie_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditMovieHandler::new( let handler = EditMovieHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -1162,7 +1162,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -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);
@@ -778,11 +845,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_library_handler_ignore_alt_navigation( fn test_library_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = LibraryHandler::new( let handler = LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -790,7 +857,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -853,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(),
@@ -870,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(),
@@ -887,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(),
+22 -4
View File
@@ -35,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> {
@@ -80,8 +83,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
|| LIBRARY_BLOCKS.contains(&active_block) || LIBRARY_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -181,7 +184,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
.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 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) => { _ if matches_key!(update, key) => {
self self
@@ -223,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",
@@ -135,8 +135,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
MOVIE_DETAILS_BLOCKS.contains(&active_block) MOVIE_DETAILS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -1043,11 +1043,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_movie_details_handler_ignore_alt_navigation( fn test_movie_details_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = MovieDetailsHandler::new( let handler = MovieDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -1055,7 +1055,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[rstest] #[rstest]
+6 -6
View File
@@ -64,8 +64,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
true true
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -112,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 matches_key!(left, key, app.should_ignore_quit_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 matches_key!(right, key, app.should_ignore_quit_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());
} }
@@ -142,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();
@@ -169,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,
@@ -60,7 +60,7 @@ mod tests {
#[case] right_block: ActiveRadarrBlock, #[case] right_block: ActiveRadarrBlock,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = false; app.ignore_special_keys_for_textbox_input = false;
app.data.radarr_data.main_tabs.set_index(index); app.data.radarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.alt.unwrap()); handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.alt.unwrap());
@@ -95,7 +95,7 @@ mod tests {
#[case] block: ActiveRadarrBlock, #[case] block: ActiveRadarrBlock,
) { ) {
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.push_navigation_stack(block.into()); app.push_navigation_stack(block.into());
app.data.radarr_data.main_tabs.set_index(index); app.data.radarr_data.main_tabs.set_index(index);
@@ -288,11 +288,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_radarr_handler_ignore_alt_navigation( fn test_radarr_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = RadarrHandler::new( let handler = RadarrHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -300,7 +300,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -69,8 +69,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
ROOT_FOLDERS_BLOCKS.contains(&active_block) ROOT_FOLDERS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -173,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();
} }
_ => (), _ => (),
@@ -186,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();
@@ -208,7 +208,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
.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;
} }
_ => (), _ => (),
}, },
@@ -263,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());
@@ -276,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))
@@ -292,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());
@@ -305,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(),
@@ -407,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,
@@ -424,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]
@@ -473,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());
} }
@@ -500,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());
} }
@@ -646,11 +646,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_root_folders_handler_ignore_alt_navigation( fn test_root_folders_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = RootFoldersHandler::new( let handler = RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -658,7 +658,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
+2 -2
View File
@@ -35,8 +35,8 @@ 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_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -36,8 +36,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
SYSTEM_DETAILS_BLOCKS.contains(&active_block) SYSTEM_DETAILS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -940,11 +940,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_system_details_handler_ignore_alt_navigation( fn test_system_details_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = SystemDetailsHandler::new( let handler = SystemDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -952,7 +952,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -451,11 +451,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_system_handler_ignore_alt_navigation( fn test_system_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = SystemHandler::new( let handler = SystemHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -463,7 +463,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -515,11 +515,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_blocklist_handler_ignore_alt_navigation( fn test_blocklist_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = BlocklistHandler::new( let handler = BlocklistHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -527,7 +527,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -50,8 +50,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
BLOCKLIST_BLOCKS.contains(&active_block) BLOCKLIST_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -391,11 +391,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_downloads_handler_ignore_alt_navigation( fn test_downloads_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DownloadsHandler::new( let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -403,7 +403,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -46,8 +46,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
DOWNLOADS_BLOCKS.contains(&active_block) DOWNLOADS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -308,11 +308,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_history_handler_ignore_alt_navigation( fn test_history_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = HistoryHandler::new( let handler = HistoryHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -320,7 +320,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
+2 -2
View File
@@ -51,8 +51,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, '
HISTORY_BLOCKS.contains(&active_block) HISTORY_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -65,8 +65,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
EDIT_INDEXER_BLOCKS.contains(&active_block) EDIT_INDEXER_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -360,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
@@ -406,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(),
_ => (), _ => (),
@@ -427,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(),
} }
@@ -1003,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]
@@ -1028,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]
@@ -1194,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()
@@ -1210,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
@@ -1230,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()
@@ -1246,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
@@ -1266,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()
@@ -1282,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
@@ -1302,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()
@@ -1318,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
@@ -1338,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()
@@ -1354,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
@@ -1418,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())
@@ -1795,11 +1795,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_edit_indexer_handler_ignore_alt_navigation( fn test_edit_indexer_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditIndexerHandler::new( let handler = EditIndexerHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -1807,7 +1807,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -36,8 +36,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
INDEXER_SETTINGS_BLOCKS.contains(&active_block) INDEXER_SETTINGS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -523,11 +523,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_indexer_settings_handler_ignore_alt_navigation( fn test_indexer_settings_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexerSettingsHandler::new( let handler = IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -535,7 +535,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -644,11 +644,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_indexers_handler_ignore_alt_navigation( fn test_indexers_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexersHandler::new( let handler = IndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -656,7 +656,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
+2 -2
View File
@@ -69,8 +69,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
|| INDEXERS_BLOCKS.contains(&active_block) || INDEXERS_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -48,8 +48,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for TestAllIndexersHandl
active_block == ActiveSonarrBlock::TestAllIndexers active_block == ActiveSonarrBlock::TestAllIndexers
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -50,11 +50,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_test_all_indexers_handler_ignore_alt_navigation( fn test_test_all_indexers_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = TestAllIndexersHandler::new( let handler = TestAllIndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -62,7 +62,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -127,8 +127,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a,
ADD_SERIES_BLOCKS.contains(&active_block) ADD_SERIES_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -445,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() =>
@@ -514,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
@@ -543,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;
} }
_ => (), _ => (),
} }
@@ -554,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();
@@ -575,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;
} }
_ => (), _ => (),
} }
@@ -913,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(
@@ -924,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()
@@ -937,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,
@@ -947,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()
@@ -1231,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);
} }
} }
@@ -1260,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);
} }
} }
@@ -1337,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());
@@ -1349,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);
} }
@@ -1358,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());
@@ -1371,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()
@@ -1404,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]
@@ -1452,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());
@@ -1465,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()
@@ -1716,11 +1716,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_add_series_handler_ignore_alt_navigation( fn test_add_series_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = AddSeriesHandler::new( let handler = AddSeriesHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -1728,7 +1728,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -39,8 +39,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler<
DELETE_SERIES_BLOCKS.contains(&active_block) DELETE_SERIES_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -322,11 +322,11 @@ mod tests {
} }
#[rstest] #[rstest]
fn test_delete_series_handler_ignore_alt_navigation( fn test_delete_series_handler_ignore_special_keys(
#[values(true, false)] should_ignore_quit_key: bool, #[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) { ) {
let mut app = App::test_default(); let mut app = App::test_default();
app.should_ignore_quit_key = should_ignore_quit_key; app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DeleteSeriesHandler::new( let handler = DeleteSeriesHandler::new(
DEFAULT_KEYBINDINGS.esc.key, DEFAULT_KEYBINDINGS.esc.key,
&mut app, &mut app,
@@ -334,7 +334,10 @@ mod tests {
None, None,
); );
assert_eq!(handler.ignore_alt_navigation(), should_ignore_quit_key); assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
} }
#[test] #[test]
@@ -82,8 +82,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a
EDIT_SERIES_BLOCKS.contains(&active_block) EDIT_SERIES_BLOCKS.contains(&active_block)
} }
fn ignore_alt_navigation(&self) -> bool { fn ignore_special_keys(&self) -> bool {
self.app.should_ignore_quit_key self.app.ignore_special_keys_for_textbox_input
} }
fn new( fn new(
@@ -345,7 +345,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a
) )
.into(), .into(),
); );
self.app.should_ignore_quit_key = true; self.app.ignore_special_keys_for_textbox_input = true;
} }
ActiveSonarrBlock::EditSeriesToggleMonitored => { ActiveSonarrBlock::EditSeriesToggleMonitored => {
self self
@@ -395,7 +395,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a
| ActiveSonarrBlock::EditSeriesSelectLanguageProfile => self.app.pop_navigation_stack(), | ActiveSonarrBlock::EditSeriesSelectLanguageProfile => self.app.pop_navigation_stack(),
ActiveSonarrBlock::EditSeriesPathInput | ActiveSonarrBlock::EditSeriesTagsInput => { ActiveSonarrBlock::EditSeriesPathInput | ActiveSonarrBlock::EditSeriesTagsInput => {
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;
} }
_ => (), _ => (),
} }
@@ -405,7 +405,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a
match self.active_sonarr_block { match self.active_sonarr_block {
ActiveSonarrBlock::EditSeriesTagsInput | ActiveSonarrBlock::EditSeriesPathInput => { ActiveSonarrBlock::EditSeriesTagsInput | ActiveSonarrBlock::EditSeriesPathInput => {
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::EditSeriesPrompt => { ActiveSonarrBlock::EditSeriesPrompt => {
self.app.pop_navigation_stack(); self.app.pop_navigation_stack();

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