Compare commits

..

84 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
Dark-Alex-17 0048d71b74 feat: Support alternative keymappings for all keys, featuring hjkl movements 2025-03-17 22:02:15 -06:00
Dark-Alex-17 c633347ecc Merge remote-tracking branch 'refs/remotes/origin/develop' 2025-03-17 20:49:52 -06:00
Alex Clarke ecd6a0ec32 Merge pull request #37 from Dark-Alex-17/custom-themes
Support Themes
2025-03-17 14:23:18 -06:00
Dark-Alex-17 30507d9d01 docs: Updated the README to include the new flags 2025-03-17 13:26:46 -06:00
Dark-Alex-17 6245a794d5 docs: Update all screenshots to not have any auto-generated usernames in the tags columns 2025-03-10 16:22:24 -06:00
Dark-Alex-17 5c822e4890 Merge branch 'develop' 2025-03-10 16:13:48 -06:00
Dark-Alex-17 cab06fe43f fix: Marked the Season.statistics field as Option so that a panic does not happen for outdated Sonarr data. This resolves #35 2025-03-10 16:13:04 -06:00
Dark-Alex-17 b4ff5f3351 feat: Added the Eldritch theme and updated documentation 2025-03-10 15:49:40 -06:00
Dark-Alex-17 0834802481 fix: 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 2025-03-10 15:08:02 -06:00
Dark-Alex-17 3afd74dcbf fix: Fixed a bug that was rendering encompassing blocks after other widgets were rendered, thus overwriting the custom styles on each previously rendered widget 2025-03-10 15:01:58 -06:00
Dark-Alex-17 b1a0bdfbb6 Merge branch 'develop' 2025-03-07 12:02:47 -07:00
Dark-Alex-17 6d38bc5e1d Merge branch 'main' 2025-03-07 12:02:19 -07:00
Dark-Alex-17 5ba1ba15c9 ci: Update to the most recent Rust version 2025-03-07 11:55:32 -07:00
Dark-Alex-17 db05d2abfb Merge branch 'develop' into custom-themes 2025-03-07 10:37:48 -07:00
Dark-Alex-17 1840c4e39a Merge branch 'main' into develop
# Conflicts:
#	proc_macros/enum_display_style_derive/src/lib.rs
2025-03-07 10:37:23 -07:00
Dark-Alex-17 c5a3f424d6 refactor: Reformatted code to make the format checks pass 2025-03-07 10:36:40 -07:00
Dark-Alex-17 04aa6b81b5 Merge branch 'develop' into custom-themes 2025-03-07 10:35:07 -07:00
Dark-Alex-17 5ff3b9b996 Merge branch 'main' into develop 2025-03-07 10:34:16 -07:00
Dark-Alex-17 228e4a61a4 fix: Updated ring dependency to mitigate CWE-770 2025-03-07 10:33:57 -07:00
Dark-Alex-17 df38ea5413 feat: Write built in themes to the themes file on first run so users can define custom themes 2025-03-06 17:44:52 -07:00
Dark-Alex-17 709f6ca6ca test: Added integration tests for the ValidateTheme macro 2025-03-06 16:00:50 -07:00
Dark-Alex-17 b012fc29e4 Merge branch 'develop' into custom-themes
# Conflicts:
#	Cargo.toml
2025-03-06 15:35:05 -07:00
Dark-Alex-17 bdad723aef refactor: Formatted files using rustfmt 2025-03-06 15:32:59 -07:00
Dark-Alex-17 f97d46cec3 refactor: Created a derive macro for defining the display style of Enum models and removed the use of the EnumDisplayStyle trait 2025-03-06 15:29:30 -07:00
Dark-Alex-17 7381eaef57 refactor: Expanded the serde_enum_from macro to further reduce code duplication 2025-03-05 15:09:51 -07:00
Dark-Alex-17 72c922b311 feat: Created a theme validation macro to verify theme configurations before allowing the TUI to start 2025-03-05 14:37:34 -07:00
Alex Clarke fd14a8152c fix: change the name of the theme configuration file to 'themes' 2025-03-04 18:29:21 -07:00
Dark-Alex-17 5cb60c317d feat: Initial support for custom user-defined themes 2025-03-04 18:09:09 -07:00
Dark-Alex-17 847de75713 fix: Modified the Sonarr DownloadRecord so that the episode_id is optional to prevent crashes for weird downloads 2025-03-01 14:50:20 -07:00
Dark-Alex-17 58723cf3e8 ci: Ensure the docker release is fully up-to-date 2025-02-28 21:45:05 -07:00
Dark-Alex-17 c613168bfb docs: Updated the CHANGELOG accordingly 2025-02-28 21:26:13 -07:00
github-actions[bot] 6f83de77f2 chore: Bump the version in Cargo.lock 2025-03-01 03:30:05 +00:00
github-actions[bot] 3f6ef3beb4 bump: version 0.5.0 → 0.5.1 [skip ci] 2025-03-01 03:30:04 +00:00
Dark-Alex-17 14e50c1465 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-02-28 20:29:29 -07:00
Dark-Alex-17 0aa9fdca14 ci: Overwrite previous artifact uploads for proper releases 2025-02-28 20:29:15 -07:00
266 changed files with 22754 additions and 16818 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
msrv:
# check that we can build using the minimal rust version that is specified by this crate
name: 1.82.0 / check
name: 1.89.0 / check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install 1.82.0
- name: Install 1.89.0
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.82.0
toolchain: 1.89.0
- name: cargo +1.82.0 check
- name: cargo +1.89.0 check
run: cargo check
+208 -19
View File
@@ -8,9 +8,9 @@ on:
workflow_dispatch:
inputs:
bump_type:
description: "Specify the type of version bump"
description: 'Specify the type of version bump'
required: true
default: "patch"
default: 'patch'
type: choice
options:
- patch
@@ -23,6 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Configure SSH for Git
if: env.ACT != 'true'
run: |
mkdir -p ~/.ssh
echo "${{ secrets.RELEASE_BOT_SSH_KEY }}" > ~/.ssh/id_ed25519
@@ -30,15 +31,22 @@ jobs:
ssh-keyscan -H github.com >> ~/.ssh/known_hosts
- name: Checkout repository
if: env.ACT != 'true'
uses: actions/checkout@v3
with:
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
fetch-depth: 0
- name: Checkout repository
if: env.ACT == 'true'
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: '3.10'
- name: Install Commitizen
run: |
@@ -62,12 +70,6 @@ jobs:
- name: Install Rust 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
id: version
run: |
@@ -84,14 +86,133 @@ jobs:
echo "Previous tag: $PREV_TAG"
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
id: changelog
run: |
changelog=$(conventional-changelog -p angular -i CHANGELOG.md -s --from ${{ env.prev_version }} --to ${{ env.version }})
echo "$changelog" > artifacts/changelog.md
echo "changelog_body=$(cat artifacts/changelog.md)" >> $GITHUB_ENV
conventional-changelog -p angular -i CHANGELOG.md --from ${{ env.prev_version }} --to v${{ env.version }} > artifacts/changelog.md
- name: Push changes
if: env.ACT != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
@@ -102,6 +223,17 @@ jobs:
with:
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:
name: build-release
needs: [bump-version]
@@ -129,7 +261,7 @@ jobs:
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }}
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
@@ -140,10 +272,18 @@ jobs:
fetch-depth: 1
- name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: |
git fetch --all
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
name: Cache Cargo registry
with:
@@ -246,6 +386,12 @@ jobs:
needs: [build-release-artifacts]
runs-on: ubuntu-latest
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
uses: actions/checkout@v3
with:
@@ -258,6 +404,7 @@ jobs:
merge-multiple: true
- name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: |
git fetch --all
git pull
@@ -266,10 +413,14 @@ jobs:
run: |
release_version="$(cat ./artifacts/release-version)"
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
if: env.ACT != 'true'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -300,8 +451,8 @@ jobs:
artifacts/managarr-armv7-musl.tar.gz
artifacts/managarr-armv7-musl.sha256
tag_name: v${{ env.RELEASE_VERSION }}
name: "v${{ env.RELEASE_VERSION }}"
body: ${{ env.changelog_body }}
name: 'v${{ env.RELEASE_VERSION }}'
body_path: artifacts/changelog.md
draft: false
prerelease: false
@@ -309,12 +460,19 @@ jobs:
uses: actions/upload-artifact@v4
with:
path: artifacts
overwrite: true
publish-chocolatey-package:
needs: [publish-github-release]
name: Publish Chocolatey Package
runs-on: windows-latest
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
uses: actions/checkout@v4
with:
@@ -343,6 +501,7 @@ jobs:
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Package and Publish package to Chocolatey
if: env.ACT != 'true'
run: |
mkdir ./deployment/chocolatey/tools
# Run packaging script
@@ -362,6 +521,12 @@ jobs:
name: Update Homebrew formulas
runs-on: ubuntu-latest
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
uses: actions/checkout@v4
with:
@@ -394,11 +559,13 @@ jobs:
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Execute Homebrew packaging script
if: env.ACT != 'true'
run: |
# 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 }}
- name: Push changes to Homebrew tap
if: env.ACT != 'true'
env:
TOKEN: ${{ secrets.MANAGARR_GITHUB_TOKEN }}
run: |
@@ -418,6 +585,12 @@ jobs:
name: Publishing Docker image to Docker Hub
runs-on: ubuntu-latest
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
uses: actions/checkout@v4
with:
@@ -429,6 +602,12 @@ jobs:
path: artifacts
merge-multiple: true
- name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: |
git fetch --all
git pull
- name: Set version variable
run: |
version="$(cat artifacts/release-version)"
@@ -445,6 +624,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: env.ACT != 'true'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
@@ -456,7 +636,7 @@ jobs:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
push: ${{ env.ACT != 'true' }}
tags: darkalex17/managarr:latest, darkalex17/managarr:${{ env.version }}
publish-crate:
@@ -465,7 +645,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner }}
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
@@ -475,7 +655,15 @@ jobs:
with:
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
if: env.ACT != 'true'
run: |
git fetch --all
git pull
@@ -495,5 +683,6 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- uses: katyo/publish-crates@v2
if: env.ACT != 'true'
with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+1
View File
@@ -175,6 +175,7 @@ jobs:
uses: Swatinem/rust-cache@v2
- name: Upload to codecov.io
if: env.ACT != 'true'
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
+1
View File
@@ -1,3 +1,4 @@
/target
/.idea/
/.scannerwork/
/.act/
+58 -1
View File
@@ -5,7 +5,64 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## v0.5.0 (2025-03-01)
## 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)
### Feat
+21 -1
View File
@@ -63,12 +63,32 @@ cz commit
## Setup workspace
1. Clone this repo
2. Run `cargo test` to setup hooks
2. Run `cargo test` to set up hooks
3. Make changes
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.
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
### 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!
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
+719 -463
View File
File diff suppressed because it is too large Load Diff
+20 -7
View File
@@ -1,6 +1,6 @@
[package]
name = "managarr"
version = "0.5.0"
version = "0.6.1"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI and CLI to manage your Servarrs"
keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"]
@@ -10,9 +10,15 @@ homepage = "https://github.com/Dark-Alex-17/managarr"
readme = "README.md"
edition = "2021"
license = "MIT"
rust-version = "1.82.0"
rust-version = "1.89.0"
exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
[workspace]
members = [
"proc_macros/enum_display_style_derive",
"proc_macros/validate_theme_derive",
]
[dependencies]
anyhow = "1.0.68"
backtrace = "0.3.74"
@@ -34,18 +40,23 @@ serde_json = "1.0.91"
serde = { version = "1.0.214", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
strum_macros = "0.26.4"
tokio = { version = "1.36.0", features = ["full"] }
tokio = { version = "1.44.2", features = ["full"] }
tokio-util = "0.7.8"
ratatui = { version = "0.29.0", features = [
"all-widgets",
"unstable-widget-ref",
] }
urlencoding = "2.1.2"
clap = { version = "4.5.20", features = ["derive", "cargo", "env", "wrap_help"] }
clap = { version = "4.5.20", features = [
"derive",
"cargo",
"env",
"wrap_help",
] }
clap_complete = "4.5.33"
itertools = "0.13.0"
itertools = "0.14.0"
ctrlc = "3.4.5"
colored = "2.1.0"
colored = "3.0.0"
async-trait = "0.1.83"
dirs-next = "2.0.0"
managarr-tree-widget = "0.24.0"
@@ -55,13 +66,15 @@ deunicode = "1.6.0"
paste = "1.0.15"
openssl = { version = "0.10.70", features = ["vendored"] }
veil = "0.2.0"
validate_theme_derive = "0.1.0"
enum_display_style_derive = "0.1.0"
[dev-dependencies]
assert_cmd = "2.0.16"
mockall = "0.13.0"
mockito = "1.0.0"
pretty_assertions = "1.3.0"
rstest = "0.23.0"
rstest = "0.25.0"
serial_test = "3.2.0"
[dev-dependencies.cargo-husky]
+2 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.85 AS builder
FROM rust:1.89 AS builder
WORKDIR /usr/src
# 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
COPY Cargo.* .
COPY proc_macros ./proc_macros
RUN cargo build --release
# remove src from empty project
RUN rm -r src
+1 -1
View File
@@ -8,7 +8,7 @@ default: run
.PHONY: test test-cov build run lint lint-fix fmt analyze sonar release delete-tag
test:
@cargo test
@cargo test --all
## Run all tests with coverage - `cargo install cargo-tarpaulin`
test-cov:
+40 -20
View File
@@ -28,7 +28,7 @@ Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built
- [ ] ![bazarr_logo](logos/bazarr.png) [Bazarr](https://www.bazarr.media/)
- [ ] ![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.
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.
### 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
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
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.
3. Run the executable `managarr.exe`!
#### Linux/MacOS Instructions
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.
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`!
## Features
@@ -166,21 +166,21 @@ Key:
| TUI | CLI | Feature |
|-----|-----|--------------------------------------------------------------------------------------------------------------------|
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits |
| ✅ | ✅ | View your library, downloads, blocklist, episodes |
| ✅ | ✅ | 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 |
| ✅ | ✅ | Search your library |
| ✅ | ✅ | Add series to your library |
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
| ✅ | ✅ | Manually search for series, seasons, or episodes |
| ✅ | ✅ | Edit your series and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
| ✅ | ✅ | Search your library |
| ✅ | ✅ | Add series to your library |
| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files |
| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes |
| ✅ | ✅ | Trigger refresh and disk scan for series and downloads |
| ✅ | ✅ | Manually search for series, seasons, or episodes |
| ✅ | ✅ | Edit your series and indexers |
| ✅ | ✅ | Manage your tags |
| ✅ | ✅ | Manage your root folders |
| ✅ | ✅ | Manage your blocklist |
| ✅ | ✅ | View and browse logs, tasks, events queues, and updates |
| ✅ | ✅ | Manually trigger scheduled tasks |
### Readarr
@@ -206,6 +206,19 @@ Key:
- [ ] Support for Tautulli
### Themes
Managarr ships with a few themes out of the box. Here's a few examples:
#### Default
![default](themes/default/manual_episode_search.png)
#### Dracula
![dracula](themes/dracula/manual_episode_search.png)
#### Watermelon Dark
![watermelon-dark](themes/watermelon-dark/manual_episode_search.png)
You can also create your own custom themes as well. To learn more about what themes are built-in to Managarr and how
to create your own custom themes, check out the [Themes README](themes/README.md).
### The Managarr CLI
Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs.
@@ -218,7 +231,7 @@ To see all available commands, simply run `managarr --help`:
```shell
$ managarr --help
managarr 0.5.0
managarr 0.5.1
Alex Clarke <alex.j.tusa@gmail.com>
A TUI and CLI to manage your Servarrs
@@ -235,6 +248,8 @@ Commands:
Options:
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
--config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
--themes-file <THEMES_FILE> The Managarr themes file to use [env: MANAGARR_THEMES_FILE=]
--theme <THEME> The name of the Managarr theme to use [env: MANAGARR_THEME=]
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
This is useful when you have multiple instances of the same Servarr defined in your config file.
By default, if left empty, the first configured Servarr instance listed in the config file will be used.
@@ -315,6 +330,7 @@ managarr --config-file /path/to/config.yml
### Example Configuration:
```yaml
theme: default
radarr:
- host: 192.168.0.78
port: 7878
@@ -341,6 +357,9 @@ whisparr:
port: 6969
api_token: someApiToken1234567890
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:
- host: 192.168.0.67
port: 6767
@@ -357,6 +376,7 @@ tautulli:
### Example Multi-Instance Configuration:
```yaml
theme: default
radarr:
- host: 192.168.0.78 # No name specified, so this instance's name will default to 'Radarr 1'
port: 7878
+1 -1
View File
@@ -1,7 +1,7 @@
# Documentation: https://docs.brew.sh/Formula-Cookbook
# https://rubydoc.brew.sh/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"
if OS.mac? and Hardware::CPU.arm?
url "https://github.com/Dark-Alex-17/managarr/releases/download/v$version/managarr-macos-arm64.tar.gz"
@@ -0,0 +1,18 @@
[package]
name = "enum_display_style_derive"
version = "0.6.1"
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]
proc-macro = true
[dependencies]
quote = "1.0.39"
syn = "2.0.99"
darling = "0.20.10"
@@ -0,0 +1,76 @@
mod macro_models;
use crate::macro_models::DisplayStyleArgs;
use darling::FromVariant;
use quote::quote;
use syn::{Data, DeriveInput, parse_macro_input};
/// Derive macro for generating a `to_display_str` method for an enum.
///
/// # Example
///
/// Using default values for the display style:
///
/// ```
/// use enum_display_style_derive::EnumDisplayStyle;
///
/// #[derive(EnumDisplayStyle)]
/// enum Weekend {
/// Saturday,
/// Sunday,
/// }
///
/// assert_eq!(Weekend::Saturday.to_display_str(), "Saturday");
/// assert_eq!(Weekend::Sunday.to_display_str(), "Sunday");
/// ```
///
/// Using custom values for the display style:
///
/// ```
/// use enum_display_style_derive::EnumDisplayStyle;
///
/// #[derive(EnumDisplayStyle)]
/// enum MonitorStatus {
/// #[display_style(name = "Monitor Transactions")]
/// Active,
/// #[display_style(name = "Don't Monitor Transactions")]
/// None,
/// }
///
/// assert_eq!(MonitorStatus::Active.to_display_str(), "Monitor Transactions");
/// assert_eq!(MonitorStatus::None.to_display_str(), "Don't Monitor Transactions");
/// ```
#[proc_macro_derive(EnumDisplayStyle, attributes(display_style))]
pub fn enum_display_style_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let enum_name = &input.ident;
let mut match_arms = Vec::new();
if let Data::Enum(data_enum) = &input.data {
let variants = &data_enum.variants;
for variant in variants {
let variant_ident = &variant.ident;
let variant_display_name = DisplayStyleArgs::from_variant(variant)
.unwrap()
.name
.unwrap_or_else(|| variant_ident.to_string());
match_arms.push(quote! {
#enum_name::#variant_ident => #variant_display_name,
});
}
}
quote! {
impl<'a> #enum_name {
pub fn to_display_str(self) -> &'a str {
match self {
#(#match_arms)*
}
}
}
}
.into()
}
@@ -0,0 +1,7 @@
use darling::FromVariant;
#[derive(Debug, FromVariant)]
#[darling(attributes(display_style))]
pub struct DisplayStyleArgs {
pub name: Option<String>,
}
@@ -0,0 +1,20 @@
[package]
name = "validate_theme_derive"
version = "0.6.1"
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]
proc-macro = true
[dependencies]
quote = "1.0.39"
syn = "2.0.99"
[dev-dependencies]
log = "0.4.17"
@@ -0,0 +1,106 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, parse_macro_input};
/// Derive macro for generating a `validate` method for a Theme struct.
/// The `validate` method ensures that all values with the `validate` attribute are not `None`.
/// Otherwise, an error message it output to both the log file and stdout and the program exits.
///
/// # Example
///
/// Valid themes pass through the program transitively without any messages being output.
///
/// ```
/// use validate_theme_derive::ValidateTheme;
///
/// #[derive(ValidateTheme, Default)]
/// struct Theme {
/// pub name: String,
/// #[validate]
/// pub good: Option<Style>,
/// #[validate]
/// pub bad: Option<Style>,
/// pub ugly: Option<Style>,
/// }
///
/// struct Style {
/// color: String,
/// }
///
/// let theme = Theme {
/// good: Some(Style { color: "Green".to_owned() }),
/// bad: Some(Style { color: "Red".to_owned() }),
/// ..Theme::default()
/// };
///
/// // Since only `good` and `bad` have the `validate` attribute, the `validate` method will only check those fields.
/// theme.validate();
/// // Since both `good` and `bad` have values, the program will not exit and no message is output.
/// ```
///
/// Invalid themes will output an error message to both the log file and stdout and the program will exit.
///
/// ```should_panic
/// use validate_theme_derive::ValidateTheme;
///
/// #[derive(ValidateTheme, Default)]
/// struct Theme {
/// pub name: String,
/// #[validate]
/// pub good: Option<Style>,
/// #[validate]
/// pub bad: Option<Style>,
/// pub ugly: Option<Style>,
/// }
///
/// struct Style {
/// color: String,
/// }
///
/// let theme = Theme {
/// bad: Some(Style { color: "Red".to_owned() }),
/// ..Theme::default()
/// };
///
/// // Since `good` has the `validate` attribute and since `good` is `None`, the `validate` method will output an error message and exit the program.
/// theme.validate();
/// ```
#[proc_macro_derive(ValidateTheme, attributes(validate))]
pub fn derive_validate_theme(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let mut validation_checks = Vec::new();
if let Data::Struct(data_struct) = &input.data
&& let Fields::Named(fields) = &data_struct.fields
{
for field in &fields.named {
let field_name = &field.ident;
let has_validate_attr = field
.attrs
.iter()
.any(|attr| attr.path().is_ident("validate"));
if has_validate_attr {
validation_checks.push(quote! {
if self.#field_name.is_none() {
log::error!("{} is missing a color value.", stringify!(#field_name));
eprintln!("{} is missing a color value.", stringify!(#field_name));
std::process::exit(1);
}
})
}
}
}
quote! {
impl #struct_name {
pub fn validate(&self) {
#(#validation_checks)*
}
}
}
.into()
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 220 KiB

+107 -22
View File
@@ -2,10 +2,11 @@
mod tests {
use anyhow::anyhow;
use pretty_assertions::{assert_eq, assert_str_eq};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde_json::Value;
use serial_test::serial;
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::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
@@ -31,6 +32,7 @@ mod tests {
};
let sonarr_config_2 = ServarrConfig::default();
let config = AppConfig {
theme: None,
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
};
@@ -38,40 +40,24 @@ mod tests {
TabRoute {
title: "Sonarr Test".to_owned(),
route: ActiveSonarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(sonarr_config_1),
},
TabRoute {
title: "Radarr 1".to_owned(),
route: ActiveRadarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(radarr_config_2),
},
TabRoute {
title: "Radarr Test".to_owned(),
route: ActiveRadarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(radarr_config_1),
},
TabRoute {
title: "Sonarr 1".to_owned(),
route: ActiveSonarrBlock::default().into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(sonarr_config_2),
},
@@ -97,7 +83,7 @@ mod tests {
assert!(!app.is_loading);
assert!(!app.is_routing);
assert!(!app.should_refresh);
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app.cli_mode);
}
@@ -117,7 +103,7 @@ mod tests {
assert!(!app.is_loading);
assert!(!app.is_routing);
assert!(!app.should_refresh);
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app.cli_mode);
}
@@ -283,7 +269,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -356,6 +342,43 @@ mod tests {
assert_eq!(servarr_config.api_token, Some(String::new()));
assert_eq!(servarr_config.api_token_file, 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]
@@ -399,6 +422,66 @@ mod tests {
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]
#[serial]
fn test_deserialize_optional_u16_env_var_is_present() {
@@ -512,8 +595,9 @@ mod tests {
let api_token = "thisisatest".to_owned();
let api_token_file = "/root/.config/api_token".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(\"{}\") }}",
name, host, port, uri, weight, api_token_file, ssl_cert_path);
let mut custom_headers = HeaderMap::new();
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 {
name: Some(name),
host: Some(host),
@@ -523,6 +607,7 @@ mod tests {
api_token: Some(api_token),
api_token_file: Some(api_token_file),
ssl_cert_path: Some(ssl_cert_path),
custom_headers: Some(custom_headers),
};
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::radarr::radarr_context_clues::RadarrContextClueProvider;
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
use crate::app::App;
use crate::models::Route;
#[cfg(test)]
#[path = "context_clues_tests.rs"]
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 {
context_clues
.iter()
.map(|(key_binding, desc)| format!("{} {desc}", key_binding.key))
.collect::<Vec<String>>()
.join(" | ")
pub trait ContextClueProvider {
fn get_context_clues(_app: &mut App<'_>) -> Option<&'static [ContextClue]>;
}
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.desc,
@@ -24,6 +45,7 @@ pub static SERVARR_CONTEXT_CLUES: [ContextClue; 3] = [
DEFAULT_KEYBINDINGS.previous_servarr.desc,
),
(DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc),
(DEFAULT_KEYBINDINGS.help, DEFAULT_KEYBINDINGS.help.desc),
];
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 crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
ContextClueProvider, ServarrContextClueProvider, BARE_POPUP_CONTEXT_CLUES,
BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_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};
#[test]
fn test_build_context_clue_string() {
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"
);
}
use crate::app::{key_binding::DEFAULT_KEYBINDINGS, App};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::servarr_data::ActiveKeybindingBlock;
#[test]
fn test_servarr_context_clues() {
@@ -28,6 +19,36 @@ mod test {
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_str_eq!(*description, DEFAULT_KEYBINDINGS.next_servarr.desc);
@@ -40,6 +61,11 @@ mod test {
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.quit);
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);
}
@@ -209,4 +235,42 @@ mod test {
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(system_context_clues_iter.next(), None);
}
#[test]
fn test_servarr_context_clue_provider_delegates_to_radarr_provider() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(
&crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
context_clues.unwrap()
);
}
#[test]
fn test_servarr_context_clue_provider_delegates_to_sonarr_provider() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into());
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_some());
assert_eq!(
&crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES,
context_clues.unwrap()
);
}
#[test]
fn test_servarr_context_clue_provider_unsupported_route_returns_none() {
let mut app = App::test_default();
app.push_navigation_stack(ActiveKeybindingBlock::Help.into());
let context_clues = ServarrContextClueProvider::get_context_clues(&mut app);
assert!(context_clues.is_none());
}
}
+79 -1
View File
@@ -14,6 +14,8 @@ generate_keybindings! {
down,
left,
right,
pg_down,
pg_up,
backspace,
next_servarr,
previous_servarr,
@@ -21,6 +23,7 @@ generate_keybindings! {
search,
auto_search,
settings,
help,
filter,
sort,
edit,
@@ -44,128 +47,203 @@ generate_keybindings! {
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct KeyBinding {
pub key: Key,
pub alt: Option<Key>,
pub desc: &'static str,
}
pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
add: KeyBinding {
key: Key::Char('a'),
alt: None,
desc: "add",
},
up: KeyBinding {
key: Key::Up,
alt: Some(Key::Char('k')),
desc: "up",
},
down: KeyBinding {
key: Key::Down,
alt: Some(Key::Char('j')),
desc: "down",
},
left: KeyBinding {
key: Key::Left,
alt: Some(Key::Char('h')),
desc: "left",
},
right: KeyBinding {
key: Key::Right,
alt: Some(Key::Char('l')),
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 {
key: Key::Backspace,
alt: Some(Key::Ctrl('h')),
desc: "backspace",
},
next_servarr: KeyBinding {
key: Key::Tab,
alt: None,
desc: "next servarr",
},
previous_servarr: KeyBinding {
key: Key::BackTab,
alt: None,
desc: "previous servarr",
},
clear: KeyBinding {
key: Key::Char('c'),
alt: None,
desc: "clear",
},
auto_search: KeyBinding {
key: Key::Char('S'),
alt: None,
desc: "auto search",
},
search: KeyBinding {
key: Key::Char('s'),
alt: None,
desc: "search",
},
settings: KeyBinding {
key: Key::Char('S'),
alt: None,
desc: "settings",
},
help: KeyBinding {
key: Key::Char('?'),
alt: None,
desc: "show/hide keybindings",
},
filter: KeyBinding {
key: Key::Char('f'),
alt: None,
desc: "filter",
},
sort: KeyBinding {
key: Key::Char('o'),
alt: None,
desc: "sort",
},
edit: KeyBinding {
key: Key::Char('e'),
alt: None,
desc: "edit",
},
events: KeyBinding {
key: Key::Char('e'),
alt: None,
desc: "events",
},
logs: KeyBinding {
key: Key::Char('l'),
key: Key::Char('L'),
alt: None,
desc: "logs",
},
tasks: KeyBinding {
key: Key::Char('t'),
alt: None,
desc: "tasks",
},
test: KeyBinding {
key: Key::Char('t'),
alt: None,
desc: "test",
},
test_all: KeyBinding {
key: Key::Char('T'),
alt: None,
desc: "test all",
},
toggle_monitoring: KeyBinding {
key: Key::Char('m'),
alt: None,
desc: "toggle monitoring",
},
refresh: KeyBinding {
key: Key::Ctrl('r'),
alt: None,
desc: "refresh",
},
update: KeyBinding {
key: Key::Char('u'),
alt: None,
desc: "update",
},
home: KeyBinding {
key: Key::Home,
alt: None,
desc: "home",
},
end: KeyBinding {
key: Key::End,
alt: None,
desc: "end",
},
delete: KeyBinding {
key: Key::Delete,
alt: None,
desc: "delete",
},
submit: KeyBinding {
key: Key::Enter,
alt: None,
desc: "submit",
},
confirm: KeyBinding {
key: Key::Ctrl('s'),
alt: None,
desc: "submit",
},
quit: KeyBinding {
key: Key::Char('q'),
alt: None,
desc: "quit",
},
esc: KeyBinding {
key: Key::Esc,
alt: None,
desc: "close",
},
};
#[macro_export]
macro_rules! matches_key {
($binding:ident, $key:expr) => {
$crate::app::key_binding::DEFAULT_KEYBINDINGS.$binding.key == $key
|| ($crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.is_some()
&& $crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.unwrap()
== $key)
};
($binding:ident, $key:expr, $ignore_special_keys:expr) => {
$crate::app::key_binding::DEFAULT_KEYBINDINGS.$binding.key == $key
|| !$ignore_special_keys
&& ($crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.is_some()
&& $crate::app::key_binding::DEFAULT_KEYBINDINGS
.$binding
.alt
.unwrap()
== $key)
};
}
+74 -30
View File
@@ -5,44 +5,88 @@ mod test {
use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS};
use crate::event::Key;
use crate::matches_key;
#[rstest]
#[case(DEFAULT_KEYBINDINGS.add, Key::Char('a'), "add")]
#[case(DEFAULT_KEYBINDINGS.up, Key::Up, "up")]
#[case(DEFAULT_KEYBINDINGS.down, Key::Down, "down")]
#[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")]
#[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")]
#[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, "backspace")]
#[case(DEFAULT_KEYBINDINGS.next_servarr, Key::Tab, "next servarr")]
#[case(DEFAULT_KEYBINDINGS.previous_servarr, Key::BackTab, "previous servarr")]
#[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), "clear")]
#[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), "auto search")]
#[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), "search")]
#[case(DEFAULT_KEYBINDINGS.settings, Key::Char('S'), "settings")]
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), "filter")]
#[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), "sort")]
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), "edit")]
#[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), "events")]
#[case(DEFAULT_KEYBINDINGS.logs, Key::Char('l'), "logs")]
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), "tasks")]
#[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), "test")]
#[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('T'), "test all")]
#[case(DEFAULT_KEYBINDINGS.toggle_monitoring, Key::Char('m'), "toggle monitoring")]
#[case(DEFAULT_KEYBINDINGS.refresh, Key::Ctrl('r'), "refresh")]
#[case(DEFAULT_KEYBINDINGS.update, Key::Char('u'), "update")]
#[case(DEFAULT_KEYBINDINGS.home, Key::Home, "home")]
#[case(DEFAULT_KEYBINDINGS.end, Key::End, "end")]
#[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, "delete")]
#[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, "submit")]
#[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), "submit")]
#[case(DEFAULT_KEYBINDINGS.quit, Key::Char('q'), "quit")]
#[case(DEFAULT_KEYBINDINGS.esc, Key::Esc, "close")]
#[case(DEFAULT_KEYBINDINGS.add, Key::Char('a'), None, "add")]
#[case(DEFAULT_KEYBINDINGS.up, Key::Up, Some(Key::Char('k')), "up")]
#[case(DEFAULT_KEYBINDINGS.down, Key::Down, Some(Key::Char('j')), "down")]
#[case(DEFAULT_KEYBINDINGS.left, Key::Left, Some(Key::Char('h')), "left")]
#[case(DEFAULT_KEYBINDINGS.right, Key::Right, Some(Key::Char('l')), "right")]
#[case(DEFAULT_KEYBINDINGS.pg_down, Key::PgDown, Some(Key::Ctrl('d')), "page down")]
#[case(DEFAULT_KEYBINDINGS.pg_up, Key::PgUp, Some(Key::Ctrl('u')), "page up")]
#[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, Some(Key::Ctrl('h')), "backspace")]
#[case(DEFAULT_KEYBINDINGS.next_servarr, Key::Tab, None, "next servarr")]
#[case(DEFAULT_KEYBINDINGS.previous_servarr, Key::BackTab, None, "previous servarr")]
#[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), None, "clear")]
#[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), None, "auto search")]
#[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), None, "search")]
#[case(DEFAULT_KEYBINDINGS.settings, Key::Char('S'), None, "settings")]
#[case(DEFAULT_KEYBINDINGS.help, Key::Char('?'), None, "show/hide keybindings")]
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), None, "filter")]
#[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), None, "sort")]
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), None, "edit")]
#[case(DEFAULT_KEYBINDINGS.events, Key::Char('e'), None, "events")]
#[case(DEFAULT_KEYBINDINGS.logs, Key::Char('L'), None, "logs")]
#[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), None, "tasks")]
#[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), None, "test")]
#[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('T'), None, "test all")]
#[case(DEFAULT_KEYBINDINGS.toggle_monitoring, Key::Char('m'), None, "toggle monitoring")]
#[case(DEFAULT_KEYBINDINGS.refresh, Key::Ctrl('r'), None, "refresh")]
#[case(DEFAULT_KEYBINDINGS.update, Key::Char('u'), None, "update")]
#[case(DEFAULT_KEYBINDINGS.home, Key::Home, None, "home")]
#[case(DEFAULT_KEYBINDINGS.end, Key::End, None, "end")]
#[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, None, "delete")]
#[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(
#[case] key_binding: KeyBinding,
#[case] expected_key: Key,
#[case] expected_alt_key: Option<Key>,
#[case] expected_desc: &str,
) {
assert_eq!(key_binding.key, expected_key);
assert_eq!(key_binding.alt, expected_alt_key);
assert_str_eq!(key_binding.desc, expected_desc);
}
#[test]
fn test_matches_key_macro() {
let key = Key::Char('t');
assert!(matches_key!(test, key));
assert!(!matches_key!(test, Key::Char('T')));
}
#[test]
fn test_matches_key_macro_with_alt_keybinding() {
let alt_key = Key::Char('k');
let key = Key::Up;
assert!(matches_key!(up, key));
assert!(matches_key!(up, alt_key));
assert!(!matches_key!(up, Key::Char('t')));
}
#[test]
fn test_matches_key_macro_with_alt_keybinding_uses_alt_key_when_ignore_special_keys_is_false() {
let alt_key = Key::Char('k');
let key = Key::Up;
assert!(matches_key!(up, key, false));
assert!(matches_key!(up, alt_key, false));
assert!(!matches_key!(up, Key::Char('t'), false));
}
#[test]
fn test_matches_key_macro_with_alt_keybinding_ignores_alt_key_when_ignore_special_keys_is_true() {
let alt_key = Key::Char('k');
let key = Key::Up;
assert!(matches_key!(up, key, true));
assert!(!matches_key!(up, alt_key, true));
assert!(!matches_key!(up, Key::Char('t'), true));
}
}
+64 -24
View File
@@ -3,17 +3,20 @@ use colored::Colorize;
use itertools::Itertools;
use log::{debug, error};
use regex::Regex;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::{fs, process};
use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken;
use veil::Redact;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::cli::Command;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
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::network::NetworkEvent;
@@ -32,6 +35,7 @@ pub struct App<'a> {
pub cancellation_token: CancellationToken,
pub is_first_render: bool,
pub server_tabs: TabState,
pub keymapping_table: Option<StatefulTable<KeybindingItem>>,
pub error: HorizontallyScrollableText,
pub tick_until_poll: u64,
pub ticks_until_scroll: u64,
@@ -39,7 +43,7 @@ pub struct App<'a> {
pub is_routing: bool,
pub is_loading: bool,
pub should_refresh: bool,
pub should_ignore_quit_key: bool,
pub ignore_special_keys_for_textbox_input: bool,
pub cli_mode: bool,
pub data: Data<'a>,
}
@@ -51,10 +55,6 @@ impl App<'_> {
cancellation_token: CancellationToken,
) -> Self {
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 {
let mut idx = 0;
@@ -63,13 +63,12 @@ impl App<'_> {
name
} else {
idx += 1;
format!("Radarr {}", idx)
format!("Radarr {idx}")
};
server_tabs.push(TabRoute {
title: name,
route: ActiveRadarrBlock::Movies.into(),
help: help.clone(),
contextual_help: None,
config: Some(radarr_config),
});
@@ -84,13 +83,12 @@ impl App<'_> {
name
} else {
idx += 1;
format!("Sonarr {}", idx)
format!("Sonarr {idx}")
};
server_tabs.push(TabRoute {
title: name,
route: ActiveSonarrBlock::Series.into(),
help: help.clone(),
contextual_help: None,
config: Some(sonarr_config),
});
@@ -215,6 +213,7 @@ impl Default for App<'_> {
navigation_stack: Vec::new(),
network_tx: None,
cancellation_token: CancellationToken::new(),
keymapping_table: None,
error: HorizontallyScrollableText::default(),
is_first_render: true,
server_tabs: TabState::new(Vec::new()),
@@ -224,7 +223,7 @@ impl Default for App<'_> {
is_loading: false,
is_routing: false,
should_refresh: false,
should_ignore_quit_key: false,
ignore_special_keys_for_textbox_input: false,
cli_mode: false,
data: Data::default(),
}
@@ -239,20 +238,12 @@ impl App<'_> {
TabRoute {
title: "Radarr".to_owned(),
route: ActiveRadarrBlock::Movies.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
TabRoute {
title: "Sonarr".to_owned(),
route: ActiveSonarrBlock::Series.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None,
config: Some(ServarrConfig::default()),
},
@@ -270,6 +261,7 @@ pub struct Data<'a> {
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct AppConfig {
pub theme: Option<String>,
pub radarr: Option<Vec<ServarrConfig>>,
pub sonarr: Option<Vec<ServarrConfig>>,
}
@@ -295,8 +287,7 @@ impl AppConfig {
pub fn verify_config_present_for_cli(&self, command: &Command) {
let msg = |servarr: &str| {
log_and_print_error(format!(
"{} configuration missing; Unable to run any {} commands.",
servarr, servarr
"{servarr} configuration missing; Unable to run any {servarr} commands."
))
};
match command {
@@ -346,6 +337,12 @@ pub struct ServarrConfig {
pub api_token_file: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
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 {
@@ -367,8 +364,7 @@ impl ServarrConfig {
if let Some(api_token_file) = self.api_token_file.as_ref() {
if !PathBuf::from(api_token_file).exists() {
log_and_print_error(format!(
"The specified {} API token file does not exist",
api_token_file
"The specified {api_token_file} API token file does not exist"
));
process::exit(1);
}
@@ -392,15 +388,37 @@ impl Default for ServarrConfig {
api_token: Some(String::new()),
api_token_file: None,
ssl_cert_path: None,
custom_headers: None,
}
}
}
pub fn log_and_print_error(error: String) {
error!("{}", error);
error!("{error}");
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>
where
D: serde::Deserializer<'de>,
@@ -415,6 +433,28 @@ where
}
}
fn deserialize_optional_env_var_header_map<'de, D>(
deserializer: D,
) -> Result<Option<HeaderMap>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<HashMap<String, String>> = Option::deserialize(deserializer)?;
match opt {
Some(map) => {
let mut header_map = HeaderMap::new();
for (k, v) in map.iter() {
let name = HeaderName::from_bytes(k.as_bytes()).map_err(serde::de::Error::custom)?;
let value_str = interpolate_env_vars(v);
let value = HeaderValue::from_str(&value_str).map_err(serde::de::Error::custom)?;
header_map.insert(name, value);
}
Ok(Some(header_map))
}
None => Ok(None),
}
}
fn deserialize_u16_env_var<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
where
D: serde::Deserializer<'de>,
+3 -3
View File
@@ -40,7 +40,7 @@ impl App<'_> {
}
ActiveRadarrBlock::Downloads => {
self
.dispatch_network_event(RadarrEvent::GetDownloads.into())
.dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await;
}
ActiveRadarrBlock::RootFolders => {
@@ -59,7 +59,7 @@ impl App<'_> {
.dispatch_network_event(RadarrEvent::GetMovies.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetDownloads.into())
.dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await;
}
ActiveRadarrBlock::Indexers => {
@@ -201,7 +201,7 @@ impl App<'_> {
.dispatch_network_event(RadarrEvent::GetRootFolders.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetDownloads.into())
.dispatch_network_event(RadarrEvent::GetDownloads(500).into())
.await;
self
.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::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)]
#[path = "radarr_context_clues_tests.rs"]
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.edit, DEFAULT_KEYBINDINGS.edit.desc),
(
DEFAULT_KEYBINDINGS.toggle_monitoring,
DEFAULT_KEYBINDINGS.toggle_monitoring.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.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),
];
pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 6] = [
pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
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.desc,
),
(DEFAULT_KEYBINDINGS.submit, "details"),
(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] = [
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "edit search"),
@@ -82,3 +92,54 @@ pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.edit, "edit collection"),
(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)]
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::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
RadarrContextClueProvider, ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_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]
fn test_library_context_clues() {
@@ -26,6 +33,11 @@ mod tests {
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_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();
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_str_eq!(*description, "details");
assert_eq!(
manual_movie_search_contextual_context_clues_iter.next(),
None
);
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]
@@ -249,4 +250,255 @@ mod tests {
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
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_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -186,7 +186,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -591,7 +591,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -625,7 +625,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -650,7 +650,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(!app.data.radarr_data.prompt_confirm);
}
@@ -675,7 +675,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(app.should_refresh);
assert!(!app.data.radarr_data.prompt_confirm);
@@ -692,7 +692,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
assert!(app.should_refresh);
@@ -722,7 +722,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
RadarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
}
+3 -3
View File
@@ -53,7 +53,7 @@ impl App<'_> {
)
.await;
self
.dispatch_network_event(SonarrEvent::GetDownloads.into())
.dispatch_network_event(SonarrEvent::GetDownloads(500).into())
.await;
}
ActiveSonarrBlock::SeasonHistory => {
@@ -108,7 +108,7 @@ impl App<'_> {
}
ActiveSonarrBlock::Downloads => {
self
.dispatch_network_event(SonarrEvent::GetDownloads.into())
.dispatch_network_event(SonarrEvent::GetDownloads(500).into())
.await;
}
ActiveSonarrBlock::Blocklist => {
@@ -234,7 +234,7 @@ impl App<'_> {
.dispatch_network_event(SonarrEvent::GetRootFolders.into())
.await;
self
.dispatch_network_event(SonarrEvent::GetDownloads.into())
.dispatch_network_event(SonarrEvent::GetDownloads(500).into())
.await;
self
.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)]
#[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"),
];
pub static SERIES_CONTEXT_CLUES: [ContextClue; 10] = [
pub static SERIES_CONTEXT_CLUES: [ContextClue; 11] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.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.delete, DEFAULT_KEYBINDINGS.delete.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"),
];
pub static SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "episode details"),
(DEFAULT_KEYBINDINGS.delete, "delete episode"),
];
pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 7] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
@@ -95,9 +102,11 @@ pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
DEFAULT_KEYBINDINGS.auto_search.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.desc,
@@ -109,10 +118,11 @@ pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.submit, "details"),
(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.desc,
@@ -122,10 +132,11 @@ pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(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.desc,
@@ -135,12 +146,10 @@ pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(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] = [
(
DEFAULT_KEYBINDINGS.refresh,
@@ -153,7 +162,84 @@ pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(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] = [
(DEFAULT_KEYBINDINGS.submit, "start task"),
(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)]
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::{
key_binding::DEFAULT_KEYBINDINGS,
sonarr::sonarr_context_clues::{
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, DETAILS_CONTEXTUAL_CONTEXT_CLUES,
EPISODE_DETAILS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES,
MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES,
SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES,
SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES,
HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES,
MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES,
SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_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]
fn test_add_series_search_results_context_clues() {
@@ -46,6 +58,11 @@ mod tests {
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_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
@@ -247,23 +264,18 @@ mod tests {
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(season_details_context_clues_iter.next(), None);
}
#[test]
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();
let (key_binding, description) = season_details_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
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_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]
@@ -296,6 +308,11 @@ mod tests {
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_str_eq!(*description, "cancel filter/close");
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();
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_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
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();
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_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
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]
fn test_episode_details_context_clues() {
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);
}
#[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]
fn test_system_tasks_context_clues() {
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_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!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -366,7 +366,7 @@ mod tests {
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
@@ -604,7 +604,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -642,7 +642,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -667,7 +667,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(!app.data.sonarr_data.prompt_confirm);
}
@@ -692,7 +692,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(app.should_refresh);
assert!(!app.data.sonarr_data.prompt_confirm);
@@ -709,7 +709,7 @@ mod tests {
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
assert!(app.should_refresh);
@@ -743,7 +743,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
SonarrEvent::GetDownloads.into()
SonarrEvent::GetDownloads(500).into()
);
assert!(app.is_loading);
}
+6 -3
View File
@@ -23,7 +23,10 @@ pub enum RadarrListCommand {
#[command(about = "List all Radarr collections")]
Collections,
#[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")]
DiskSpace,
#[command(about = "List all Radarr indexers")]
@@ -104,10 +107,10 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrListCommand> for RadarrListCommandH
.await?;
serde_json::to_string_pretty(&resp)?
}
RadarrListCommand::Downloads => {
RadarrListCommand::Downloads { count } => {
let resp = self
.network
.handle_network_event(RadarrEvent::GetDownloads.into())
.handle_network_event(RadarrEvent::GetDownloads(count).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
+47 -2
View File
@@ -29,7 +29,6 @@ mod tests {
#[values(
"blocklist",
"collections",
"downloads",
"disk-space",
"indexers",
"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]
fn test_list_logs_events_flag_requires_arguments() {
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]
fn test_list_logs_default_values() {
let expected_args = RadarrListCommand::Logs {
@@ -122,7 +142,6 @@ mod tests {
#[rstest]
#[case(RadarrListCommand::Blocklist, RadarrEvent::GetBlocklist)]
#[case(RadarrListCommand::Collections, RadarrEvent::GetCollections)]
#[case(RadarrListCommand::Downloads, RadarrEvent::GetDownloads)]
#[case(RadarrListCommand::DiskSpace, RadarrEvent::GetDiskSpace)]
#[case(RadarrListCommand::Indexers, RadarrEvent::GetIndexers)]
#[case(RadarrListCommand::Movies, RadarrEvent::GetMovies)]
@@ -182,6 +201,32 @@ mod tests {
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]
async fn test_handle_list_logs_command() {
let expected_events = 1000;
+18
View File
@@ -118,6 +118,17 @@ pub enum RadarrCommand {
},
#[command(about = "Test all Radarr indexers")]
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")]
TriggerAutomaticSearch {
#[arg(
@@ -250,6 +261,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, RadarrCommand> for RadarrCliHandler<'a, '
.await?;
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 } => {
let resp = self
.network
+51
View File
@@ -215,6 +215,31 @@ mod tests {
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]
fn test_trigger_automatic_search_requires_movie_id() {
let result =
@@ -461,6 +486,32 @@ mod tests {
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]
async fn test_trigger_automatic_search_command() {
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")]
Blocklist,
#[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")]
DiskSpace,
#[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?;
serde_json::to_string_pretty(&resp)?
}
SonarrListCommand::Downloads => {
SonarrListCommand::Downloads { count } => {
let resp = self
.network
.handle_network_event(SonarrEvent::GetDownloads.into())
.handle_network_event(SonarrEvent::GetDownloads(count).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
+48 -2
View File
@@ -28,7 +28,6 @@ mod tests {
#[values(
"blocklist",
"series",
"downloads",
"disk-space",
"quality-profiles",
"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]
fn test_list_history_events_flag_requires_arguments() {
let result =
@@ -287,7 +308,6 @@ mod tests {
#[rstest]
#[case(SonarrListCommand::Blocklist, SonarrEvent::GetBlocklist)]
#[case(SonarrListCommand::Downloads, SonarrEvent::GetDownloads)]
#[case(SonarrListCommand::DiskSpace, SonarrEvent::GetDiskSpace)]
#[case(SonarrListCommand::Indexers, SonarrEvent::GetIndexers)]
#[case(SonarrListCommand::QualityProfiles, SonarrEvent::GetQualityProfiles)]
@@ -374,6 +394,32 @@ mod tests {
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]
async fn test_handle_list_history_command() {
let expected_events = 1000;
+18
View File
@@ -146,6 +146,17 @@ pub enum SonarrCommand {
)]
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 {
@@ -290,6 +301,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, '
.await?;
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)
+56 -2
View File
@@ -216,6 +216,31 @@ mod tests {
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 {
@@ -692,7 +717,7 @@ mod tests {
}
#[tokio::test]
async fn test_list_toggle_episode_monitoring_command() {
async fn test_toggle_episode_monitoring_command() {
let expected_episode_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
@@ -722,7 +747,7 @@ mod tests {
}
#[tokio::test]
async fn test_list_toggle_season_monitoring_command() {
async fn test_toggle_season_monitoring_command() {
let expected_series_id = 1;
let expected_season_number = 1;
let mut mock_network = MockNetworkTrait::new();
@@ -753,5 +778,34 @@ mod tests {
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 crossterm::event;
use crossterm::event::Event as CrosstermEvent;
use crossterm::event::{Event as CrosstermEvent, KeyEventKind};
use crate::event::Key;
@@ -30,8 +30,12 @@ impl Events {
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout).unwrap() {
if let CrosstermEvent::Key(key_event) = event::read().unwrap() {
let key = Key::from(key_event);
tx.send(InputEvent::KeyEvent(key)).unwrap();
// Only process the key event if it's a press event
// 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,
Left,
Right,
PgDown,
PgUp,
Enter,
Esc,
Backspace,
@@ -29,21 +31,23 @@ pub enum Key {
impl Display for Key {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Key::Char(c) => write!(f, "<{c}>"),
Key::Ctrl(c) => write!(f, "<ctrl-{c}>"),
Key::Up => write!(f, "<↑>"),
Key::Down => write!(f, "<↓>"),
Key::Left => write!(f, "<←>"),
Key::Right => write!(f, "<→>"),
Key::Enter => write!(f, "<enter>"),
Key::Esc => write!(f, "<esc>"),
Key::Backspace => write!(f, "<backspace>"),
Key::Home => write!(f, "<home>"),
Key::End => write!(f, "<end>"),
Key::Tab => write!(f, "<tab>"),
Key::BackTab => write!(f, "<shift-tab>"),
Key::Delete => write!(f, "<del>"),
_ => write!(f, "<{self:?}>"),
Key::Char(c) => write!(f, "{c}"),
Key::Ctrl(c) => write!(f, "ctrl-{c}"),
Key::Up => write!(f, ""),
Key::Down => write!(f, ""),
Key::Left => write!(f, ""),
Key::Right => write!(f, ""),
Key::PgDown => write!(f, "pgDown"),
Key::PgUp => write!(f, "pgUp"),
Key::Enter => write!(f, "enter"),
Key::Esc => write!(f, "esc"),
Key::Backspace => write!(f, "backspace"),
Key::Home => write!(f, "home"),
Key::End => write!(f, "end"),
Key::Tab => write!(f, "tab"),
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,
..
} => Key::Right,
KeyEvent {
code: KeyCode::PageDown,
..
} => Key::PgDown,
KeyEvent {
code: KeyCode::PageUp,
..
} => Key::PgUp,
KeyEvent {
code: KeyCode::Backspace,
..
+13 -1
View File
@@ -11,6 +11,8 @@ mod tests {
#[case(Key::Down, "")]
#[case(Key::Left, "")]
#[case(Key::Right, "")]
#[case(Key::PgDown, "pgDown")]
#[case(Key::PgUp, "pgUp")]
#[case(Key::Enter, "enter")]
#[case(Key::Esc, "esc")]
#[case(Key::Backspace, "backspace")]
@@ -22,7 +24,7 @@ mod tests {
#[case(Key::Char('q'), "q")]
#[case(Key::Ctrl('q'), "ctrl-q")]
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]
@@ -45,6 +47,16 @@ mod tests {
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]
fn test_key_from_backspace() {
assert_eq!(
+177 -3
View File
@@ -6,13 +6,20 @@ mod tests {
use rstest::rstest;
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::event::Key;
use crate::handlers::handle_events;
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::ActiveKeybindingBlock;
use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable;
use crate::models::HorizontallyScrollableText;
use crate::models::Route;
@@ -82,6 +89,82 @@ mod tests {
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]
fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::test_default();
@@ -113,4 +196,95 @@ mod tests {
assert!(!app.data.sonarr_data.prompt_confirm);
}
#[test]
fn test_populate_keymapping_table_global_options() {
let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| {
let (key, alt_key) = if key.alt.is_some() {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
} else {
(key.key.to_string(), String::new())
};
KeybindingItem {
key,
alt_key,
desc: desc.to_string(),
}
})
.collect::<Vec<_>>();
let mut app = App::test_default();
app.push_navigation_stack(ActiveKeybindingBlock::Help.into());
populate_keymapping_table(&mut app);
assert!(app.keymapping_table.is_some());
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
#[test]
fn test_populate_keymapping_table_populates_servarr_specific_tab_info_before_global_options() {
let mut expected_keybinding_items = LIBRARY_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
expected_keybinding_items.extend(
SERVARR_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(ActiveRadarrBlock::default().into());
populate_keymapping_table(&mut app);
assert!(app.keymapping_table.is_some());
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
#[test]
fn test_populate_keymapping_table_populates_delegated_servarr_context_provider_options_before_global_options(
) {
let mut expected_keybinding_items = MOVIE_DETAILS_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
expected_keybinding_items.extend(
SERVARR_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
let mut app = App::test_default();
app.data.radarr_data = RadarrData::default();
app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into());
populate_keymapping_table(&mut app);
assert!(app.keymapping_table.is_some());
assert_eq!(
expected_keybinding_items,
app.keymapping_table.unwrap().items
);
}
fn context_clue_to_keybinding_item(key: &KeyBinding, desc: &&str) -> KeybindingItem {
let (key, alt_key) = if key.alt.is_some() {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
} else {
(key.key.to_string(), String::new())
};
KeybindingItem {
key,
alt_key,
desc: desc.to_string(),
}
}
}
+80
View File
@@ -0,0 +1,80 @@
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::KeyEventHandler;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_models::KeybindingItem;
#[cfg(test)]
#[path = "keybinding_handler_tests.rs"]
mod keybinding_handler_tests;
pub(super) struct KeybindingHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
}
impl KeybindingHandler<'_, '_> {
handle_table_events!(
self,
keybindings,
self.app.keymapping_table.as_mut().unwrap(),
KeybindingItem
);
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandler<'a, 'b> {
fn handle(&mut self) {
let keybinding_table_handling_config = TableHandlingConfig::new(self.app.get_current_route());
if !self.handle_keybindings_table_events(keybinding_table_handling_config) {
self.handle_key_event();
}
}
fn accepts(_active_block: ActiveKeybindingBlock) -> bool {
true
}
fn new(
key: Key,
app: &'a mut App<'b>,
_active_block: ActiveKeybindingBlock,
_context: Option<ActiveKeybindingBlock>,
) -> KeybindingHandler<'a, 'b> {
KeybindingHandler { key, app }
}
fn get_key(&self) -> Key {
self.key
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn is_ready(&self) -> bool {
self.app.keymapping_table.is_some()
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_down(&mut self) {}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {}
fn handle_submit(&mut self) {}
fn handle_esc(&mut self) {
self.app.keymapping_table = None;
}
fn handle_char_key_event(&mut self) {}
}
+69
View File
@@ -0,0 +1,69 @@
#[cfg(test)]
mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::KeyEventHandler;
use crate::handlers::KeybindingHandler;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::stateful_table::StatefulTable;
use rstest::rstest;
mod test_handle_esc {
use super::*;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use pretty_assertions::assert_eq;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_esc_empties_keymapping_table() {
let mut app = App::test_default();
app.is_loading = true;
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
KeybindingHandler::new(ESC_KEY, &mut app, ActiveKeybindingBlock::Help, None).handle();
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
assert!(app.keymapping_table.is_none());
}
}
#[test]
fn test_keybinding_handler_accepts() {
assert!(KeybindingHandler::accepts(ActiveKeybindingBlock::Help));
}
#[test]
fn test_keybinding_handler_not_ready_when_keybinding_is_empty() {
let mut app = App::test_default();
app.is_loading = false;
let handler = KeybindingHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveKeybindingBlock::Help,
None,
);
assert!(!handler.is_ready());
}
#[rstest]
fn test_keybinding_handler_ready_when_keymapping_table_is_not_empty(
#[values(true, false)] is_loading: bool,
) {
let mut app = App::test_default();
app.keymapping_table = Some(StatefulTable::default());
app.is_loading = is_loading;
let handler = KeybindingHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveKeybindingBlock::Help,
None,
);
assert!(handler.is_ready());
}
}
+84 -23
View File
@@ -1,11 +1,20 @@
use radarr_handlers::RadarrHandler;
use sonarr_handlers::SonarrHandler;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::context_clues::{
ContextClueProvider, ServarrContextClueProvider, SERVARR_CONTEXT_CLUES,
};
use crate::app::key_binding::KeyBinding;
use crate::app::App;
use crate::event::Key;
use crate::handlers::keybinding_handler::KeybindingHandler;
use crate::matches_key;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, Route};
mod keybinding_handler;
mod radarr_handlers;
mod sonarr_handlers;
@@ -22,40 +31,42 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
fn handle_key_event(&mut self) {
let key = self.get_key();
match key {
_ if key == DEFAULT_KEYBINDINGS.up.key => {
_ if matches_key!(up, key, self.ignore_special_keys()) => {
if self.is_ready() {
self.handle_scroll_up();
}
}
_ if key == DEFAULT_KEYBINDINGS.down.key => {
_ if matches_key!(down, key, self.ignore_special_keys()) => {
if self.is_ready() {
self.handle_scroll_down();
}
}
_ if key == DEFAULT_KEYBINDINGS.home.key => {
_ if matches_key!(home, key) => {
if self.is_ready() {
self.handle_home();
}
}
_ if key == DEFAULT_KEYBINDINGS.end.key => {
_ if matches_key!(end, key) => {
if self.is_ready() {
self.handle_end();
}
}
_ if key == DEFAULT_KEYBINDINGS.delete.key => {
_ if matches_key!(delete, key) => {
if self.is_ready() {
self.handle_delete();
}
}
_ if key == DEFAULT_KEYBINDINGS.left.key || key == DEFAULT_KEYBINDINGS.right.key => {
_ if matches_key!(left, key, self.ignore_special_keys())
|| matches_key!(right, key, self.ignore_special_keys()) =>
{
self.handle_left_right_action()
}
_ if key == DEFAULT_KEYBINDINGS.submit.key => {
_ if matches_key!(submit, key) => {
if self.is_ready() {
self.handle_submit();
}
}
_ if key == DEFAULT_KEYBINDINGS.esc.key => self.handle_esc(),
_ if matches_key!(esc, key) => self.handle_esc(),
_ => {
if self.is_ready() {
self.handle_char_key_event();
@@ -71,6 +82,7 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
fn accepts(active_block: T) -> bool;
fn new(key: Key, app: &'a mut App<'b>, active_block: T, context: Option<T>) -> Self;
fn get_key(&self) -> Key;
fn ignore_special_keys(&self) -> bool;
fn is_ready(&self) -> bool;
fn handle_scroll_up(&mut self);
fn handle_scroll_down(&mut self);
@@ -84,18 +96,27 @@ pub trait KeyEventHandler<'a, 'b, T: Into<Route> + Copy> {
}
pub fn handle_events(key: Key, app: &mut App<'_>) {
if key == DEFAULT_KEYBINDINGS.next_servarr.key {
if matches_key!(next_servarr, key) {
app.reset();
app.server_tabs.next();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
app.cancellation_token.cancel();
} else if key == DEFAULT_KEYBINDINGS.previous_servarr.key {
} else if matches_key!(previous_servarr, key) {
app.reset();
app.server_tabs.previous();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
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 {
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) => {
RadarrHandler::new(key, app, active_radarr_block, context).handle()
}
@@ -107,6 +128,48 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
}
}
fn populate_keymapping_table(app: &mut App<'_>) {
let context_clue_to_keybinding_item = |key: &KeyBinding, desc: &&str| {
let (key, alt_key) = if key.alt.is_some() {
(key.key.to_string(), key.alt.as_ref().unwrap().to_string())
} else {
(key.key.to_string(), String::new())
};
KeybindingItem {
key,
alt_key,
desc: desc.to_string(),
}
};
let mut keybindings = Vec::new();
let global_keybindings = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))
.collect::<Vec<_>>();
if let Some(contextual_help) = app.server_tabs.get_active_route_contextual_help() {
keybindings.extend(
contextual_help
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
}
if let Some(contextual_help) = ServarrContextClueProvider::get_context_clues(app) {
keybindings.extend(
contextual_help
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc)),
);
}
keybindings.extend(global_keybindings);
let mut table = StatefulTable::default();
table.set_items(keybindings);
app.keymapping_table = Some(table);
}
fn handle_clear_errors(app: &mut App<'_>) {
if !app.error.text.is_empty() {
app.error = HorizontallyScrollableText::default();
@@ -115,17 +178,15 @@ fn handle_clear_errors(app: &mut App<'_>) {
fn handle_prompt_toggle(app: &mut App<'_>, key: Key) {
match key {
_ if key == DEFAULT_KEYBINDINGS.left.key || key == DEFAULT_KEYBINDINGS.right.key => {
match app.get_current_route() {
Route::Radarr(_, _) => {
app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm
}
Route::Sonarr(_, _) => {
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
}
_ => (),
_ if matches_key!(left, key) || matches_key!(right, key) => match app.get_current_route() {
Route::Radarr(_, _) => {
app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm
}
}
Route::Sonarr(_, _) => {
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
}
_ => (),
},
_ => (),
}
}
@@ -149,7 +210,7 @@ macro_rules! handle_text_box_left_right_keys {
macro_rules! handle_text_box_keys {
($self:expr, $key:expr, $input:expr) => {
match $self.key {
_ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.backspace.key => {
_ if $crate::matches_key!(backspace, $key) => {
$input.pop();
}
Key::Char(character) => {
@@ -165,7 +226,7 @@ macro_rules! handle_prompt_left_right_keys {
($self:expr, $confirm_prompt:expr, $data:ident) => {
if $self.app.data.$data.selected_block.get_active_block() == $confirm_prompt {
handle_prompt_toggle($self.app, $self.key);
} else if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.left.key {
} else if $crate::matches_key!(left, $self.key) {
$self.app.data.$data.selected_block.left();
} else {
$self.app.data.$data.selected_block.right();
@@ -4,6 +4,7 @@ mod tests {
use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -541,6 +542,25 @@ mod tests {
})
}
#[rstest]
fn test_blocklist_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = BlocklistHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_extract_blocklist_item_id() {
let mut app = App::test_default();
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
@@ -9,6 +7,7 @@ use crate::models::radarr_models::BlocklistItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::stateful_table::SortOption;
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)]
#[path = "blocklist_handler_tests.rs"]
@@ -51,6 +50,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
BLOCKLIST_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -143,10 +146,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ if key == DEFAULT_KEYBINDINGS.clear.key => {
_ if matches_key!(clear, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into());
@@ -154,7 +157,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
_ => (),
},
ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem(
self.extract_blocklist_item_id(),
@@ -164,7 +167,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a,
}
}
ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist);
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
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::radarr_models::CollectionMovie;
@@ -11,6 +9,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
};
use crate::models::stateful_table::StatefulTable;
use crate::models::BlockSelectionState;
use crate::{handle_table_events, matches_key};
#[cfg(test)]
#[path = "collection_details_handler_tests.rs"]
@@ -46,6 +45,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
COLLECTION_DETAILS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -130,7 +133,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan
fn handle_char_key_event(&mut self) {
if self.active_radarr_block == ActiveRadarrBlock::CollectionDetails
&& self.key == DEFAULT_KEYBINDINGS.edit.key
&& matches_key!(edit, self.key)
{
self.app.push_navigation_stack(
(
@@ -1,6 +1,7 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_str_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -278,6 +279,25 @@ mod tests {
});
}
#[rstest]
fn test_collection_details_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = CollectionDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_collection_details_handler_not_ready_when_loading() {
let mut app = App::test_default();
@@ -589,6 +589,25 @@ mod tests {
});
}
#[rstest]
fn test_collections_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = CollectionsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_collections_handler_not_ready_when_loading() {
let mut app = App::test_default();
@@ -644,7 +663,7 @@ mod tests {
Collection {
id: 3,
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()),
quality_profile_id: 1,
search_on_add: false,
@@ -654,7 +673,7 @@ mod tests {
Collection {
id: 2,
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()),
quality_profile_id: 3,
search_on_add: true,
@@ -664,7 +683,7 @@ mod tests {
Collection {
id: 1,
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()),
quality_profile_id: 1,
search_on_add: false,
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -7,7 +6,7 @@ use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_COLLECTION_BLOCKS};
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys};
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)]
#[path = "edit_collection_handler_tests.rs"]
@@ -69,6 +68,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
EDIT_COLLECTION_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -262,7 +265,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
)
.into(),
);
self.app.should_ignore_quit_key = true;
self.app.ignore_special_keys_for_textbox_input = true;
}
ActiveRadarrBlock::EditCollectionToggleMonitored => {
self
@@ -311,7 +314,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
| ActiveRadarrBlock::EditCollectionSelectQualityProfile => self.app.pop_navigation_stack(),
ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
@@ -321,7 +324,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
match self.active_radarr_block {
ActiveRadarrBlock::EditCollectionRootFolderPathInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveRadarrBlock::EditCollectionPrompt => {
self.app.pop_navigation_stack();
@@ -354,7 +357,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle
ActiveRadarrBlock::EditCollectionPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditCollectionConfirmPrompt
&& key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, key)
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(
@@ -2,6 +2,7 @@
mod tests {
use bimap::BiMap;
use pretty_assertions::assert_str_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -457,7 +458,7 @@ mod tests {
#[test]
fn test_edit_collection_root_folder_path_input_submit() {
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 {
path: "Test Path".into(),
..EditCollectionModal::default()
@@ -473,7 +474,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.radarr_data
@@ -759,7 +760,7 @@ mod tests {
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
if selected_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput {
assert!(app.should_ignore_quit_key);
assert!(app.ignore_special_keys_for_textbox_input);
}
}
@@ -791,7 +792,7 @@ mod tests {
);
if active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput {
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
}
}
}
@@ -811,7 +812,7 @@ mod tests {
let mut app = App::test_default();
app.data.radarr_data = create_test_radarr_data();
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::EditCollectionRootFolderPathInput.into());
@@ -823,7 +824,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::EditCollectionPrompt.into()
@@ -926,7 +927,7 @@ mod tests {
app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default());
EditCollectionHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditCollectionRootFolderPathInput,
None,
@@ -942,7 +943,7 @@ mod tests {
.unwrap()
.path
.text,
"h"
"a"
);
}
@@ -1019,6 +1020,25 @@ mod tests {
});
}
#[rstest]
fn test_edit_collection_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditCollectionHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_edit_collection_params() {
let mut app = App::test_default();
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::collections::collection_details_handler::CollectionDetailsHandler;
use crate::handlers::radarr_handlers::collections::edit_collection_handler::EditCollectionHandler;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
@@ -14,6 +12,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
use crate::models::stateful_table::SortOption;
use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
mod collection_details_handler;
mod edit_collection_handler;
@@ -73,6 +72,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
|| COLLECTIONS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -145,7 +148,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Collections => match self.key {
_ if key == DEFAULT_KEYBINDINGS.edit.key => {
_ if matches_key!(edit, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into());
@@ -154,18 +157,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS);
}
_ if key == DEFAULT_KEYBINDINGS.update.key => {
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveRadarrBlock::UpdateAllCollectionsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections);
@@ -1,6 +1,7 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -387,6 +388,25 @@ mod tests {
})
}
#[rstest]
fn test_downloads_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_extract_download_id() {
let mut app = App::test_default();
@@ -1,13 +1,12 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::DownloadRecord;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS};
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)]
#[path = "downloads_handler_tests.rs"]
@@ -47,6 +46,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
DOWNLOADS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -130,18 +133,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Downloads => match self.key {
_ if key == DEFAULT_KEYBINDINGS.update.key => {
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::UpdateDownloadsPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveRadarrBlock::DeleteDownloadPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteDownload(self.extract_download_id()));
@@ -150,7 +153,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a,
}
}
ActiveRadarrBlock::UpdateDownloadsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads);
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -6,7 +5,9 @@ use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::servarr_models::EditIndexerParams;
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys};
use crate::{
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
#[cfg(test)]
#[path = "edit_indexer_handler_tests.rs"]
@@ -65,6 +66,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
EDIT_INDEXER_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -356,7 +361,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
| ActiveRadarrBlock::EditIndexerSeedRatioInput
| ActiveRadarrBlock::EditIndexerTagsInput => {
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
.app
@@ -402,7 +407,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
| ActiveRadarrBlock::EditIndexerSeedRatioInput
| ActiveRadarrBlock::EditIndexerTagsInput => {
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(),
_ => (),
@@ -423,7 +428,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
| ActiveRadarrBlock::EditIndexerPriorityInput
| ActiveRadarrBlock::EditIndexerTagsInput => {
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(),
}
@@ -504,7 +509,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'
ActiveRadarrBlock::EditIndexerPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditIndexerConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, self.key)
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
@@ -10,6 +10,7 @@ mod tests {
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::servarr_models::EditIndexerParams;
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
mod test_handle_scroll_up_and_down {
@@ -1002,7 +1003,7 @@ mod tests {
.handle();
assert_eq!(app.get_current_route(), block.into());
assert!(app.should_ignore_quit_key);
assert!(app.ignore_special_keys_for_textbox_input);
}
#[test]
@@ -1027,7 +1028,7 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::EditIndexerPriorityInput.into()
);
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
}
#[test]
@@ -1193,7 +1194,7 @@ mod tests {
fn test_edit_indexer_name_input_submit() {
let mut app = App::test_default();
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 {
name: "Test".into(),
..EditIndexerModal::default()
@@ -1209,7 +1210,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.radarr_data
@@ -1229,7 +1230,7 @@ mod tests {
fn test_edit_indexer_url_input_submit() {
let mut app = App::test_default();
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 {
url: "Test".into(),
..EditIndexerModal::default()
@@ -1245,7 +1246,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.radarr_data
@@ -1265,7 +1266,7 @@ mod tests {
fn test_edit_indexer_api_key_input_submit() {
let mut app = App::test_default();
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 {
api_key: "Test".into(),
..EditIndexerModal::default()
@@ -1281,7 +1282,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.radarr_data
@@ -1301,7 +1302,7 @@ mod tests {
fn test_edit_indexer_seed_ratio_input_submit() {
let mut app = App::test_default();
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 {
seed_ratio: "Test".into(),
..EditIndexerModal::default()
@@ -1317,7 +1318,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.radarr_data
@@ -1337,7 +1338,7 @@ mod tests {
fn test_edit_indexer_tags_input_submit() {
let mut app = App::test_default();
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 {
tags: "Test".into(),
..EditIndexerModal::default()
@@ -1353,7 +1354,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.radarr_data
@@ -1417,12 +1418,12 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::Indexers.into());
app.push_navigation_stack(active_radarr_block.into());
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();
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!(
app.data.radarr_data.edit_indexer_modal,
Some(EditIndexerModal::default())
@@ -1597,7 +1598,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditIndexerNameInput,
None,
@@ -1613,7 +1614,7 @@ mod tests {
.unwrap()
.name
.text,
"h"
"a"
);
}
@@ -1624,7 +1625,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditIndexerUrlInput,
None,
@@ -1640,7 +1641,7 @@ mod tests {
.unwrap()
.url
.text,
"h"
"a"
);
}
@@ -1651,7 +1652,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditIndexerApiKeyInput,
None,
@@ -1667,7 +1668,7 @@ mod tests {
.unwrap()
.api_key
.text,
"h"
"a"
);
}
@@ -1678,7 +1679,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditIndexerSeedRatioInput,
None,
@@ -1694,7 +1695,7 @@ mod tests {
.unwrap()
.seed_ratio
.text,
"h"
"a"
);
}
@@ -1705,7 +1706,7 @@ mod tests {
app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditIndexerTagsInput,
None,
@@ -1721,7 +1722,7 @@ mod tests {
.unwrap()
.tags
.text,
"h"
"a"
);
}
@@ -1793,6 +1794,25 @@ mod tests {
})
}
#[rstest]
fn test_edit_indexer_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditIndexerHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_edit_indexer_params() {
let mut app = App::test_default();
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -7,7 +6,9 @@ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS,
};
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys};
use crate::{
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
#[cfg(test)]
#[path = "edit_indexer_settings_handler_tests.rs"]
@@ -37,6 +38,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
INDEXER_SETTINGS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -207,7 +212,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
self.app.push_navigation_stack(
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into(),
);
self.app.should_ignore_quit_key = true;
self.app.ignore_special_keys_for_textbox_input = true;
}
ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags => {
let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap();
@@ -224,7 +229,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
}
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput
| ActiveRadarrBlock::IndexerSettingsRetentionInput
@@ -244,7 +249,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
}
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => {
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(),
}
@@ -269,7 +274,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
ActiveRadarrBlock::AllIndexerSettingsPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::IndexerSettingsConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, self.key)
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(
@@ -1,6 +1,7 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -602,7 +603,7 @@ mod tests {
app.get_current_route(),
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into()
);
assert!(app.should_ignore_quit_key);
assert!(app.ignore_special_keys_for_textbox_input);
}
#[test]
@@ -716,7 +717,7 @@ mod tests {
#[test]
fn test_edit_indexer_settings_whitelisted_subtitle_tags_input_submit() {
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 {
whitelisted_hardcoded_subs: "Test tags".into(),
..IndexerSettings::default()
@@ -734,7 +735,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.radarr_data
@@ -814,7 +815,7 @@ mod tests {
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into(),
);
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(
ESC_KEY,
@@ -825,7 +826,7 @@ mod tests {
.handle();
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!(
app.data.radarr_data.indexer_settings,
Some(IndexerSettings::default())
@@ -907,7 +908,7 @@ mod tests {
app.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
IndexerSettingsHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
None,
@@ -923,7 +924,7 @@ mod tests {
.unwrap()
.whitelisted_hardcoded_subs
.text,
"h"
"a"
);
}
@@ -970,6 +971,25 @@ mod tests {
})
}
#[rstest]
fn test_indexer_settings_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_edit_indexer_settings_body() {
let mut app = App::test_default();
@@ -633,6 +633,25 @@ mod tests {
})
}
#[rstest]
fn test_indexers_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_extract_indexer_id() {
let mut app = App::test_default();
+10 -7
View File
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
use crate::handlers::radarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler;
@@ -15,6 +13,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
use crate::models::servarr_models::Indexer;
use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
mod edit_indexer_handler;
mod edit_indexer_settings_handler;
@@ -70,6 +69,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
|| INDEXERS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -169,20 +172,20 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Indexers => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ if key == DEFAULT_KEYBINDINGS.test.key => {
_ if matches_key!(test, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::TestIndexer.into());
}
_ if key == DEFAULT_KEYBINDINGS.test_all.key => {
_ if matches_key!(test_all, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::TestAllIndexers.into());
}
_ if key == DEFAULT_KEYBINDINGS.settings.key => {
_ if matches_key!(settings, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into());
@@ -192,7 +195,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
_ => (),
},
ActiveRadarrBlock::DeleteIndexerPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteIndexer(self.extract_indexer_id()));
@@ -48,6 +48,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl
active_block == ActiveRadarrBlock::TestAllIndexers
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -7,6 +7,7 @@ mod tests {
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::StatefulTable;
use rstest::rstest;
use strum::IntoEnumIterator;
mod test_handle_esc {
@@ -48,6 +49,25 @@ mod tests {
});
}
#[rstest]
fn test_test_all_indexers_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = TestAllIndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_test_all_indexers_handler_is_not_ready_when_loading() {
let mut app = App::test_default();
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::{
@@ -11,7 +10,9 @@ use crate::models::servarr_data::radarr::radarr_data::{
use crate::models::stateful_table::StatefulTable;
use crate::models::{BlockSelectionState, Scrollable};
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, App, Key};
use crate::{
handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, matches_key, App, Key,
};
#[cfg(test)]
#[path = "add_movie_handler_tests.rs"]
@@ -132,6 +133,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
ADD_MOVIE_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -404,7 +409,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
self
.app
.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
&& self.app.data.radarr_data.add_searched_movies.is_some() =>
@@ -468,7 +473,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
)
.into(),
);
self.app.should_ignore_quit_key = true;
self.app.ignore_special_keys_for_textbox_input = true;
}
_ => (),
}
@@ -479,7 +484,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
| ActiveRadarrBlock::AddMovieSelectRootFolder => self.app.pop_navigation_stack(),
ActiveRadarrBlock::AddMovieTagsInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
@@ -490,12 +495,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
ActiveRadarrBlock::AddMovieSearchInput => {
self.app.pop_navigation_stack();
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 => {
self.app.pop_navigation_stack();
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 => {
self.app.pop_navigation_stack();
@@ -509,7 +514,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
| ActiveRadarrBlock::AddMovieSelectRootFolder => self.app.pop_navigation_stack(),
ActiveRadarrBlock::AddMovieTagsInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
@@ -542,7 +547,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a,
ActiveRadarrBlock::AddMoviePrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::AddMovieConfirmPrompt
&& key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, key)
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
@@ -782,7 +782,7 @@ mod tests {
#[test]
fn test_add_movie_search_input_submit() {
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());
AddMovieHandler::new(
@@ -793,7 +793,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::AddMovieSearchResults.into()
@@ -805,7 +805,7 @@ mod tests {
let mut app = App::test_default();
app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default());
app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into());
app.should_ignore_quit_key = true;
app.ignore_special_keys_for_textbox_input = true;
AddMovieHandler::new(
SUBMIT_KEY,
@@ -815,7 +815,7 @@ mod tests {
)
.handle();
assert!(app.should_ignore_quit_key);
assert!(app.ignore_special_keys_for_textbox_input);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::AddMovieSearchInput.into()
@@ -1093,7 +1093,7 @@ mod tests {
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
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 {
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();
app.is_loading = is_ready;
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());
AddMovieHandler::new(
@@ -1160,7 +1160,7 @@ mod tests {
)
.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.data.radarr_data.add_movie_search, None);
}
@@ -1169,7 +1169,7 @@ mod tests {
fn test_add_movie_input_esc() {
let mut app = App::test_default();
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::AddMovieTagsInput.into());
@@ -1181,7 +1181,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::AddMoviePrompt.into()
@@ -1213,7 +1213,7 @@ mod tests {
ActiveRadarrBlock::AddMovieSearchInput.into()
);
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]
@@ -1259,7 +1259,7 @@ mod tests {
fn test_add_movie_tags_input_esc() {
let mut app = App::test_default();
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::AddMovieTagsInput.into());
@@ -1271,7 +1271,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!(
app.get_current_route(),
ActiveRadarrBlock::AddMoviePrompt.into()
@@ -1395,7 +1395,7 @@ mod tests {
app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default());
AddMovieHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::AddMovieSearchInput,
None,
@@ -1404,7 +1404,7 @@ mod tests {
assert_str_eq!(
app.data.radarr_data.add_movie_search.as_ref().unwrap().text,
"h"
"a"
);
}
@@ -1414,7 +1414,7 @@ mod tests {
app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default());
AddMovieHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::AddMovieTagsInput,
None,
@@ -1430,7 +1430,7 @@ mod tests {
.unwrap()
.tags
.text,
"h"
"a"
);
}
@@ -1523,6 +1523,25 @@ mod tests {
});
}
#[rstest]
fn test_add_movie_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = AddMovieHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_add_movie_search_no_panic_on_none_search_result() {
let mut app = App::test_default();
@@ -1,7 +1,7 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::matches_key;
use crate::models::radarr_models::DeleteMovieParams;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS};
use crate::network::radarr_network::RadarrEvent;
@@ -37,6 +37,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
DELETE_MOVIE_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -122,7 +126,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'
if self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt
&& self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::DeleteMovieConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, self.key)
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
@@ -1,6 +1,7 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -313,6 +314,25 @@ mod tests {
});
}
#[rstest]
fn test_delete_movie_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DeleteMovieHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_delete_movie_params() {
let mut app = App::test_default();
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -7,7 +6,7 @@ use crate::models::servarr_data::radarr::modals::EditMovieModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_MOVIE_BLOCKS};
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys};
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key};
#[cfg(test)]
#[path = "edit_movie_handler_tests.rs"]
@@ -68,6 +67,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
EDIT_MOVIE_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -290,7 +293,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
)
.into(),
);
self.app.should_ignore_quit_key = true;
self.app.ignore_special_keys_for_textbox_input = true;
}
ActiveRadarrBlock::EditMovieToggleMonitored => {
self
@@ -319,7 +322,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
| ActiveRadarrBlock::EditMovieSelectQualityProfile => self.app.pop_navigation_stack(),
ActiveRadarrBlock::EditMoviePathInput | ActiveRadarrBlock::EditMovieTagsInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
_ => (),
}
@@ -329,7 +332,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
match self.active_radarr_block {
ActiveRadarrBlock::EditMovieTagsInput | ActiveRadarrBlock::EditMoviePathInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
self.app.ignore_special_keys_for_textbox_input = false;
}
ActiveRadarrBlock::EditMoviePrompt => {
self.app.pop_navigation_stack();
@@ -376,7 +379,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a,
ActiveRadarrBlock::EditMoviePrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== ActiveRadarrBlock::EditMovieConfirmPrompt
&& key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, key)
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
@@ -2,6 +2,7 @@
mod tests {
use bimap::BiMap;
use pretty_assertions::assert_str_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -548,7 +549,7 @@ mod tests {
#[test]
fn test_edit_movie_path_input_submit() {
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 {
path: "Test Path".into(),
..EditMovieModal::default()
@@ -564,7 +565,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.radarr_data
@@ -583,7 +584,7 @@ mod tests {
#[test]
fn test_edit_movie_tags_input_submit() {
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 {
tags: "Test Tags".into(),
..EditMovieModal::default()
@@ -599,7 +600,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.radarr_data
@@ -813,7 +814,7 @@ mod tests {
if selected_block == ActiveRadarrBlock::EditMoviePathInput
|| selected_block == ActiveRadarrBlock::EditMovieTagsInput
{
assert!(app.should_ignore_quit_key);
assert!(app.ignore_special_keys_for_textbox_input);
}
}
@@ -851,7 +852,7 @@ mod tests {
.into()
);
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]
@@ -885,7 +886,7 @@ mod tests {
if active_radarr_block == ActiveRadarrBlock::EditMoviePathInput
|| active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput
{
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
}
}
}
@@ -911,13 +912,13 @@ mod tests {
) {
let mut app = App::test_default();
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(active_radarr_block.into());
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!(
app.get_current_route(),
ActiveRadarrBlock::EditMoviePrompt.into()
@@ -1033,7 +1034,7 @@ mod tests {
app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default());
EditMovieHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditMoviePathInput,
None,
@@ -1049,7 +1050,7 @@ mod tests {
.unwrap()
.path
.text,
"h"
"a"
);
}
@@ -1059,7 +1060,7 @@ mod tests {
app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default());
EditMovieHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::EditMovieTagsInput,
None,
@@ -1075,7 +1076,7 @@ mod tests {
.unwrap()
.tags
.text,
"h"
"a"
);
}
@@ -1148,6 +1149,25 @@ mod tests {
});
}
#[rstest]
fn test_edit_movie_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditMovieHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_edit_movie_params() {
let mut app = App::test_default();
@@ -9,6 +9,7 @@ mod tests {
use crate::app::App;
use crate::event::Key;
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::models::radarr_models::Movie;
use crate::models::servarr_data::radarr::radarr_data::{
@@ -331,7 +332,7 @@ mod tests {
app.get_current_route(),
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());
}
@@ -355,7 +356,7 @@ mod tests {
.handle();
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());
}
@@ -391,6 +392,51 @@ mod tests {
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]
fn test_update_all_movies_key() {
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]
fn test_movies_sorting_options_title() {
let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| {
@@ -615,8 +677,13 @@ mod tests {
#[test]
fn test_movies_sorting_options_studio() {
let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering =
|a, b| a.studio.to_lowercase().cmp(&b.studio.to_lowercase());
let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| {
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();
expected_movies_vec.sort_by(expected_cmp_fn);
@@ -777,6 +844,25 @@ mod tests {
});
}
#[rstest]
fn test_library_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = LibraryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_library_handler_not_ready_when_loading() {
let mut app = App::test_default();
@@ -837,7 +923,7 @@ mod tests {
name: "English".to_owned(),
},
size_on_disk: 1024,
studio: "Studio 1".to_owned(),
studio: Some("Studio 1".to_owned()),
year: 2024,
monitored: false,
runtime: 12.into(),
@@ -854,7 +940,7 @@ mod tests {
name: "Chinese".to_owned(),
},
size_on_disk: 2048,
studio: "Studio 2".to_owned(),
studio: Some("Studio 2".to_owned()),
year: 1998,
monitored: false,
runtime: 60.into(),
@@ -871,7 +957,7 @@ mod tests {
name: "Japanese".to_owned(),
},
size_on_disk: 512,
studio: "studio 3".to_owned(),
studio: Some("studio 3".to_owned()),
year: 1954,
monitored: true,
runtime: 120.into(),
+30 -9
View File
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
@@ -8,7 +7,6 @@ use crate::handlers::radarr_handlers::library::edit_movie_handler::EditMovieHand
use crate::handlers::radarr_handlers::library::movie_details_handler::MovieDetailsHandler;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::handle_table_events;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::models::radarr_models::Movie;
use crate::models::servarr_data::radarr::radarr_data::{
@@ -17,6 +15,7 @@ use crate::models::servarr_data::radarr::radarr_data::{
use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, HorizontallyScrollableText};
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
mod add_movie_handler;
mod delete_movie_handler;
@@ -36,6 +35,9 @@ pub(super) struct LibraryHandler<'a, 'b> {
impl LibraryHandler<'_, '_> {
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> {
@@ -81,6 +83,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
|| LIBRARY_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -161,7 +167,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::Movies => match self.key {
_ if key == DEFAULT_KEYBINDINGS.edit.key => {
_ if matches_key!(edit, key) => {
self.app.push_navigation_stack(
(
ActiveRadarrBlock::EditMoviePrompt,
@@ -173,25 +179,34 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS);
}
_ if key == DEFAULT_KEYBINDINGS.add.key => {
_ if matches_key!(add, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into());
self.app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default());
self.app.should_ignore_quit_key = true;
self.app.ignore_special_keys_for_textbox_input = true;
}
_ if key == DEFAULT_KEYBINDINGS.update.key => {
_ if matches_key!(toggle_monitoring, key) => {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::ToggleMovieMonitoring(self.extract_movie_id()));
self
.app
.pop_and_push_navigation_stack(self.active_radarr_block.into());
}
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveRadarrBlock::UpdateAllMoviesPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies);
@@ -220,7 +235,13 @@ fn movies_sorting_options() -> Vec<SortOption<Movie>> {
},
SortOption {
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 {
name: "Runtime",
@@ -1,9 +1,7 @@
use serde_json::Number;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::{
@@ -16,6 +14,7 @@ use crate::models::servarr_models::Language;
use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, Scrollable};
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)]
#[path = "movie_details_handler_tests.rs"]
@@ -136,6 +135,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
MOVIE_DETAILS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -245,13 +248,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
| ActiveRadarrBlock::Cast
| ActiveRadarrBlock::Crew
| ActiveRadarrBlock::ManualSearch => match self.key {
_ if self.key == DEFAULT_KEYBINDINGS.left.key => {
_ if matches_key!(left, self.key) => {
self.app.data.radarr_data.movie_info_tabs.previous();
self.app.pop_and_push_navigation_stack(
self.app.data.radarr_data.movie_info_tabs.get_active_route(),
);
}
_ if self.key == DEFAULT_KEYBINDINGS.right.key => {
_ if matches_key!(right, self.key) => {
self.app.data.radarr_data.movie_info_tabs.next();
self.app.pop_and_push_navigation_stack(
self.app.data.radarr_data.movie_info_tabs.get_active_route(),
@@ -332,12 +335,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
| ActiveRadarrBlock::Cast
| ActiveRadarrBlock::Crew
| ActiveRadarrBlock::ManualSearch => match self.key {
_ if key == DEFAULT_KEYBINDINGS.auto_search.key => {
_ if matches_key!(auto_search, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::AutomaticallySearchMoviePrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.edit.key => {
_ if matches_key!(edit, key) => {
self.app.push_navigation_stack(
(
ActiveRadarrBlock::EditMoviePrompt,
@@ -349,35 +352,33 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS);
}
_ if key == DEFAULT_KEYBINDINGS.update.key => {
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::UpdateAndScanPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self
.app
.pop_and_push_navigation_stack(self.active_radarr_block.into());
}
_ => (),
},
ActiveRadarrBlock::AutomaticallySearchMoviePrompt
if key == DEFAULT_KEYBINDINGS.confirm.key =>
{
ActiveRadarrBlock::AutomaticallySearchMoviePrompt if matches_key!(confirm, key) => {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::TriggerAutomaticSearch(self.extract_movie_id()));
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::UpdateAndScanPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => {
ActiveRadarrBlock::UpdateAndScanPrompt if matches_key!(confirm, key) => {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::UpdateAndScan(self.extract_movie_id()));
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::ManualSearchConfirmPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => {
ActiveRadarrBlock::ManualSearchConfirmPrompt if matches_key!(confirm, key) => {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease(
self.build_radarr_release_download_body(),
@@ -1042,6 +1042,25 @@ mod tests {
});
}
#[rstest]
fn test_movie_details_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = MovieDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[rstest]
fn test_movie_details_handler_is_not_ready_when_loading(
#[values(
+9 -6
View File
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::radarr_handlers::blocklist::BlocklistHandler;
use crate::handlers::radarr_handlers::collections::CollectionsHandler;
use crate::handlers::radarr_handlers::downloads::DownloadsHandler;
@@ -8,7 +7,7 @@ use crate::handlers::radarr_handlers::root_folders::RootFoldersHandler;
use crate::handlers::radarr_handlers::system::SystemHandler;
use crate::handlers::KeyEventHandler;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::{App, Key};
use crate::{matches_key, App, Key};
mod blocklist;
mod collections;
@@ -65,6 +64,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
true
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -109,11 +112,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
pub fn handle_change_tab_left_right_keys(app: &mut App<'_>, key: Key) {
let key_ref = key;
match key_ref {
_ if key == DEFAULT_KEYBINDINGS.left.key => {
_ if matches_key!(left, key, app.ignore_special_keys_for_textbox_input) => {
app.data.radarr_data.main_tabs.previous();
app.pop_and_push_navigation_stack(app.data.radarr_data.main_tabs.get_active_route());
}
_ if key == DEFAULT_KEYBINDINGS.right.key => {
_ if matches_key!(right, key, app.ignore_special_keys_for_textbox_input) => {
app.data.radarr_data.main_tabs.next();
app.pop_and_push_navigation_stack(app.data.radarr_data.main_tabs.get_active_route());
}
@@ -139,7 +142,7 @@ macro_rules! search_table {
};
$app.data.radarr_data.is_searching = false;
$app.should_ignore_quit_key = false;
$app.ignore_special_keys_for_textbox_input = false;
if search_index.is_some() {
$app.pop_navigation_stack();
@@ -166,7 +169,7 @@ macro_rules! search_table {
};
$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() {
$app.pop_navigation_stack();
@@ -275,7 +275,7 @@ pub(in crate::handlers::radarr_handlers) mod utils {
audio_stream_count: 1,
video_bit_depth: 10,
video_bitrate: 0,
video_codec: "x265".to_owned(),
video_codec: Some("x265".to_owned()),
video_fps: Number::from_f64(23.976).unwrap(),
resolution: "1920x804".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(),
overview: "Blah blah blah".to_owned(),
path: "/nfs/movies".to_owned(),
studio: "21st Century Alex".to_owned(),
studio: Some("21st Century Alex".to_owned()),
genres: genres(),
year: 2023,
monitored: true,
@@ -46,6 +46,76 @@ mod tests {
assert_eq!(app.get_current_route(), right_block.into());
}
#[rstest]
#[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)]
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)]
#[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)]
#[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)]
#[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)]
#[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)]
#[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)]
fn test_radarr_handler_change_tab_left_right_keys_alt_navigation(
#[case] index: usize,
#[case] left_block: ActiveRadarrBlock,
#[case] right_block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = false;
app.data.radarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.alt.unwrap());
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
left_block.into()
);
assert_eq!(app.get_current_route(), left_block.into());
app.data.radarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.alt.unwrap());
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
right_block.into()
);
assert_eq!(app.get_current_route(), right_block.into());
}
#[rstest]
#[case(0, ActiveRadarrBlock::Movies)]
#[case(1, ActiveRadarrBlock::Collections)]
#[case(2, ActiveRadarrBlock::Downloads)]
#[case(3, ActiveRadarrBlock::Blocklist)]
#[case(4, ActiveRadarrBlock::RootFolders)]
#[case(5, ActiveRadarrBlock::Indexers)]
#[case(6, ActiveRadarrBlock::System)]
fn test_radarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key(
#[case] index: usize,
#[case] block: ActiveRadarrBlock,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = true;
app.push_navigation_stack(block.into());
app.data.radarr_data.main_tabs.set_index(index);
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.alt.unwrap());
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
block.into()
);
assert_eq!(app.get_current_route(), block.into());
handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.alt.unwrap());
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
block.into()
);
assert_eq!(app.get_current_route(), block.into());
}
#[rstest]
fn test_delegates_system_blocks_to_system_handler(
#[values(
@@ -217,6 +287,25 @@ mod tests {
})
}
#[rstest]
fn test_radarr_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = RadarrHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::Movies,
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_radarr_handler_is_ready() {
let mut app = App::test_default();
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
@@ -8,7 +7,9 @@ use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_F
use crate::models::servarr_models::{AddRootFolderBody, RootFolder};
use crate::models::HorizontallyScrollableText;
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys};
use crate::{
handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
#[cfg(test)]
#[path = "root_folders_handler_tests.rs"]
@@ -68,6 +69,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
ROOT_FOLDERS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -168,7 +173,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
self.build_add_root_folder_body(),
));
self.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();
}
_ => (),
@@ -181,7 +186,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
self.app.pop_navigation_stack();
self.app.data.radarr_data.edit_root_folder = None;
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 => {
self.app.pop_navigation_stack();
@@ -195,15 +200,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
let key = self.key;
match self.active_radarr_block {
ActiveRadarrBlock::RootFolders => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ if key == DEFAULT_KEYBINDINGS.add.key => {
_ if matches_key!(add, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into());
self.app.data.radarr_data.edit_root_folder = Some(HorizontallyScrollableText::default());
self.app.should_ignore_quit_key = true;
self.app.ignore_special_keys_for_textbox_input = true;
}
_ => (),
},
@@ -215,7 +220,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'
)
}
ActiveRadarrBlock::DeleteRootFolderPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DeleteRootFolder(self.extract_root_folder_id()));
@@ -1,6 +1,7 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -262,7 +263,7 @@ mod tests {
.set_items(vec![RootFolder::default()]);
app.data.radarr_data.edit_root_folder = Some("Test".into());
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::AddRootFolderPrompt.into());
@@ -275,7 +276,7 @@ mod tests {
.handle();
assert!(app.data.radarr_data.prompt_confirm);
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::AddRootFolder(expected_add_root_folder_body))
@@ -291,7 +292,7 @@ mod tests {
let mut app = App::test_default();
app.data.radarr_data.edit_root_folder = Some(HorizontallyScrollableText::default());
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::AddRootFolderPrompt.into());
@@ -304,7 +305,7 @@ mod tests {
.handle();
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_eq!(
app.get_current_route(),
@@ -406,7 +407,7 @@ mod tests {
app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into());
app.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.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(
ESC_KEY,
@@ -423,7 +424,7 @@ mod tests {
assert!(app.data.radarr_data.edit_root_folder.is_none());
assert!(!app.data.radarr_data.prompt_confirm);
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
}
#[rstest]
@@ -472,7 +473,7 @@ mod tests {
app.get_current_route(),
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());
}
@@ -499,7 +500,7 @@ mod tests {
app.get_current_route(),
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());
}
@@ -589,7 +590,7 @@ mod tests {
app.data.radarr_data.edit_root_folder = Some(HorizontallyScrollableText::default());
RootFoldersHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveRadarrBlock::AddRootFolderPrompt,
None,
@@ -598,7 +599,7 @@ mod tests {
assert_str_eq!(
app.data.radarr_data.edit_root_folder.as_ref().unwrap().text,
"h"
"a"
);
}
@@ -644,6 +645,25 @@ mod tests {
})
}
#[rstest]
fn test_root_folders_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = RootFoldersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_add_root_folder_body() {
let mut app = App::test_default();
+10 -6
View File
@@ -1,9 +1,9 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::radarr_handlers::system::system_details_handler::SystemDetailsHandler;
use crate::handlers::{handle_clear_errors, KeyEventHandler};
use crate::matches_key;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::Scrollable;
@@ -35,6 +35,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b
SystemDetailsHandler::accepts(active_block) || active_block == ActiveRadarrBlock::System
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -86,15 +90,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b
if self.active_radarr_block == ActiveRadarrBlock::System {
let key = self.key;
match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ if key == DEFAULT_KEYBINDINGS.events.key => {
_ if matches_key!(events, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::SystemQueuedEvents.into());
}
_ if key == DEFAULT_KEYBINDINGS.logs.key => {
_ if matches_key!(logs, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::SystemLogs.into());
@@ -106,12 +110,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b
.set_items(self.app.data.radarr_data.logs.items.to_vec());
self.app.data.radarr_data.log_details.scroll_to_bottom();
}
_ if key == DEFAULT_KEYBINDINGS.tasks.key => {
_ if matches_key!(tasks, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into());
}
_ if key == DEFAULT_KEYBINDINGS.update.key => {
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::SystemUpdates.into());
@@ -1,7 +1,7 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::matches_key;
use crate::models::radarr_models::RadarrTaskName;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS};
use crate::models::stateful_list::StatefulList;
@@ -36,6 +36,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
SYSTEM_DETAILS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -114,7 +118,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
match self.active_radarr_block {
ActiveRadarrBlock::SystemLogs => match self.key {
_ if key == DEFAULT_KEYBINDINGS.left.key => {
_ if matches_key!(left, key) => {
self
.app
.data
@@ -124,7 +128,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
.iter()
.for_each(|log| log.scroll_right());
}
_ if key == DEFAULT_KEYBINDINGS.right.key => {
_ if matches_key!(right, key) => {
self
.app
.data
@@ -178,14 +182,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler
}
fn handle_char_key_event(&mut self) {
if SYSTEM_DETAILS_BLOCKS.contains(&self.active_radarr_block)
&& self.key == DEFAULT_KEYBINDINGS.refresh.key
if SYSTEM_DETAILS_BLOCKS.contains(&self.active_radarr_block) && matches_key!(refresh, self.key)
{
self.app.should_refresh = true;
}
if self.active_radarr_block == ActiveRadarrBlock::SystemTaskStartConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, self.key)
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
@@ -1,6 +1,7 @@
#[cfg(test)]
mod tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -938,6 +939,25 @@ mod tests {
})
}
#[rstest]
fn test_system_details_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = SystemDetailsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_extract_task_name() {
let mut app = App::test_default();
@@ -450,6 +450,25 @@ mod tests {
})
}
#[rstest]
fn test_system_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = SystemHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveRadarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_system_handler_is_not_ready_when_loading() {
let mut app = App::test_default();
@@ -4,6 +4,7 @@ mod tests {
use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -513,6 +514,25 @@ mod tests {
})
}
#[rstest]
fn test_blocklist_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = BlocklistHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_extract_blocklist_item_id() {
let mut app = App::test_default();
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
@@ -9,6 +7,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKL
use crate::models::sonarr_models::BlocklistItem;
use crate::models::stateful_table::SortOption;
use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)]
#[path = "blocklist_handler_tests.rs"]
@@ -51,6 +50,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
BLOCKLIST_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -143,10 +146,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
let key = self.key;
match self.active_sonarr_block {
ActiveSonarrBlock::Blocklist => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ if key == DEFAULT_KEYBINDINGS.clear.key => {
_ if matches_key!(clear, key) => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::BlocklistClearAllItemsPrompt.into());
@@ -154,7 +157,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
_ => (),
},
ActiveSonarrBlock::DeleteBlocklistItemPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteBlocklistItem(
self.extract_blocklist_item_id(),
@@ -164,7 +167,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a,
}
}
ActiveSonarrBlock::BlocklistClearAllItemsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::ClearBlocklist);
@@ -1,6 +1,7 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -389,6 +390,25 @@ mod tests {
})
}
#[rstest]
fn test_downloads_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = DownloadsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_extract_download_id() {
let mut app = App::test_default();
@@ -1,13 +1,12 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS};
use crate::models::sonarr_models::DownloadRecord;
use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_table_events, matches_key};
#[cfg(test)]
#[path = "downloads_handler_tests.rs"]
@@ -47,6 +46,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
DOWNLOADS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -130,18 +133,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
let key = self.key;
match self.active_sonarr_block {
ActiveSonarrBlock::Downloads => match self.key {
_ if key == DEFAULT_KEYBINDINGS.update.key => {
_ if matches_key!(update, key) => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::UpdateDownloadsPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
},
ActiveSonarrBlock::DeleteDownloadPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::DeleteDownload(self.extract_download_id()));
@@ -150,7 +153,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a,
}
}
ActiveSonarrBlock::UpdateDownloadsPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::UpdateDownloads);
@@ -4,6 +4,7 @@ mod tests {
use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -306,6 +307,25 @@ mod tests {
})
}
#[rstest]
fn test_history_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = HistoryHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_history_handler_not_ready_when_loading() {
let mut app = App::test_default();
+6 -3
View File
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_clear_errors, KeyEventHandler};
@@ -9,6 +7,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTOR
use crate::models::servarr_models::Language;
use crate::models::sonarr_models::SonarrHistoryItem;
use crate::models::stateful_table::SortOption;
use crate::{handle_table_events, matches_key};
#[cfg(test)]
#[path = "history_handler_tests.rs"]
@@ -52,6 +51,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, '
HISTORY_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -110,7 +113,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, '
let key = self.key;
if self.active_sonarr_block == ActiveSonarrBlock::History {
match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ => (),
@@ -1,4 +1,3 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
@@ -6,7 +5,9 @@ use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::servarr_models::EditIndexerParams;
use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys};
use crate::{
handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys, matches_key,
};
#[cfg(test)]
#[path = "edit_indexer_handler_tests.rs"]
@@ -64,6 +65,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
EDIT_INDEXER_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -355,7 +360,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
| ActiveSonarrBlock::EditIndexerSeedRatioInput
| ActiveSonarrBlock::EditIndexerTagsInput => {
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
.app
@@ -401,7 +406,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
| ActiveSonarrBlock::EditIndexerSeedRatioInput
| ActiveSonarrBlock::EditIndexerTagsInput => {
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(),
_ => (),
@@ -422,7 +427,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
| ActiveSonarrBlock::EditIndexerPriorityInput
| ActiveSonarrBlock::EditIndexerTagsInput => {
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(),
}
@@ -503,7 +508,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'
ActiveSonarrBlock::EditIndexerPrompt => {
if self.app.data.sonarr_data.selected_block.get_active_block()
== ActiveSonarrBlock::EditIndexerConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, self.key)
{
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action =
@@ -10,6 +10,7 @@ mod tests {
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::servarr_models::EditIndexerParams;
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
mod test_handle_scroll_up_and_down {
@@ -1002,7 +1003,7 @@ mod tests {
.handle();
assert_eq!(app.get_current_route(), block.into());
assert!(app.should_ignore_quit_key);
assert!(app.ignore_special_keys_for_textbox_input);
}
#[test]
@@ -1027,7 +1028,7 @@ mod tests {
app.get_current_route(),
ActiveSonarrBlock::EditIndexerPriorityInput.into()
);
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
}
#[test]
@@ -1193,7 +1194,7 @@ mod tests {
fn test_edit_indexer_name_input_submit() {
let mut app = App::test_default();
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 {
name: "Test".into(),
..EditIndexerModal::default()
@@ -1209,7 +1210,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.sonarr_data
@@ -1229,7 +1230,7 @@ mod tests {
fn test_edit_indexer_url_input_submit() {
let mut app = App::test_default();
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 {
url: "Test".into(),
..EditIndexerModal::default()
@@ -1245,7 +1246,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.sonarr_data
@@ -1265,7 +1266,7 @@ mod tests {
fn test_edit_indexer_api_key_input_submit() {
let mut app = App::test_default();
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 {
api_key: "Test".into(),
..EditIndexerModal::default()
@@ -1281,7 +1282,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.sonarr_data
@@ -1301,7 +1302,7 @@ mod tests {
fn test_edit_indexer_seed_ratio_input_submit() {
let mut app = App::test_default();
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 {
seed_ratio: "Test".into(),
..EditIndexerModal::default()
@@ -1317,7 +1318,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.sonarr_data
@@ -1337,7 +1338,7 @@ mod tests {
fn test_edit_indexer_tags_input_submit() {
let mut app = App::test_default();
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 {
tags: "Test".into(),
..EditIndexerModal::default()
@@ -1353,7 +1354,7 @@ mod tests {
)
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.ignore_special_keys_for_textbox_input);
assert!(!app
.data
.sonarr_data
@@ -1417,12 +1418,12 @@ mod tests {
app.push_navigation_stack(ActiveSonarrBlock::Indexers.into());
app.push_navigation_stack(active_sonarr_block.into());
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();
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!(
app.data.sonarr_data.edit_indexer_modal,
Some(EditIndexerModal::default())
@@ -1597,7 +1598,7 @@ mod tests {
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveSonarrBlock::EditIndexerNameInput,
None,
@@ -1613,7 +1614,7 @@ mod tests {
.unwrap()
.name
.text,
"h"
"a"
);
}
@@ -1624,7 +1625,7 @@ mod tests {
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveSonarrBlock::EditIndexerUrlInput,
None,
@@ -1640,7 +1641,7 @@ mod tests {
.unwrap()
.url
.text,
"h"
"a"
);
}
@@ -1651,7 +1652,7 @@ mod tests {
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveSonarrBlock::EditIndexerApiKeyInput,
None,
@@ -1667,7 +1668,7 @@ mod tests {
.unwrap()
.api_key
.text,
"h"
"a"
);
}
@@ -1678,7 +1679,7 @@ mod tests {
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveSonarrBlock::EditIndexerSeedRatioInput,
None,
@@ -1694,7 +1695,7 @@ mod tests {
.unwrap()
.seed_ratio
.text,
"h"
"a"
);
}
@@ -1705,7 +1706,7 @@ mod tests {
app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default());
EditIndexerHandler::new(
Key::Char('h'),
Key::Char('a'),
&mut app,
ActiveSonarrBlock::EditIndexerTagsInput,
None,
@@ -1721,7 +1722,7 @@ mod tests {
.unwrap()
.tags
.text,
"h"
"a"
);
}
@@ -1793,6 +1794,25 @@ mod tests {
})
}
#[rstest]
fn test_edit_indexer_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = EditIndexerHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_edit_indexer_params() {
let mut app = App::test_default();
@@ -1,13 +1,12 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_prompt_left_right_keys;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS,
};
use crate::models::sonarr_models::IndexerSettings;
use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_prompt_left_right_keys, matches_key};
#[cfg(test)]
#[path = "edit_indexer_settings_handler_tests.rs"]
@@ -37,6 +36,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
INDEXER_SETTINGS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -183,7 +186,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandl
if self.active_sonarr_block == ActiveSonarrBlock::AllIndexerSettingsPrompt
&& self.app.data.sonarr_data.selected_block.get_active_block()
== ActiveSonarrBlock::IndexerSettingsConfirmPrompt
&& self.key == DEFAULT_KEYBINDINGS.confirm.key
&& matches_key!(confirm, self.key)
{
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditAllIndexerSettings(
@@ -1,6 +1,7 @@
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
@@ -521,6 +522,25 @@ mod tests {
})
}
#[rstest]
fn test_indexer_settings_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexerSettingsHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_build_edit_indexer_settings_params() {
let mut app = App::test_default();
@@ -643,6 +643,25 @@ mod tests {
})
}
#[rstest]
fn test_indexers_handler_ignore_special_keys(
#[values(true, false)] ignore_special_keys_for_textbox_input: bool,
) {
let mut app = App::test_default();
app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input;
let handler = IndexersHandler::new(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::default(),
None,
);
assert_eq!(
handler.ignore_special_keys(),
ignore_special_keys_for_textbox_input
);
}
#[test]
fn test_extract_indexer_id() {
let mut app = App::test_default();
+10 -7
View File
@@ -1,7 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::sonarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
use crate::handlers::sonarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler;
@@ -15,6 +13,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{
use crate::models::servarr_models::Indexer;
use crate::models::BlockSelectionState;
use crate::network::sonarr_network::SonarrEvent;
use crate::{handle_table_events, matches_key};
mod edit_indexer_handler;
mod edit_indexer_settings_handler;
@@ -70,6 +69,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
|| INDEXERS_BLOCKS.contains(&active_block)
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,
@@ -168,20 +171,20 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
let key = self.key;
match self.active_sonarr_block {
ActiveSonarrBlock::Indexers => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => {
_ if matches_key!(refresh, key) => {
self.app.should_refresh = true;
}
_ if key == DEFAULT_KEYBINDINGS.test.key => {
_ if matches_key!(test, key) => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::TestIndexer.into());
}
_ if key == DEFAULT_KEYBINDINGS.test_all.key => {
_ if matches_key!(test_all, key) => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::TestAllIndexers.into());
}
_ if key == DEFAULT_KEYBINDINGS.settings.key => {
_ if matches_key!(settings, key) => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into());
@@ -191,7 +194,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a,
_ => (),
},
ActiveSonarrBlock::DeleteIndexerPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
if matches_key!(confirm, key) {
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::DeleteIndexer(self.extract_indexer_id()));
@@ -48,6 +48,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for TestAllIndexersHandl
active_block == ActiveSonarrBlock::TestAllIndexers
}
fn ignore_special_keys(&self) -> bool {
self.app.ignore_special_keys_for_textbox_input
}
fn new(
key: Key,
app: &'a mut App<'b>,

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