diff --git a/.github/workflows/build_and_release.yml b/.github/workflows/build_and_release.yml new file mode 100644 index 0000000..1ce0b61 --- /dev/null +++ b/.github/workflows/build_and_release.yml @@ -0,0 +1,88 @@ +name: Upload release asset + +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +jobs: + release: + name: Create release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + body: Please see the changelog for details about this new release. + draft: false + prerelease: false + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + + build-linux-x86_64: + name: Build Linux x86_64 release assets + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - run: sudo apt-get update + - run: sudo apt-get install libncurses5-dev libncursesw5-dev + - name: Build project + run: | + cargo build --release --locked + mkdir shellcaster + cp target/release/shellcaster shellcaster/ + cp config.toml README.md LICENSE shellcaster/ + tar czvf shellcaster-linux-x86_64-bundled.tar.gz shellcaster/{shellcaster,config.toml,README.md,LICENSE} + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.release.outputs.upload_url }} + asset_path: ./shellcaster-linux-x86_64-bundled.tar.gz + asset_name: shellcaster-linux-x86_64-bundled.tar.gz + asset_content_type: application/gzip + + build-mac-x86_64: + name: Build MacOS x86_64 release assets + needs: release + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: Build project + run: | + cargo build --release --locked + mkdir shellcaster + cp target/release/shellcaster shellcaster/ + cp config.toml README.md LICENSE shellcaster/ + tar czvf shellcaster-macos-x86_64-bundled.tar.gz shellcaster/{shellcaster,config.toml,README.md,LICENSE} + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.release.outputs.upload_url }} + asset_path: ./shellcaster-macos-x86_64-bundled.tar.gz + asset_name: shellcaster-macos-x86_64-bundled.tar.gz + asset_content_type: application/gzip + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..64407b3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: Compile and test + +on: + push: + branches: + - master + +jobs: + test: + name: Check and run tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - run: sudo apt-get update + # need to install ncurses library headers + - run: sudo apt-get install libncurses5-dev libncursesw5-dev + - run: cargo check --locked + - run: cargo test --locked \ No newline at end of file diff --git a/.gitignore b/.gitignore index 19bde5d..ebb995c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target **/*.rs.bk *.db -/.meta \ No newline at end of file +/.meta +/build \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a18d671..b28ee05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v0.8.2 (2020-07-24) +- Adds details panel on the right-hand side when the screen is large enough, providing more information about the selected episode +- Better notifications for syncing and downloading files +- New config option: Adjust the maximum number of retries to connect when syncing podcasts or downloading episodes +- Changed from `reqwest` package to `ureq` package, which simplifies some things, and also cuts out numerous other dependencies (meaning a smaller binary size!) +- Syncing podcasts now uses the same threadpool as downloading, leading to some efficiencies and somewhat simpler code +- Bug fixes: + - Creates directory for database if it does not exist + - Mark episode as played when user plays the episode + ## v0.8.1 (2020-07-01) - Can now remove one or more episodes from the list of episodes, effectively hiding them so they will not be re-synced - Can also remove podcasts entirely diff --git a/Cargo.lock b/Cargo.lock index 7441e27..79fe0e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,18 +1,5 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "adler32" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "aho-corasick" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "aho-corasick" version = "0.7.10" @@ -31,47 +18,19 @@ name = "arrayvec" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "autocfg" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "autocfg" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "backtrace" -version = "0.3.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "backtrace-sys 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "backtrace-sys" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "base64" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] name = "base64" -version = "0.11.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -91,32 +50,12 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "byteorder" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "bytes" -version = "0.5.4" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "cc" -version = "1.0.50" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -135,51 +74,22 @@ dependencies = [ ] [[package]] -name = "cloudabi" -version = "0.0.3" +name = "chunked_transfer" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "cookie" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "cookie_store" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cookie 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "publicsuffix 1.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "try_from 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "core-foundation" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -187,47 +97,6 @@ name = "core-foundation-sys" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "crc32fast" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-deque" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-epoch 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memoffset 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-queue" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "crossbeam-utils" version = "0.7.2" @@ -317,7 +186,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", "redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -327,21 +196,11 @@ name = "dirs-sys-next" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", "redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "dtoa" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "either" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "encoding_rs" version = "0.8.22" @@ -350,34 +209,6 @@ dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "error-chain" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "failure" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", - "failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "failure_derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", - "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -388,17 +219,6 @@ name = "fallible-streaming-iterator" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "flate2" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "fnv" version = "1.0.6" @@ -418,627 +238,202 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" +name = "getrandom" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "futures" -version = "0.1.29" +name = "idna" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] -name = "futures-channel" -version = "0.3.4" +name = "js-sys" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "futures-core" -version = "0.3.4" +name = "lazy_static" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "futures-cpupool" -version = "0.1.8" +name = "libc" +version = "0.2.71" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] -name = "futures-io" -version = "0.3.4" +name = "libsqlite3-sys" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] -name = "futures-sink" -version = "0.3.4" +name = "linked-hash-map" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "futures-task" -version = "0.3.4" +name = "log" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] -name = "futures-util" -version = "0.3.4" +name = "lru-cache" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "getrandom" -version = "0.1.14" +name = "matches" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] -name = "h2" -version = "0.1.26" +name = "memchr" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] -name = "h2" +name = "native-tls" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.10.30 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.58 (registry+https://github.com/rust-lang/crates.io-index)", + "schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "hermit-abi" -version = "0.1.10" +name = "ncurses" +version = "5.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "http" -version = "0.1.21" +name = "num-integer" +version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "http" -version = "0.2.1" +name = "num-traits" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "http-body" -version = "0.1.0" +name = "once_cell" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] -name = "http-body" -version = "0.3.1" +name = "openssl" +version = "0.10.30" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.58 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "httparse" -version = "1.3.4" +name = "openssl-probe" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "hyper" -version = "0.12.35" +name = "openssl-sys" +version = "0.9.58" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "h2 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", - "http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-reactor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-tcp 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-threadpool 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-timer 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", - "want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "hyper" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "h2 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", - "tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "hyper-tls" -version = "0.3.2" +name = "pancurses" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ncurses 5.99.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pdcurses-sys 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winreg 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "hyper-tls" -version = "0.4.1" +name = "pdcurses-sys" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "ident_case" -version = "1.0.1" +name = "percent-encoding" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "idna" -version = "0.1.5" +name = "pkg-config" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] -name = "idna" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "indexmap" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "itoa" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "js-sys" -version = "0.3.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "lazy_static" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "libc" -version = "0.2.68" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "libsqlite3-sys" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "lock_api" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "log" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "lru-cache" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "matches" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "memchr" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "memoffset" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "mime_guess" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "miniz_oxide" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mio" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "miow" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "native-tls" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl 0.10.28 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.54 (registry+https://github.com/rust-lang/crates.io-index)", - "schannel 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework-sys 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "ncurses" -version = "5.99.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "net2" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "num-integer" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "num-traits" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "num_cpus" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "hermit-abi 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "openssl" -version = "0.10.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.54 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "openssl-probe" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "openssl-sys" -version = "0.9.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "pancurses" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "ncurses 5.99.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pdcurses-sys 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winreg 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "parking_lot" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "lock_api 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "parking_lot_core" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "pdcurses-sys" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "percent-encoding" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "pin-project" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "pin-project-internal 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "pin-project-internal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "pin-project-lite" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "pin-utils" -version = "0.1.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "pkg-config" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "ppv-lite86" -version = "0.2.6" +name = "ppv-lite86" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1050,15 +445,11 @@ dependencies = [ ] [[package]] -name = "publicsuffix" -version = "1.5.4" +name = "qstring" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "error-chain 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", - "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1078,67 +469,27 @@ dependencies = [ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "rand_chacha" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "rand_core" version = "0.5.1" @@ -1147,14 +498,6 @@ dependencies = [ "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "rand_hc" version = "0.2.0" @@ -1163,62 +506,6 @@ dependencies = [ "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "redox_syscall" version = "0.1.56" @@ -1234,18 +521,6 @@ dependencies = [ "rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "regex" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "utf8-ranges 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "regex" version = "1.3.6" @@ -1257,14 +532,6 @@ dependencies = [ "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "regex-syntax" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "ucd-util 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "regex-syntax" version = "0.6.17" @@ -1272,87 +539,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "remove_dir_all" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "reqwest" -version = "0.9.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "cookie 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cookie_store 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)", - "flate2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", - "mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-threadpool 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-timer 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", - "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "reqwest" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper-tls 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", - "mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-futures 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", - "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +name = "rfc822_sanitizer" +version = "0.3.3" +source = "git+https://github.com/jeff-hughes/rfc822_sanitizer_copy#e262dac63a420acea7acdf5d5419e99dcca438a4" +dependencies = [ + "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "rfc822_sanitizer" -version = "0.3.3" +name = "ring" +version = "0.16.15" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1362,7 +576,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1391,23 +604,17 @@ dependencies = [ ] [[package]] -name = "rustc-demangle" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "rustc_version" -version = "0.2.3" +name = "rustls" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)", + "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.21.3 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "ryu" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "sanitize-filename" version = "0.2.1" @@ -1419,7 +626,7 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1427,44 +634,35 @@ dependencies = [ ] [[package]] -name = "scopeguard" -version = "1.1.0" +name = "sct" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "security-framework" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework-sys 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "security-framework-sys" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "serde" version = "1.0.106" @@ -1483,55 +681,24 @@ dependencies = [ "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "serde_json" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "serde_urlencoded" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "serde_urlencoded" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", - "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "shellcaster" -version = "0.8.1" +version = "0.8.2" dependencies = [ "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "dirs-next 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "pancurses 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rfc822_sanitizer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rfc822_sanitizer 0.3.3 (git+https://github.com/jeff-hughes/rfc822_sanitizer_copy)", "rss 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusqlite 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", "sanitize-filename 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", "shellexpand 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ureq 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1542,31 +709,15 @@ dependencies = [ "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "slab" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "smallvec" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "smallvec" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "string" -version = "0.2.1" +name = "spin" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] name = "strsim" @@ -1583,36 +734,25 @@ dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "synstructure" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "tempfile" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "thread_local" -version = "0.3.6" +name = "textwrap" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1628,173 +768,11 @@ name = "time" version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "tokio" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-current-thread 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-reactor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-tcp 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-threadpool 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-timer 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-buf" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-current-thread" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-executor" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-io" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-reactor" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-sync 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-sync" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-tcp" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-reactor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-threadpool" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-deque 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-timer" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-tls" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-util" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "toml" version = "0.5.6" @@ -1803,37 +781,6 @@ dependencies = [ "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "tower-service" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "try-lock" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "try_from" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "ucd-util" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "unicode-bidi" version = "0.3.4" @@ -1850,19 +797,35 @@ dependencies = [ "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "url" -version = "1.7.2" +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ureq" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "chunked_transfer 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "qstring 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.21.3 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki-roots 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1875,48 +838,11 @@ dependencies = [ "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "utf8-ranges" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "uuid" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "vcpkg" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "version_check" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "want" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -1928,8 +854,6 @@ version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen-macro 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1938,7 +862,7 @@ name = "wasm-bindgen-backend" version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bumpalo 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bumpalo 3.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1947,17 +871,6 @@ dependencies = [ "wasm-bindgen-shared 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.62" @@ -1994,9 +907,21 @@ dependencies = [ ] [[package]] -name = "winapi" -version = "0.2.8" +name = "webpki" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "webpki-roots" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "webpki 0.21.3 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "winapi" @@ -2007,11 +932,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -2030,54 +950,23 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "winreg" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "ws2_32-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - [metadata] -"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" -"checksum aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" "checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" "checksum arrayref 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" "checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" -"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" -"checksum backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)" = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e" -"checksum backtrace-sys 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "7de8aba10a69c8e8d7622c5710229485ec32e9d55fdad160ea559c086fdcd118" -"checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" "checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +"checksum base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" -"checksum bumpalo 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "12ae9db68ad7fac5fe51304d20f016c911539251075a214f8e663babefa35187" -"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" -"checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -"checksum bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" -"checksum cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" +"checksum bumpalo 3.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +"checksum cc 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)" = "0fde55d2a2bfaa4c9668bbc63f531fbdeee3ffe188f4662511ce2c22b3eedebe" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" -"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum chunked_transfer 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d29eb15132782371f71da8f947dba48b3717bdb6fa771b9b434d645e40a7193" "checksum constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" -"checksum cookie 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" -"checksum cookie_store 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46750b3f362965f197996c4448e4a0935e791bf7d6631bfce9ee0af3d24c919c" "checksum core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" "checksum core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" -"checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" -"checksum crossbeam-deque 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" -"checksum crossbeam-epoch 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" -"checksum crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" "checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" "checksum darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" "checksum darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" @@ -2088,190 +977,91 @@ dependencies = [ "checksum dirs-next 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1cbcf9241d9e8d106295bd496bbe2e9cffd5fa098f2a8c9e2bbcbf09773c11a8" "checksum dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" "checksum dirs-sys-next 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9c60f7b8a8953926148223260454befb50c751d3c50e1c178c4fd1ace4083c9a" -"checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" -"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" "checksum encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8d03faa7fe0c1431609dfad7bbe827af30f82e1e2ae6f7ee4fca6bd764bc28" -"checksum error-chain 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" -"checksum failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b8529c2421efa3066a5cbd8063d2244603824daccb6936b079010bb2aa89464b" -"checksum failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231" "checksum fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" "checksum fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -"checksum flate2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" "checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" -"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" -"checksum futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)" = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" -"checksum futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" -"checksum futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" -"checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" -"checksum futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" -"checksum futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6" -"checksum futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" -"checksum futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" -"checksum h2 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" -"checksum h2 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "377038bf3c89d18d6ca1431e7a5027194fbd724ca10592b9487ede5e8e144f42" -"checksum hermit-abi 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e" -"checksum http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" -"checksum http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" -"checksum http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" -"checksum http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" -"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" -"checksum hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)" = "9dbe6ed1438e1f8ad955a4701e9a944938e9519f6888d12d8558b645e247d5f6" -"checksum hyper 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96816e1d921eca64d208a85aab4f7798455a8e34229ee5a88c935bdee1b78b14" -"checksum hyper-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3a800d6aa50af4b5850b2b0f659625ce9504df908e9733b635720483be26174f" -"checksum hyper-tls 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3adcd308402b9553630734e9c36b77a7e48b3821251ca2493e8cd596763aafaa" "checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" -"checksum indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" -"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" "checksum js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)" = "fa5a448de267e7358beaf4a5d849518fe9a0c13fce7afd44b06e68550e5562a7" -"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -"checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)" = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0" +"checksum libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" "checksum libsqlite3-sys 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)" = "266eb8c361198e8d1f682bc974e5d9e2ae90049fb1943890904d11dad7d4a77d" "checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" -"checksum lock_api 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "79b2de95ecb4691949fea4716ca53cdbcfccb2c612e19644a8bad05edcf9f47b" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" "checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" -"checksum memoffset 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" -"checksum mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -"checksum mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" -"checksum miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" -"checksum mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)" = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" -"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" "checksum native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" "checksum ncurses 5.99.0 (registry+https://github.com/rust-lang/crates.io-index)" = "15699bee2f37e9f8828c7b35b2bc70d13846db453f2d507713b758fabe536b82" -"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" "checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" -"checksum num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" -"checksum openssl 0.10.28 (registry+https://github.com/rust-lang/crates.io-index)" = "973293749822d7dd6370d6da1e523b0d1db19f06c459134c658b2a4261378b52" +"checksum once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" +"checksum openssl 0.10.30 (registry+https://github.com/rust-lang/crates.io-index)" = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" -"checksum openssl-sys 0.9.54 (registry+https://github.com/rust-lang/crates.io-index)" = "1024c0a59774200a555087a6da3f253a9095a5f344e353b212ac4c8b8e450986" +"checksum openssl-sys 0.9.58 (registry+https://github.com/rust-lang/crates.io-index)" = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" "checksum pancurses 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d3058bc37c433096b2ac7afef1c5cdfae49ede0a4ffec3dfc1df1df0959d0ff0" -"checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" -"checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" "checksum pdcurses-sys 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "084dd22796ff60f1225d4eb6329f33afaf4c85419d51d440ab6b8c6f4529166b" -"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" "checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -"checksum pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6f6a7f5eee6292c559c793430c55c00aea9d3b3d1905e855806ca4d7253426a2" -"checksum pin-project-internal 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "8988430ce790d8682672117bc06dda364c0be32d3abd738234f19f3240bad99a" -"checksum pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" -"checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" -"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" +"checksum ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" "checksum proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" -"checksum publicsuffix 1.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" +"checksum qstring 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" "checksum quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0" "checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" -"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" "checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" "checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -"checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" "checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -"checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -"checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -"checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" -"checksum regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" "checksum regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" -"checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" "checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" -"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" -"checksum reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)" = "02b81e49ddec5109a9dcfc5f2a317ff53377c915e9ae9d4f2fb50914b85614e2" -"checksum reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)" = "f88643aea3c1343c804950d7bf983bd2067f5ab59db6d613a08e05572f2714ab" -"checksum rfc822_sanitizer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "680e8305c1e0cdf836dc4bec5424e045f278c975a3cac36d1ca01c4695f9d815" +"checksum remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +"checksum rfc822_sanitizer 0.3.3 (git+https://github.com/jeff-hughes/rfc822_sanitizer_copy)" = "" +"checksum ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)" = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4" "checksum rss 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "99979205510c60f80a119dedbabd0b8426517384edf205322f8bcd51796bcef9" "checksum rusqlite 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "64a656821bb6317a84b257737b7934f79c0dbb7eb694710475908280ebad3e64" "checksum rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" -"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" -"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -"checksum ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" +"checksum rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1" "checksum sanitize-filename 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "23fd0fec94ec480abfd86bb8f4f6c57e0efb36dac5c852add176ea7b04c74801" -"checksum schannel 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "039c25b130bd8c1321ee2d7de7fde2659fa9c2744e4bb29711cfc852ea53cd19" -"checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -"checksum security-framework 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "572dfa3a0785509e7a44b5b4bebcf94d41ba34e9ed9eb9df722545c3b3c4144a" -"checksum security-framework-sys 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8ddb15a5fec93b7021b8a9e96009c5d8d51c15673569f7c0f6b7204e5b7b404f" -"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +"checksum schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +"checksum sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +"checksum security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" +"checksum security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" "checksum serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" "checksum serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" -"checksum serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "78a7a12c167809363ec3bd7329fc0a3369056996de43c4b37ef3cd54a6ce4867" -"checksum serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a" -"checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" "checksum shellexpand 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2b22262a9aaf9464d356f656fea420634f78c881c5eebd5ef5e66d8b9bc603" -"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" -"checksum smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6" "checksum smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" -"checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" +"checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" "checksum strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" "checksum syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" -"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" -"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +"checksum textwrap 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" -"checksum tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" -"checksum tokio 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "34ef16d072d2b6dc8b4a56c70f5c5ced1a37752116f8e7c1e80c659aa7cb6713" -"checksum tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" -"checksum tokio-current-thread 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" -"checksum tokio-executor 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" -"checksum tokio-io 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" -"checksum tokio-reactor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" -"checksum tokio-sync 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" -"checksum tokio-tcp 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" -"checksum tokio-threadpool 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" -"checksum tokio-timer 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" -"checksum tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bde02a3a5291395f59b06ec6945a3077602fac2b07eeeaf0dee2122f3619828" -"checksum tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" "checksum toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" -"checksum tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" -"checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" -"checksum try_from 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" -"checksum ucd-util 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c85f514e095d348c279b1e5cd76795082cf15bd59b93207832abe0b1d8fed236" -"checksum unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" "checksum unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" +"checksum unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" -"checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +"checksum untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +"checksum ureq 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "677df6896edc382f1a2abcbb3e4058edfe973cdc4e1ed764b11891a7a289bfc0" "checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" -"checksum utf8-ranges 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" -"checksum uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" "checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" -"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" -"checksum want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" -"checksum want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" "checksum wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "e3c7d40d09cdbf0f4895ae58cf57d92e1e57a9dd8ed2e8390514b54a47cc5551" "checksum wasm-bindgen-backend 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "c3972e137ebf830900db522d6c8fd74d1900dcfc733462e9a12e942b00b4ac94" -"checksum wasm-bindgen-futures 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "8a369c5e1dfb7569e14d62af4da642a3cbc2f9a3652fe586e26ac22222aa4b04" "checksum wasm-bindgen-macro 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "2cd85aa2c579e8892442954685f0d801f9129de24fa2136b2c6a539c76b65776" "checksum wasm-bindgen-macro-support 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "8eb197bd3a47553334907ffd2f16507b4f4f01bbec3ac921a7719e0decdfe72a" "checksum wasm-bindgen-shared 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "a91c2916119c17a8e316507afaaa2dd94b47646048014bbdf6bef098c1bb58ad" "checksum web-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)" = "8bc359e5dd3b46cb9687a051d50a2fdd228e4ba7cf6fcf861a5365c3d671a642" -"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum webpki 0.21.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" +"checksum webpki-roots 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" -"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum winreg 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" -"checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" -"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" diff --git a/Cargo.toml b/Cargo.toml index f6e936f..a8aa43c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shellcaster" -version = "0.8.1" +version = "0.8.2" authors = ["Jeff Hughes "] edition = "2018" license = "GPL-3.0-or-later" @@ -17,15 +17,43 @@ readme = "README.md" [dependencies] pancurses = "0.16.1" +rss = "1.9.0" rusqlite = "0.21.0" -rss = { version = "1.9.0", features = ["from_url"] } -reqwest = { version = "0.10.4", features = ["blocking"] } toml = "0.5.6" -serde = "1.0.106" +serde = { version = "1.0.106", features = ["derive"] } chrono = "0.4.11" -rfc822_sanitizer = "0.3.3" +rfc822_sanitizer = { git = "https://github.com/jeff-hughes/rfc822_sanitizer_copy" } lazy_static = "1.4.0" regex = "1.3.6" sanitize-filename = "0.2.1" shellexpand = "2.0.0" -dirs = { package = "dirs-next", version = "1.0.1" } \ No newline at end of file +dirs = { package = "dirs-next", version = "1.0.1" } +textwrap = "0.12.1" + +[dependencies.ureq] +version = "1.3.0" +default-features = false + + +[features] +default = ["sqlite_bundled", "native-tls"] + +# bundle sqlite library with app; recommended for Windows. This is +# turned on by default, but if you are building this for a package +# manager, consider building with `--no-default-features` specified, and +# adding libsqlite3-dev or sqlite3 as a dependency on the package +sqlite_bundled = ["rusqlite/bundled"] + +# by default, shellcaster uses `native-tls` crate to enable TLS support; +# if this is causing issues for some websites, you can try building it +# to use `rustls` crate instead; build with `--no-default-features` and +# then specify `--features "rustls"` +native-tls = ["ureq/native-tls"] +rustls = ["ureq/tls"] + +# specific to Unix systems; see pancurses docs for more details +wide = ["pancurses/wide"] + +# specific to Windows; see pancurses docs for more details +win32 = ["pancurses/win32"] +win32a = ["pancurses/win32a"] \ No newline at end of file diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..7564823 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,8 @@ +[target.x86_64-unknown-linux-gnu] +image = "jeffhughes22/shellcaster_x86_64-unknown-linux-gnu:1.0" + +[target.aarch64-unknown-linux-gnu] +image = "jeffhughes22/shellcaster_aarch64-unknown-linux-gnu:1.0" + +[target.i686-unknown-linux-gnu] +image = "jeffhughes22/shellcaster_i686-unknown-linux-gnu:1.0" diff --git a/README.md b/README.md index d815569..e82fb8d 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,26 @@ Note that shellcaster is not yet in stable format, and is still in active develo ## Installing shellcaster -There are currently a couple of ways to install shellcaster. The following assumes you already have Rust + cargo installed. +There are currently a few ways to install shellcaster. -1. You can install the latest version of the binary directly from crates.io with one command: +1. If you are running Linux or MacOS on x86_64 (i.e., 64-bit), you can find binaries for the latest release on the [Releases page](https://github.com/jeff-hughes/shellcaster/releases). Download the `.tar.gz` file appropriate for your system, and install it with the following commands in your terminal: + +```bash +tar xzvf shellcaster-OS_NAME-x64_64-bundled.tar.gz +cd shellcaster +sudo cp shellcaster /usr/local/bin +shellcaster # to run +``` + +Replacing *OS_NAME* with the filename you downloaded. + +2. If you have Rust + cargo installed, you can install the latest version of the binary directly from crates.io with one command: ```bash cargo install shellcaster ``` -2. You can clone the Github repo and compile it yourself: +3. If you have Rust + cargo installed, you can also clone the Github repo and compile it yourself: ```bash git clone https://github.com/jeff-hughes/shellcaster.git @@ -46,9 +57,15 @@ Or you can put `config.toml` in a place of your choosing, and specify the locati shellcaster -c /path/to/config.toml ``` -**Note:** Shellcaster has currently only been tested on Linux x86. Earlier versions were tested on MacOS, but not extensively. You may be able to compile and use it on Windows as well, but you're on your own for that right now. If you are on Windows, your best bet at this point is to use Windows Subsystem for Linux (WSL) to compile and use it. +**Note:** Packages for various Linux distros are on their way -- stay tuned! + +## Platform support + +Shellcaster has currently only been tested extensively on Linux x86_64. Earlier versions were tested on MacOS, but not extensively. Unix systems in general, on x86_64 (64-bit), i686 (32-bit), and ARM, will be the primary targets for support for the app. -## Default Keybindings +Shellcaster is **not currently supported on Windows**, although some work has been done to try to get it working. Unicode support is weak, however, and there are issues when resizing the screen. You *might* have better luck using the new Windows Terminal and building with the `win32a` feature enabled, but this has not been tested. If you are a Windows user and want to help work out the bugs, pull requests are more than welcome! + +## Default keybindings | Key | Action | | ------- | -------------- | @@ -71,6 +88,10 @@ Keybindings can be modified in the config.toml file. Actions can be mapped to more than one key, but a single key may not do more than one action. +## Contributing + +Contributions from others are welcome! If you wish to contribute, feel free to clone the repo and submit pull requests. **Please ensure you are on the `develop` branch when making your edits**, as this is where the continued development of the app is taking place. Pull requests will only be merged to the `develop` branch, so you can help to avoid merge conflicts by doing your work on that branch in the first place. + ## Why "shellcaster"? I was trying to come up with a play on the word "podcast", and I liked the use of the word "shell" for several reasons. "Shell" is a synonym for the word "pod". The terminal is also referred to as a shell (and shellcaster is a terminal-based program). In addition, the program is built on Rust, whose mascot is Ferris the crab. Finally, I just personally enjoy that "shellcaster" sounds a lot like "spellcaster", so you can feel like a wizard when you use the program... \ No newline at end of file diff --git a/ci/aarch64.Dockerfile b/ci/aarch64.Dockerfile new file mode 100644 index 0000000..53bed02 --- /dev/null +++ b/ci/aarch64.Dockerfile @@ -0,0 +1,5 @@ +FROM rustembedded/cross:aarch64-unknown-linux-gnu + +RUN dpkg --add-architecture arm64 && \ + apt-get update && \ + apt-get install --assume-yes libncurses5-dev:arm64 libncursesw5-dev:arm64 libsqlite3-dev:arm64 \ No newline at end of file diff --git a/ci/i686.Dockerfile b/ci/i686.Dockerfile new file mode 100644 index 0000000..8c0ad84 --- /dev/null +++ b/ci/i686.Dockerfile @@ -0,0 +1,5 @@ +FROM rustembedded/cross:i686-unknown-linux-gnu + +RUN dpkg --add-architecture i386 && \ + apt-get update && \ + apt-get install --assume-yes libncurses5-dev:i386 libncursesw5-dev:i386 libsqlite3-dev:i386 diff --git a/ci/x86_64.Dockerfile b/ci/x86_64.Dockerfile new file mode 100644 index 0000000..23b2bf9 --- /dev/null +++ b/ci/x86_64.Dockerfile @@ -0,0 +1,4 @@ +FROM rustembedded/cross:x86_64-unknown-linux-gnu + +RUN apt-get update && \ + apt-get install --assume-yes libncurses5-dev libncursesw5-dev libsqlite3-dev diff --git a/config.toml b/config.toml index c3b22cf..12da92e 100644 --- a/config.toml +++ b/config.toml @@ -10,20 +10,27 @@ #download_path = "~/.local/share/shellcaster/" +# Command to use to play episodes. Use "%s" to indicate where file/URL +# will be entered to the command. +# Default: vlc %s + +play_command = "vlc %s" + + # Maximum number of files to download simultaneously. Setting this too # high could result in network requests being denied. A good general # guide would be to set this to the number of processor cores on your # computer. # Default: 3 -#simultaneous_downloads = 3; +#simultaneous_downloads = 3 -# Command to use to play episodes. Use "%s" to indicate where file/URL -# will be entered to the command. -# Default: vlc %s +# Maximum number of times to retry connecting to a URL to sync a +# podcast or download an episode. +# Default: 3 -play_command = "vlc %s" +#max_retries = 3 [keybindings] diff --git a/img/screenshot.png b/img/screenshot.png index 78db7d8..376709b 100644 Binary files a/img/screenshot.png and b/img/screenshot.png differ diff --git a/src/config.rs b/src/config.rs index adea193..da5949e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,12 +5,34 @@ use std::path::PathBuf; use crate::keymap::{Keybindings, UserAction}; +// Specifies how long, in milliseconds, to display messages at the +// bottom of the screen in the UI. +pub const MESSAGE_TIME: u64 = 5000; + +// How many columns we need, minimum, before we display the +// (unplayed/total) after the podcast title +pub const PODCAST_UNPLAYED_TOTALS_LENGTH: usize = 25; + +// How many columns we need, minimum, before we display the duration of +// the episode +pub const EPISODE_DURATION_LENGTH: usize = 45; + +// How many columns we need, minimum, before we display the pubdate +// of the episode +pub const EPISODE_PUBDATE_LENGTH: usize = 60; + +// How many columns we need (total terminal window width) before we +// display the details panel +pub const DETAILS_PANEL_LENGTH: i32 = 135; + + /// Holds information about user configuration of program. #[derive(Debug, Clone)] pub struct Config { pub download_path: PathBuf, - pub simultaneous_downloads: usize, pub play_command: String, + pub simultaneous_downloads: usize, + pub max_retries: usize, pub keybindings: Keybindings, } @@ -20,8 +42,9 @@ pub struct Config { #[derive(Deserialize)] struct ConfigFromToml { download_path: Option, - simultaneous_downloads: Option, play_command: Option, + simultaneous_downloads: Option, + max_retries: Option, keybindings: KeybindingsFromToml, } @@ -89,8 +112,9 @@ impl Config { }; config_toml = ConfigFromToml { download_path: None, - simultaneous_downloads: None, play_command: None, + simultaneous_downloads: None, + max_retries: None, keybindings: keybindings, }; } @@ -146,21 +170,28 @@ fn config_with_defaults(config_toml: &ConfigFromToml) -> Config { config_toml.download_path.as_deref(), dirs::data_local_dir()); + let play_command = match config_toml.play_command.as_deref() { + Some(cmd) => cmd.to_string(), + None => "vlc %s".to_string(), + }; + let simultaneous_downloads = match config_toml.simultaneous_downloads { Some(num) if num > 0 => num, Some(_) => 3, None => 3, }; - let play_command = match config_toml.play_command.as_deref() { - Some(cmd) => cmd.to_string(), - None => "vlc %s".to_string(), + let max_retries = match config_toml.max_retries { + Some(num) if num > 0 => num, + Some(_) => 3, + None => 3, }; return Config { download_path: download_path, - simultaneous_downloads: simultaneous_downloads, play_command: play_command, + simultaneous_downloads: simultaneous_downloads, + max_retries: max_retries, keybindings: keymap, }; } diff --git a/src/db.rs b/src/db.rs index d7447f8..593629e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -18,6 +18,9 @@ impl Database { /// it does not already exist). Panics if database cannot be accessed. pub fn connect(path: &PathBuf) -> Database { let mut db_path = path.clone(); + if std::fs::create_dir_all(&db_path).is_err() { + panic!("Unable to create subdirectory for database."); + } db_path.push("data.db"); match Connection::open(db_path) { Ok(conn) => { @@ -118,10 +121,14 @@ impl Database { let pod_id = stmt .query_row::(params![podcast.url], |row| row.get(0)) .unwrap(); - let num_episodes = podcast.episodes.borrow().len(); + let num_episodes; + { + let borrow = podcast.episodes.borrow(); + num_episodes = borrow.len(); - for ep in podcast.episodes.borrow().iter().rev() { - let _ = &self.insert_episode(pod_id, &ep)?; + for ep in borrow.iter().rev() { + let _ = &self.insert_episode(pod_id, &ep)?; + } } return Ok(num_episodes); @@ -250,13 +257,13 @@ impl Database { let conn = self.conn.as_ref().unwrap(); let mut stmt = conn.prepare( - "SELECT id, url, pubdate FROM episodes + "SELECT id, title, pubdate FROM episodes WHERE podcast_id = ?;").unwrap(); let episode_iter = stmt.query_map(params![podcast_id], |row| { - Ok((row.get("id")?, row.get("url")?, row.get("pubdate")?)) + Ok((row.get("id")?, row.get("title")?, row.get("pubdate")?)) }).unwrap(); - // create hashmap of all episodes, indexed by URL and pub date + // create hashmap of all episodes, indexed by title and pub date let mut ep_map: HashMap<(String, i64), i64> = HashMap::new(); for ep in episode_iter { let epuw = ep.unwrap(); @@ -264,7 +271,7 @@ impl Database { } for ep in episodes.borrow().iter().rev() { - match ep_map.get(&(ep.url.clone(), ep.pubdate.unwrap().timestamp())) { + match ep_map.get(&(ep.title.clone(), ep.pubdate.unwrap().timestamp())) { // update existing episode Some(id) => { let pubdate = match ep.pubdate { diff --git a/src/downloads.rs b/src/downloads.rs index 4cb91c8..75610f5 100644 --- a/src/downloads.rs +++ b/src/downloads.rs @@ -1,13 +1,11 @@ use std::fs::File; -use std::io::Write; use std::path::PathBuf; -use std::sync::{Arc, Mutex, mpsc, mpsc::Sender}; -use std::thread; +use std::sync::mpsc::Sender; -use reqwest::blocking::Client; use sanitize_filename::{sanitize_with_options, Options}; -use crate::types::{Episode, Message}; +use crate::types::Message; +use crate::threadpool::Threadpool; /// Enum used for communicating back to the main controller upon /// successful or unsuccessful downloading of a file. i32 value @@ -16,7 +14,6 @@ use crate::types::{Episode, Message}; pub enum DownloadMsg { Complete(EpData), ResponseError(EpData), - ResponseDataError(EpData), FileCreateError(EpData), FileWriteError(EpData), } @@ -26,189 +23,71 @@ pub enum DownloadMsg { pub struct EpData { pub id: i64, pub pod_id: i64, + pub title: String, pub url: String, - pub file_path: PathBuf, + pub file_path: Option, } +/// This is the function the main controller uses to indicate new +/// files to download. It uses the threadpool to start jobs +/// for every episode to be downloaded. New jobs can be requested +/// by the user while there are still ongoing jobs. +pub fn download_list(episodes: Vec, dest: &PathBuf, max_retries: usize, threadpool: &Threadpool, tx_to_main: Sender) { + // parse episode details and push to queue + for mut ep in episodes.into_iter() { + let file_name = sanitize_with_options(&ep.title, Options { + truncate: true, + windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone + replacement: "" + }); -/// Main controller for managing downloads. The DownloadManager will -/// spin up a threadpool the first time downloads are requested. -pub struct DownloadManager { - request_client: Client, - tx_to_main: Sender, - threadpool: Option, - n_threads: usize, -} - -impl DownloadManager { - /// Creates a new DownloadManager. - pub fn new(n_threads: usize, tx_to_main: Sender) -> DownloadManager { - return DownloadManager { - request_client: Client::new(), - tx_to_main: tx_to_main, - threadpool: None, - n_threads: n_threads, - }; - } - - /// Clones the reference to the reqwest client. - fn get_client(&self) -> Client { - return self.request_client.clone(); - } - - /// This is the method the main controller uses to indicate new - /// files to download. The DownloadManager will spin up a new - /// threadpool if one is not already running, and then starts jobs - /// for every episode to be downloaded. New jobs can be requested - /// by the user while there are still ongoing jobs. - pub fn download_list(&mut self, episodes: &[&Episode], dest: &PathBuf) { - // download thread and threadpool only exist when download - // queue is not empty - if self.threadpool.is_none() { - self.threadpool = Some(ThreadPool::new(self.n_threads)); - } - - // parse episode details and push to queue - for ep in episodes.iter() { - let file_name = sanitize_with_options(&ep.title, Options { - truncate: true, - windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone - replacement: "" - }); - - let mut file_path = dest.clone(); - file_path.push(format!("{}.mp3", file_name)); - - let ep_data = EpData { - id: ep.id.unwrap(), - pod_id: ep.pod_id.unwrap(), - url: ep.url.clone(), - file_path: file_path, - }; + let mut file_path = dest.clone(); + file_path.push(format!("{}.mp3", file_name)); - let client_clone = self.get_client(); - let tx_to_main = self.tx_to_main.clone(); + ep.file_path = Some(file_path); - self.threadpool.as_ref().unwrap().execute(move || { - let result = download_file(client_clone, ep_data); - tx_to_main.send(Message::Dl(result)).unwrap(); - }); - } + let tx = tx_to_main.clone(); + threadpool.execute(move || { + let result = download_file(ep, max_retries); + tx.send(Message::Dl(result)).unwrap(); + }); } } /// Downloads a file to a local filepath, returning DownloadMsg variant /// indicating success or failure. -fn download_file(client: Client, ep_data: EpData) -> DownloadMsg { - let response = client.get(&ep_data.url).send(); - if response.is_err() { - return DownloadMsg::ResponseError(ep_data); - } - - let resp_data = response.unwrap().bytes(); - if resp_data.is_err() { - return DownloadMsg::ResponseDataError(ep_data); - } +fn download_file(ep_data: EpData, mut max_retries: usize) -> DownloadMsg { + let data = ep_data.clone(); - let dst = File::create(&ep_data.file_path); + let dst = File::create(&ep_data.file_path.unwrap()); if dst.is_err() { - return DownloadMsg::FileCreateError(ep_data); + return DownloadMsg::FileCreateError(data); }; - return match dst.unwrap().write(&resp_data.unwrap()) { - Ok(_) => DownloadMsg::Complete(ep_data), - Err(_) => DownloadMsg::FileWriteError(ep_data), - }; -} - - -// Much of the threadpool implementation here was taken directly from -// the Rust Book: https://doc.rust-lang.org/book/ch20-02-multithreaded.html -// and https://doc.rust-lang.org/book/ch20-03-graceful-shutdown-and-cleanup.html - -/// Manages a threadpool of a given size, sending jobs to workers as -/// necessary. Implements Drop trait to allow threads to complete -/// their current jobs before being stopped. -struct ThreadPool { - workers: Vec, - sender: mpsc::Sender, -} - -impl ThreadPool { - /// Creates a new ThreadPool of a given size. - fn new(n_threads: usize) -> ThreadPool { - let (sender, receiver) = mpsc::channel(); - let receiver_lock = Arc::new(Mutex::new(receiver)); - - let mut workers = Vec::with_capacity(n_threads); - - for _ in 0..n_threads { - workers.push(Worker::new(Arc::clone(&receiver_lock))); - } - - return ThreadPool { - workers: workers, - sender: sender, - }; - } - - /// Adds a new job to the threadpool, passing closure to first - /// available worker. - fn execute(&self, func: F) - where F: FnOnce() + Send + 'static { - - let job = Box::new(func); - self.sender.send(JobMessage::NewJob(job)).unwrap(); - } -} - -impl Drop for ThreadPool { - /// Upon going out of scope, ThreadPool sends terminate message to - /// all workers but allows them to complete current jobs. - fn drop(&mut self) { - for _ in &self.workers { - self.sender.send(JobMessage::Terminate).unwrap(); - } - - for worker in &mut self.workers { - if let Some(thread) = worker.thread.take() { - // joins to ensure threads finish job before stopping - thread.join().unwrap(); + let request: Result = loop { + let response = ureq::get(&ep_data.url) + .timeout_connect(5000) + .timeout_read(30000) + .call(); + if response.error() { + max_retries -= 1; + if max_retries == 0 { + break Err(()); } + } else { + break Ok(response); } - } -} - -type Job = Box; - -/// Messages used by ThreadPool to communicate with Workers. -enum JobMessage { - NewJob(Job), - Terminate, -} - -/// Used by ThreadPool to complete jobs. Each Worker manages a single -/// thread. -struct Worker { - thread: Option>, -} - -impl Worker { - /// Creates a new Worker, which waits for Jobs to be passed by the - /// ThreadPool. - fn new(receiver: Arc>>) -> Worker { - let thread = thread::spawn(move || loop { - let message = receiver.lock().unwrap().recv().unwrap(); + }; - match message { - JobMessage::NewJob(job) => job(), - JobMessage::Terminate => break, - } - }); + if request.is_err() { + return DownloadMsg::ResponseError(data); + }; - return Worker { - thread: Some(thread), - }; - } + let response = request.unwrap(); + let mut reader = response.into_reader(); + return match std::io::copy(&mut reader, &mut dst.unwrap()) { + Ok(_) => DownloadMsg::Complete(data), + Err(_) => DownloadMsg::FileWriteError(data), + }; } \ No newline at end of file diff --git a/src/feeds.rs b/src/feeds.rs index e00c4ee..8143b44 100644 --- a/src/feeds.rs +++ b/src/feeds.rs @@ -1,5 +1,5 @@ -use std::thread; use std::sync::mpsc; +use std::io::Read; use rss::{Channel, Item}; use chrono::{DateTime, Utc}; @@ -8,6 +8,7 @@ use lazy_static::lazy_static; use regex::{Regex, Match}; use crate::types::{Podcast, Episode, Message, LockVec}; +use crate::threadpool::Threadpool; lazy_static! { /// Regex for parsing an episode "duration", which could take the form @@ -25,9 +26,9 @@ pub enum FeedMsg { } /// Spawns a new thread to check a feed and retrieve podcast data. -pub fn spawn_feed_checker(tx_to_main: mpsc::Sender, url: String, pod_id: Option) -> thread::JoinHandle<()> { - return thread::spawn(move || { - match get_feed_data(url) { +pub fn check_feed(url: String, pod_id: Option, max_retries: usize, threadpool: &Threadpool, tx_to_main: mpsc::Sender) { + threadpool.execute(move || { + match get_feed_data(url, max_retries) { Ok(mut pod) => { match pod_id { Some(id) => { @@ -46,9 +47,33 @@ pub fn spawn_feed_checker(tx_to_main: mpsc::Sender, url: String, pod_id /// Given a URL, this attempts to pull the data about a podcast and its /// episodes from an RSS feed. -pub fn get_feed_data(url: String) -> Result> { - let channel = Channel::from_url(&url)?; - return parse_feed_data(channel, &url); +fn get_feed_data(url: String, mut max_retries: usize) -> Result> { + let request: Result> = loop { + let response = ureq::get(&url) + .timeout_connect(5000) + .timeout_read(15000) + .call(); + if response.error() { + max_retries -= 1; + if max_retries == 0 { + break Err(String::from("TODO: Better error handling here.").into()); + } + } else { + break Ok(response); + } + }; + + return match request { + Ok(resp) => { + let mut reader = resp.into_reader(); + let mut resp_data = Vec::new(); + reader.read_to_end(&mut resp_data)?; + + let channel = Channel::read_from(&resp_data[..])?; + Ok(parse_feed_data(channel, &url)) + }, + Err(err) => Err(err), + } } @@ -57,7 +82,7 @@ pub fn get_feed_data(url: String) -> Result> /// specifications for podcast RSS feeds that a feed should adhere to, but /// this does try to make some attempt to account for the possibility that /// a feed might not be valid according to the spec. -pub fn parse_feed_data(channel: Channel, url: &str) -> Result> { +fn parse_feed_data(channel: Channel, url: &str) -> Podcast { let title = channel.title().to_string(); let url = url.to_string(); let description = Some(channel.description().to_string()); @@ -91,7 +116,7 @@ pub fn parse_feed_data(channel: Channel, url: &str) -> Result Result = std::env::args().collect(); @@ -73,130 +49,8 @@ fn main() { // MAIN LOOP -------------------------------------------------------- + main_ctrl.loop_msgs(); - // wait for messages from the UI and other threads, and then process - let mut message_iter = main_ctrl.rx_to_main.iter(); - while let Some(message) = message_iter.next() { - match message { - Message::Ui(UiMsg::Quit) => break, - - Message::Ui(UiMsg::AddFeed(url)) => { - let tx_feeds_to_main = mpsc::Sender::clone(&main_ctrl.tx_to_main); - let _ = feeds::spawn_feed_checker(tx_feeds_to_main, url, None); - }, - - Message::Feed(FeedMsg::NewData(pod)) => - main_ctrl.add_or_sync_data(pod, false), - - Message::Feed(FeedMsg::Error) => - main_ctrl.msg_to_ui("Error retrieving RSS feed.".to_string(), true), - - Message::Ui(UiMsg::Sync(pod_index)) => - main_ctrl.sync(Some(pod_index)), - - Message::Feed(FeedMsg::SyncData(pod)) => - main_ctrl.add_or_sync_data(pod, true), - - Message::Ui(UiMsg::SyncAll) => - main_ctrl.sync(None), - - Message::Ui(UiMsg::Play(pod_index, ep_index)) => - main_ctrl.play_file(pod_index, ep_index), - - Message::Ui(UiMsg::MarkPlayed(pod_index, ep_index, played)) => - main_ctrl.mark_played(pod_index, ep_index, played), - - Message::Ui(UiMsg::MarkAllPlayed(pod_index, played)) => - main_ctrl.mark_all_played(pod_index, played), - - // TODO: Stuck with this here for now because - // `main_ctrl.download_manager.download_list()` requires - // mutable borrow - Message::Ui(UiMsg::Download(pod_index, ep_index)) => { - let pod_title; - { - let borrowed_podcast_list = main_ctrl.podcasts.borrow(); - pod_title = borrowed_podcast_list[pod_index].title.clone(); - } - let episode = main_ctrl.podcasts - .clone_episode(pod_index, ep_index).unwrap(); - if episode.path.is_some() { - // don't re-download if file already exists - // TODO: Might want to revisit this decision at some - // point, and ask user if they want to re-download - // the file - return; - } - - // add directory for podcast, create if it does not exist - let dir_name = sanitize_with_options(&pod_title, Options { - truncate: true, - windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone - replacement: "" - }); - match main_ctrl.create_podcast_dir(dir_name) { - Ok(path) => main_ctrl.download_manager.download_list( - &[&episode], &path), - Err(_) => main_ctrl.msg_to_ui( - format!("Could not create dir: {}", pod_title), true), - } - }, - - // downloading can produce any one of these responses - Message::Dl(DownloadMsg::Complete(ep_data)) => - main_ctrl.download_complete(ep_data), - Message::Dl(DownloadMsg::ResponseError(_)) => - main_ctrl.msg_to_ui("Error sending download request.".to_string(), true), - Message::Dl(DownloadMsg::ResponseDataError(_)) => - main_ctrl.msg_to_ui("Error downloading episode.".to_string(), true), - Message::Dl(DownloadMsg::FileCreateError(_)) => - main_ctrl.msg_to_ui("Error creating file.".to_string(), true), - Message::Dl(DownloadMsg::FileWriteError(_)) => - main_ctrl.msg_to_ui("Error writing file to disk.".to_string(), true), - - // TODO: Stuck with this here for now because - // `main_ctrl.download_manager.download_list()` requires - // mutable borrow - Message::Ui(UiMsg::DownloadAll(pod_index)) => { - // TODO: Try to do this without cloning the podcast... - let podcast = main_ctrl.podcasts - .clone_podcast(pod_index).unwrap(); - let pod_title = podcast.title.clone(); - let borrowed_ep_list = podcast - .episodes.borrow(); - - let mut episodes = Vec::new(); - for e in borrowed_ep_list.iter() { - episodes.push(e); - } - - // add directory for podcast, create if it does not exist - let dir_name = sanitize_with_options(&pod_title, Options { - truncate: true, - windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone - replacement: "" - }); - match main_ctrl.create_podcast_dir(dir_name) { - Ok(path) => main_ctrl.download_manager.download_list( - &episodes, &path), - Err(_) => main_ctrl.msg_to_ui( - format!("Could not create dir: {}", pod_title), true), - } - }, - - Message::Ui(UiMsg::Delete(pod_index, ep_index)) => main_ctrl.delete_file(pod_index, ep_index), - - Message::Ui(UiMsg::DeleteAll(pod_index)) => main_ctrl.delete_files(pod_index), - - Message::Ui(UiMsg::RemovePodcast(pod_index, delete_files)) => main_ctrl.remove_podcast(pod_index, delete_files), - - Message::Ui(UiMsg::RemoveEpisode(pod_index, ep_index, delete_files)) => main_ctrl.remove_episode(pod_index, ep_index, delete_files), - - Message::Ui(UiMsg::RemoveAllEpisodes(pod_index, delete_files)) => main_ctrl.remove_all_episodes(pod_index, delete_files), - - Message::Ui(UiMsg::Noop) => (), - } - } // CLEANUP ---------------------------------------------------------- main_ctrl.tx_to_ui.send(MainMessage::UiTearDown).unwrap(); diff --git a/src/main_controller.rs b/src/main_controller.rs index 4123275..1f1a061 100644 --- a/src/main_controller.rs +++ b/src/main_controller.rs @@ -2,30 +2,36 @@ use std::path::PathBuf; use std::sync::mpsc; use std::fs; +use sanitize_filename::{sanitize_with_options, Options}; + use crate::types::*; use crate::config::Config; -use crate::ui::UI; +use crate::ui::{UI, UiMsg}; use crate::db::Database; -use crate::feeds; -use crate::downloads::{self, EpData}; +use crate::threadpool::Threadpool; +use crate::feeds::{self, FeedMsg}; +use crate::downloads::{self, EpData, DownloadMsg}; use crate::play_file; -use crate::MESSAGE_TIME; /// Enum used for communicating with other threads. #[derive(Debug)] pub enum MainMessage { UiUpdateMenus, - UiSpawnMsgWin(String, u64, bool), + UiSpawnNotif(String, bool, u64), + UiSpawnPersistentNotif(String, bool), + UiClearPersistentNotif, UiTearDown, } /// Main application controller, holding all of the main application /// state and mechanisms for communicatingg with the rest of the app. pub struct MainController { - pub config: Config, - pub db: Database, - pub download_manager: downloads::DownloadManager, - pub podcasts: LockVec, + config: Config, + db: Database, + threadpool: Threadpool, + podcasts: LockVec, + sync_tracker: usize, + download_tracker: usize, pub ui_thread: std::thread::JoinHandle<()>, pub tx_to_ui: mpsc::Sender, pub tx_to_main: mpsc::Sender, @@ -43,13 +49,10 @@ impl MainController { // get connection to the database let db_inst = Database::connect(&db_path); - - // set up download manager - let tx_dl_to_main = tx_to_main.clone(); - let download_manager = downloads::DownloadManager::new( - config.simultaneous_downloads, - tx_dl_to_main); - + + // set up threadpool + let threadpool = Threadpool::new(config.simultaneous_downloads); + // create vector of podcasts, where references are checked at // runtime; this is necessary because we want main.rs to hold the // "ground truth" list of podcasts, and it must be mutable, but @@ -65,60 +68,165 @@ impl MainController { return MainController { config: config, db: db_inst, - download_manager: download_manager, + threadpool: threadpool, podcasts: podcast_list, ui_thread: ui_thread, + sync_tracker: 0, + download_tracker: 0, tx_to_ui: tx_to_ui, tx_to_main: tx_to_main, rx_to_main: rx_to_main, }; } - /// Sends the specified message to the UI, which will display at + /// Initiates the main loop where the controller waits for messages coming in from the UI and other threads, and processes them. + pub fn loop_msgs(&mut self) { + while let Some(message) = self.rx_to_main.iter().next() { + match message { + Message::Ui(UiMsg::Quit) => break, + + Message::Ui(UiMsg::AddFeed(url)) => + self.add_podcast(url), + + Message::Feed(FeedMsg::NewData(pod)) => + self.add_or_sync_data(pod, false), + + Message::Feed(FeedMsg::Error) => + self.notif_to_ui("Error retrieving RSS feed.".to_string(), true), + + Message::Ui(UiMsg::Sync(pod_index)) => + self.sync(Some(pod_index)), + + Message::Feed(FeedMsg::SyncData(pod)) => + self.add_or_sync_data(pod, true), + + Message::Ui(UiMsg::SyncAll) => + self.sync(None), + + Message::Ui(UiMsg::Play(pod_index, ep_index)) => + self.play_file(pod_index, ep_index), + + Message::Ui(UiMsg::MarkPlayed(pod_index, ep_index, played)) => + self.mark_played(pod_index, ep_index, played), + + Message::Ui(UiMsg::MarkAllPlayed(pod_index, played)) => + self.mark_all_played(pod_index, played), + + Message::Ui(UiMsg::Download(pod_index, ep_index)) => + self.download(pod_index, Some(ep_index)), + + Message::Ui(UiMsg::DownloadAll(pod_index)) => + self.download(pod_index, None), + + // downloading can produce any one of these responses + Message::Dl(DownloadMsg::Complete(ep_data)) => + self.download_complete(ep_data), + Message::Dl(DownloadMsg::ResponseError(_)) => + self.notif_to_ui("Error sending download request.".to_string(), true), + Message::Dl(DownloadMsg::FileCreateError(_)) => + self.notif_to_ui("Error creating file.".to_string(), true), + Message::Dl(DownloadMsg::FileWriteError(_)) => + self.notif_to_ui("Error downloading episode.".to_string(), true), + + Message::Ui(UiMsg::Delete(pod_index, ep_index)) => + self.delete_file(pod_index, ep_index), + + Message::Ui(UiMsg::DeleteAll(pod_index)) => + self.delete_files(pod_index), + + Message::Ui(UiMsg::RemovePodcast(pod_index, delete_files)) => + self.remove_podcast(pod_index, delete_files), + + Message::Ui(UiMsg::RemoveEpisode(pod_index, ep_index, delete_files)) => + self.remove_episode(pod_index, ep_index, delete_files), + + Message::Ui(UiMsg::RemoveAllEpisodes(pod_index, delete_files)) => + self.remove_all_episodes(pod_index, delete_files), + + Message::Ui(UiMsg::Noop) => (), + } + } + } + + /// Sends the specified notification to the UI, which will display at /// the bottom of the screen. - pub fn msg_to_ui(&self, message: String, error: bool) { - self.tx_to_ui.send(MainMessage::UiSpawnMsgWin( - message, MESSAGE_TIME, error)).unwrap(); + pub fn notif_to_ui(&self, message: String, error: bool) { + self.tx_to_ui.send(MainMessage::UiSpawnNotif( + message, error, crate::config::MESSAGE_TIME)).unwrap(); + } + + /// Sends a persistent notification to the UI, which will display at + /// the bottom of the screen until cleared. + pub fn persistent_notif_to_ui(&self, message: String, error: bool) { + self.tx_to_ui.send(MainMessage::UiSpawnPersistentNotif( + message, error)).unwrap(); } + /// Clears persistent notifications in the UI. + pub fn clear_persistent_notif(&self) { + self.tx_to_ui.send(MainMessage::UiClearPersistentNotif).unwrap(); + } + + /// Updates the persistent notification about syncing podcasts and + /// downloading files. + pub fn update_tracker_notif(&self) { + let sync_len = self.sync_tracker; + let dl_len = self.download_tracker; + let sync_plural = if sync_len > 1 { "s" } else { "" }; + let dl_plural = if dl_len > 1 { "s" } else { "" }; + + if sync_len > 0 && dl_len > 0 { + let notif = format!("Syncing {} podcast{}, downloading {} episode{}...", sync_len, sync_plural, dl_len, dl_plural); + self.persistent_notif_to_ui(notif, false); + } else if sync_len > 0 { + let notif = format!("Syncing {} podcast{}...", sync_len, sync_plural); + self.persistent_notif_to_ui(notif, false); + } else if dl_len > 0 { + let notif = format!("Downloading {} episode{}...", dl_len, dl_plural); + self.persistent_notif_to_ui(notif, false); + } else { + self.clear_persistent_notif(); + } + } + + /// Add a new podcast by fetching the RSS feed data. + pub fn add_podcast(&self, url: String) { + feeds::check_feed(url, None, self.config.max_retries, + &self.threadpool, self.tx_to_main.clone()); + } + /// Synchronize RSS feed data for one or more podcasts. - pub fn sync(&self, pod_index: Option) { + pub fn sync(&mut self, pod_index: Option) { // We pull out the data we need here first, so we can // stop borrowing the podcast list as quickly as possible. // Slightly less efficient (two loops instead of // one), but then it won't block other tasks that // need to access the list. let mut pod_data = Vec::new(); - { - let borrowed_pod_list = self.podcasts - .borrow(); - match pod_index { - Some(idx) => { - // just grab one podcast - let podcast = &borrowed_pod_list[idx]; - pod_data.push((podcast.url.clone(), podcast.id)); - }, - None => { - // get all of 'em! - for podcast in borrowed_pod_list.iter() { - pod_data.push((podcast.url.clone(), podcast.id)); - } - }, - } + match pod_index { + // just grab one podcast + Some(idx) => pod_data.push(self.podcasts + .map_single(idx, + |pod| (pod.url.clone(), pod.id)) + .unwrap()), + // get all of 'em! + None => pod_data = self.podcasts + .map(|pod| (pod.url.clone(), pod.id)), } for data in pod_data.into_iter() { let url = data.0; let id = data.1; - - let tx_feeds_to_main = mpsc::Sender::clone(&self.tx_to_main); - let _ = feeds::spawn_feed_checker(tx_feeds_to_main, url, id); + self.sync_tracker += 1; + feeds::check_feed(url, id, self.config.max_retries, + &self.threadpool, self.tx_to_main.clone()) } + self.update_tracker_notif(); } /// Handles the application logic for adding a new podcast, or /// synchronizing data from the RSS feed of an existing podcast. #[allow(clippy::useless_let_if_seq)] - pub fn add_or_sync_data(&self, pod: Podcast, update: bool) { + pub fn add_or_sync_data(&mut self, pod: Podcast, update: bool) { let title = pod.title.clone(); let db_result; let failure; @@ -132,22 +240,29 @@ impl MainController { } match db_result { Ok(num_ep) => { - *self.podcasts.borrow() = self.db.get_podcasts(); + { + *self.podcasts.borrow() = self.db.get_podcasts(); + } self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); if update { - self.msg_to_ui(format!("Synchronized {}.", title), false); + self.sync_tracker -= 1; + self.update_tracker_notif(); + if self.sync_tracker == 0 { + self.notif_to_ui(format!("Synchronized {}.", title), false); + } } else { - self.msg_to_ui(format!("Successfully added {} episodes.", num_ep), false); + self.notif_to_ui(format!("Successfully added {} episodes.", num_ep), false); } }, - Err(_err) => self.msg_to_ui(failure, true), + Err(_err) => self.notif_to_ui(failure, true), } } /// Attempts to execute the play command on the given podcast /// episode. pub fn play_file(&self, pod_index: usize, ep_index: usize) { + self.mark_played(pod_index, ep_index, true); let episode = self.podcasts.clone_episode( pod_index, ep_index).unwrap(); @@ -157,18 +272,18 @@ impl MainController { match path.to_str() { Some(p) => { if play_file::execute(&self.config.play_command, &p).is_err() { - self.msg_to_ui( + self.notif_to_ui( "Error: Could not play file. Check configuration.".to_string(), true); } }, - None => self.msg_to_ui( + None => self.notif_to_ui( "Error: Filepath is not valid Unicode.".to_string(), true), } }, // otherwise, try to stream the URL None => { if play_file::execute(&self.config.play_command, &episode.url).is_err() { - self.msg_to_ui( + self.notif_to_ui( "Error: Could not stream URL.".to_string(),true); } } @@ -177,7 +292,7 @@ impl MainController { /// Given a podcast and episode, it marks the given episode as /// played/unplayed, sending this info to the database and updating - /// in main_ctrl.podcasts + /// in self.podcasts pub fn mark_played(&self, pod_index: usize, ep_index: usize, played: bool) { let mut podcast = self.podcasts.clone_podcast(pod_index).unwrap(); @@ -186,7 +301,6 @@ impl MainController { let mut episode = podcast.episodes.clone_episode(ep_index).unwrap(); episode.played = played; - // eprintln!("{}", ep_index); self.db.set_played_status(episode.id.unwrap(), played); podcast.episodes.replace(ep_index, episode).unwrap(); @@ -201,7 +315,7 @@ impl MainController { /// Given a podcast, it marks all episodes for that podcast as /// played/unplayed, sending this info to the database and updating - /// in main_ctrl.podcasts + /// in self.podcasts pub fn mark_all_played(&self, pod_index: usize, played: bool) { let mut podcast = self.podcasts.clone_podcast(pod_index).unwrap(); let n_eps; @@ -226,42 +340,79 @@ impl MainController { self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); } - // TODO: Right now this can't be used because the main loop is - // borrowing the MainController object, and the last line of this - // function mutates MainController, so the borrow checker complains. - // pub fn download(&mut self, pod_index: usize, ep_index: Option) { - // // TODO: Try to do this without cloning the podcast... - // let podcast = self.podcasts - // .clone_podcast(pod_index).unwrap(); - // let pod_title = podcast.title.clone(); - // let borrowed_ep_list = podcast - // .episodes.borrow(); - - // let mut episodes = Vec::new(); - - // // if we are selecting one specific episode, just grab that one; - // // otherwise, loop through them all - // match ep_index { - // Some(ep_idx) => episodes.push(&borrowed_ep_list[ep_idx]), - // None => { - // for e in borrowed_ep_list.iter() { - // episodes.push(e); - // } - // } - // } - - // // add directory for podcast, create if it does not exist - // match main_ctrl.create_podcast_dir(pod_title.clone()) { - // Ok(path) => main_ctrl.download_manager.download_list( - // &episodes, &path), - // Err(_) => main_ctrl.msg_to_ui( - // format!("Could not create dir: {}", pod_title)), - // }) - // } + /// Given a podcast index (and not an episode index), this will send + /// a vector of jobs to the threadpool to download all episodes in + /// the podcast. If given an episode index as well, it will download + /// just that episode. + pub fn download(&mut self, pod_index: usize, ep_index: Option) { + let pod_title; + let mut ep_data = Vec::new(); + { + // TODO: Try to do this without cloning the podcast... + let podcast = self.podcasts + .clone_podcast(pod_index).unwrap(); + pod_title = podcast.title.clone(); + + // if we are selecting one specific episode, just grab that + // one; otherwise, loop through them all + match ep_index { + Some(ep_idx) => { + // grab just the relevant data we need + let data = podcast.episodes.map_single(ep_idx, + |ep| (EpData { + id: ep.id.unwrap(), + pod_id: ep.pod_id.unwrap(), + title: ep.title.clone(), + url: ep.url.clone(), + file_path: None, + }, ep.path.is_none())).unwrap(); + if data.1 { + ep_data.push(data.0); + } + }, + None => { + // grab just the relevant data we need + ep_data = podcast.episodes + .filter_map(|ep| if ep.path.is_none() { + Some(EpData { + id: ep.id.unwrap(), + pod_id: ep.pod_id.unwrap(), + title: ep.title.clone(), + url: ep.url.clone(), + file_path: None, + }) + } else { + None + }); + } + } + } + + if !ep_data.is_empty() { + // add directory for podcast, create if it does not exist + let dir_name = sanitize_with_options(&pod_title, Options { + truncate: true, + windows: true, // for simplicity, we'll just use Windows-friendly paths for everyone + replacement: "" + }); + match self.create_podcast_dir(dir_name) { + Ok(path) => { + self.download_tracker += ep_data.len(); + downloads::download_list( + ep_data, &path, self.config.max_retries, + &self.threadpool, self.tx_to_main.clone()); + }, + Err(_) => self.notif_to_ui( + format!("Could not create dir: {}", pod_title), true), + } + self.update_tracker_notif(); + } + } /// Handles logic for what to do when a download successfully completes. - pub fn download_complete(&self, ep_data: EpData) { - let _ = self.db.insert_file(ep_data.id, &ep_data.file_path); + pub fn download_complete(&mut self, ep_data: EpData) { + let file_path = ep_data.file_path.unwrap(); + let _ = self.db.insert_file(ep_data.id, &file_path); { let pod_index = self.podcasts .id_to_index(ep_data.pod_id).unwrap(); @@ -272,10 +423,16 @@ impl MainController { let ep_index = podcast.episodes .id_to_index(ep_data.id).unwrap(); let mut episode = podcast.episodes.clone_episode(ep_index).unwrap(); - episode.path = Some(ep_data.file_path); + episode.path = Some(file_path); podcast.episodes.replace(ep_index, episode).unwrap(); } + self.download_tracker -= 1; + self.update_tracker_notif(); + if self.download_tracker == 0 { + self.notif_to_ui("Downloads complete.".to_string(), false); + } + self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); } @@ -306,10 +463,10 @@ impl MainController { borrowed_podcast.episodes.replace(ep_index, episode).unwrap(); self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); - self.msg_to_ui( + self.notif_to_ui( format!("Deleted \"{}\"", title), false); }, - Err(_) => self.msg_to_ui( + Err(_) => self.notif_to_ui( format!("Error deleting \"{}\"", title), true), } } @@ -345,10 +502,10 @@ impl MainController { self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); if success { - self.msg_to_ui( + self.notif_to_ui( "Files successfully deleted.".to_string(), false); } else { - self.msg_to_ui( + self.notif_to_ui( "Error while deleting files".to_string(), true); } } @@ -360,12 +517,12 @@ impl MainController { self.delete_files(pod_index); } - let mut borrowed_podcast_list = self.podcasts.borrow(); - let borrowed_podcast = borrowed_podcast_list.get(pod_index).unwrap(); - - self.db.remove_podcast(borrowed_podcast.id.unwrap()); - - *borrowed_podcast_list = self.db.get_podcasts(); + let pod_id = self.podcasts + .map_single(pod_index, |pod| pod.id).unwrap(); + self.db.remove_podcast(pod_id.unwrap()); + { + *self.podcasts.borrow() = self.db.get_podcasts(); + } self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); } @@ -377,13 +534,15 @@ impl MainController { } let borrowed_podcast_list = self.podcasts.borrow(); - let borrowed_podcast = borrowed_podcast_list.get(pod_index).unwrap(); - let mut borrowed_ep_list = borrowed_podcast.episodes.borrow(); - let episode = borrowed_ep_list.get(ep_index).unwrap(); - - self.db.hide_episode(episode.id.unwrap(), true); + let borrowed_podcast = borrowed_podcast_list + .get(pod_index).unwrap(); - *borrowed_ep_list = self.db.get_episodes(borrowed_podcast.id.unwrap()); + let ep_id = borrowed_podcast.episodes + .map_single(ep_index, |ep| ep.id).unwrap(); + self.db.hide_episode(ep_id.unwrap(), true); + { + *borrowed_podcast.episodes.borrow() = self.db.get_episodes(borrowed_podcast.id.unwrap()); + } self.tx_to_ui.send(MainMessage::UiUpdateMenus).unwrap(); } diff --git a/src/threadpool.rs b/src/threadpool.rs new file mode 100644 index 0000000..d3baeee --- /dev/null +++ b/src/threadpool.rs @@ -0,0 +1,92 @@ +use std::sync::{Arc, Mutex, mpsc}; +use std::thread; + +// Much of the threadpool implementation here was taken directly from +// the Rust Book: https://doc.rust-lang.org/book/ch20-02-multithreaded.html +// and https://doc.rust-lang.org/book/ch20-03-graceful-shutdown-and-cleanup.html + +/// Manages a threadpool of a given size, sending jobs to workers as +/// necessary. Implements Drop trait to allow threads to complete +/// their current jobs before being stopped. +pub struct Threadpool { + workers: Vec, + sender: mpsc::Sender, +} + +impl Threadpool { + /// Creates a new Threadpool of a given size. + pub fn new(n_threads: usize) -> Threadpool { + let (sender, receiver) = mpsc::channel(); + let receiver_lock = Arc::new(Mutex::new(receiver)); + + let mut workers = Vec::with_capacity(n_threads); + + for _ in 0..n_threads { + workers.push(Worker::new(Arc::clone(&receiver_lock))); + } + + return Threadpool { + workers: workers, + sender: sender, + }; + } + + /// Adds a new job to the threadpool, passing closure to first + /// available worker. + pub fn execute(&self, func: F) + where F: FnOnce() + Send + 'static { + + let job = Box::new(func); + self.sender.send(JobMessage::NewJob(job)).unwrap(); + } +} + +impl Drop for Threadpool { + /// Upon going out of scope, Threadpool sends terminate message to + /// all workers but allows them to complete current jobs. + fn drop(&mut self) { + for _ in &self.workers { + self.sender.send(JobMessage::Terminate).unwrap(); + } + + for worker in &mut self.workers { + if let Some(thread) = worker.thread.take() { + // joins to ensure threads finish job before stopping + thread.join().unwrap(); + } + } + } +} + +type Job = Box; + +/// Messages used by Threadpool to communicate with Workers. +enum JobMessage { + NewJob(Job), + Terminate, +} + +/// Used by Threadpool to complete jobs. Each Worker manages a single +/// thread. +struct Worker { + thread: Option>, +} + +impl Worker { + /// Creates a new Worker, which waits for Jobs to be passed by the + /// Threadpool. + fn new(receiver: Arc>>) -> Worker { + let thread = thread::spawn(move || loop { + let message = receiver.lock().unwrap().recv().unwrap(); + + match message { + JobMessage::NewJob(job) => job(), + JobMessage::Terminate => break, + } + }); + + return Worker { + thread: Some(thread), + }; + } +} \ No newline at end of file diff --git a/src/types.rs b/src/types.rs index e413c72..55e984c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -35,7 +35,7 @@ impl Menuable for Podcast { let mut out = self.title.substring(0, length); // if the size available is big enough, we add the unplayed data // to the end - if length > super::PODCAST_UNPLAYED_TOTALS_LENGTH { + if length > crate::config::PODCAST_UNPLAYED_TOTALS_LENGTH { let meta_str = format!("({}/{})", self.num_unplayed, self.episodes.len()); out = out.substring(0, length-meta_str.chars().count()); @@ -72,7 +72,7 @@ pub struct Episode { impl Episode { /// Formats the duration in seconds into an HH:MM:SS format. - fn format_duration(&self) -> String { + pub fn format_duration(&self) -> String { return match self.duration { Some(dur) => { let mut seconds = dur; @@ -94,7 +94,7 @@ impl Menuable for Episode { Some(_) => format!("[D] {}", self.title.substring(0, length-4)), None => self.title.substring(0, length).to_string(), }; - if length > super::EPISODE_PUBDATE_LENGTH { + if length > crate::config::EPISODE_PUBDATE_LENGTH { let dur = self.format_duration(); let meta_dur = format!("[{}]", dur); @@ -109,7 +109,7 @@ impl Menuable for Episode { // just print duration return format!("{} {:>width$}", out.substring(0, length-meta_dur.chars().count()), meta_dur, width=length-out.chars().count()); } - } else if length > super::EPISODE_DURATION_LENGTH { + } else if length > crate::config::EPISODE_DURATION_LENGTH { let dur = self.format_duration(); let meta_dur = format!("[{}]", dur); return format!("{} {:>width$}", out.substring(0, length-meta_dur.chars().count()), meta_dur, width=length-out.chars().count()); @@ -160,6 +160,51 @@ impl LockVec { } } + /// Maps a closure to every element in the LockVec, in the same way + /// as an Iterator. However, to avoid issues with keeping the borrow + /// alive, the function returns a Vec of the collected results, + /// rather than an iterator. + pub fn map(&self, f: F) -> Vec + where F: FnMut(&T) -> B { + + let borrowed = self.borrow(); + return borrowed.iter().map(f).collect(); + } + + /// Maps a closure to a single element in the LockVec, specified by + /// `index`. If there is no element at `index`, this returns None. + pub fn map_single(&self, index: usize, f: F) -> Option + where F: FnOnce(&T) -> B { + + let borrowed = self.borrow(); + return match borrowed.get(index) { + Some(item) => Some(f(item)), + None => return None, + }; + } + + /// Maps a closure to every element in the LockVec, in the same way + /// as the `filter_map()` does on an Iterator, both mapping and + /// filtering. However, to avoid issues with keeping the borrow + /// alive, the function returns a Vec of the collected results, + /// rather than an iterator. + pub fn filter_map(&self, f: F) -> Vec + where F: FnMut(&T) -> Option { + + let borrowed = self.borrow(); + return borrowed.iter().filter_map(f).collect(); + } + + /// Implements the same functionality as the `fold()` method of an + /// Iterator, applying a function that accumulates a value over + /// each element to produce a single, final value. + // pub fn fold(&self, init: B, f: F) -> B + // where F: FnMut(B, &T) -> B { + + // let borrowed = self.borrow(); + // return borrowed.iter().fold(init, f); + // } + pub fn len(&self) -> usize { return self.borrow().len(); } diff --git a/src/ui/menu.rs b/src/ui/menu.rs index 5bde208..c148e78 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -1,8 +1,9 @@ use std::cmp::min; -use pancurses::Window; use crate::types::*; -use super::{Colors, ColorType}; +use super::ColorType; +use super::Panel; + /// Generic struct holding details about a list menu. These menus are /// contained by the UI, and hold the list of podcasts or podcast @@ -22,13 +23,8 @@ use super::{Colors, ColorType}; #[derive(Debug)] pub struct Menu where T: Clone + Menuable { - pub window: Window, - pub screen_pos: usize, - pub colors: Colors, - pub title: String, + pub panel: Panel, pub items: LockVec, - pub n_row: i32, - pub n_col: i32, pub top_row: i32, // top row of text shown in window pub selected: i32, // which line of text is highlighted } @@ -37,42 +33,14 @@ impl Menu { /// Prints the list of visible items to the pancurses window and /// refreshes it. pub fn init(&mut self) { - self.draw_border(); + self.panel.init(); self.update_items(); } - /// Draws a border around the window. - fn draw_border(&self) { - let top_left; - let bot_left; - match self.screen_pos { - 0 => { - top_left = pancurses::ACS_ULCORNER(); - bot_left = pancurses::ACS_LLCORNER(); - } - _ => { - top_left = pancurses::ACS_TTEE(); - bot_left = pancurses::ACS_BTEE(); - } - } - self.window.border( - pancurses::ACS_VLINE(), - pancurses::ACS_VLINE(), - pancurses::ACS_HLINE(), - pancurses::ACS_HLINE(), - top_left, - pancurses::ACS_URCORNER(), - bot_left, - pancurses::ACS_LRCORNER()); - - self.window.mvaddstr(0, 2, self.title.clone()); - } - /// Prints or reprints the list of visible items to the pancurses /// window and refreshes it. pub fn update_items(&mut self) { - self.window.erase(); - self.draw_border(); + self.panel.erase(); let borrow = self.items.borrow(); if !borrow.is_empty() { @@ -84,11 +52,11 @@ impl Menu { } // for visible rows, print strings from list - for i in 0..self.n_row { + for i in 0..self.panel.get_rows() { let item_idx = (self.top_row + i) as usize; if let Some(elem) = borrow.get(item_idx) { - self.window.mvaddstr(self.abs_y(i), self.abs_x(0), - elem.get_title(self.n_col as usize)); + self.panel.write_line(i, + elem.get_title(self.panel.get_cols() as usize)); // this is literally the same logic as // self.set_attrs(), but it's complaining about @@ -98,16 +66,14 @@ impl Menu { } else { pancurses::A_BOLD }; - self.window.mvchgat(self.abs_y(i), self.abs_x(-1), - self.n_col + 3, - attr, - self.colors.get(ColorType::Normal)); + self.panel.change_attr(i, -1, self.panel.get_cols() + 3, + attr, ColorType::Normal); } else { break; } } } - self.window.refresh(); + self.panel.refresh(); } /// Scrolls the menu up or down by `lines` lines. Negative values of @@ -122,64 +88,66 @@ impl Menu { let new_played; { - let borrow = self.items.borrow(); - if borrow.is_empty() { + let list_len = self.items.len(); + if list_len == 0 { return; } + let n_row = self.panel.get_rows(); + // TODO: currently only handles scroll value of 1; need to extend // to be able to scroll multiple lines at a time old_selected = self.selected; self.selected += lines; - // don't allow scrolling past last item in list (if shorter than - // self.n_row) - let abs_bottom = min(self.n_row, - (borrow.len() - 1) as i32); + // don't allow scrolling past last item in list (if shorter + // than self.panel.get_rows()) + let abs_bottom = min(self.panel.get_rows(), + (list_len - 1) as i32); if self.selected > abs_bottom { self.selected = abs_bottom; } // scroll list if necessary: // scroll down - if self.selected > (self.n_row - 1) { - self.selected = self.n_row - 1; - if let Some(elem) = borrow.get((self.top_row + self.n_row) as usize) { + if self.selected > (n_row - 1) { + self.selected = n_row - 1; + if let Some(title) = self.items + .map_single((self.top_row + n_row) as usize, + |el| el.get_title(self.panel.get_cols() as usize)) { + self.top_row += 1; - self.window.mv(self.abs_y(0), self.abs_x(0)); - self.window.deleteln(); + self.panel.delete_line(0); old_selected -= 1; - self.window.mv(self.abs_y(self.n_row-1), self.abs_x(-1)); - self.window.clrtobot(); - self.window.mvaddstr(self.abs_y(self.n_row-1), self.abs_x(0), elem.get_title(self.n_col as usize)); - - self.draw_border(); + self.panel.delete_line(n_row-1); + self.panel.write_line(n_row-1, title); } // scroll up } else if self.selected < 0 { self.selected = 0; - if let Some(elem) = borrow.get((self.top_row - 1) as usize) { + if let Some(title) = self.items + .map_single((self.top_row - 1) as usize, + |el| el.get_title(self.panel.get_cols() as usize)) { + self.top_row -= 1; - self.window.mv(self.abs_y(0), 0); - self.window.insertln(); + self.panel.insert_line(0, title); old_selected += 1; - - self.window.mv(self.abs_y(0), self.abs_x(0)); - self.window.addstr(elem.get_title(self.n_col as usize)); - - self.draw_border(); } } - old_played = borrow.get((self.top_row + old_selected) as usize).unwrap().is_played(); - new_played = borrow.get((self.top_row + self.selected) as usize).unwrap().is_played(); + old_played = self.items + .map_single((self.top_row + old_selected) as usize, + |el| el.is_played()).unwrap(); + new_played = self.items + .map_single((self.top_row + self.selected) as usize, + |el| el.is_played()).unwrap(); } self.set_attrs(old_selected, old_played, ColorType::Normal); self.set_attrs(self.selected, new_played, ColorType::HighlightedActive); - self.window.refresh(); + self.panel.refresh(); } /// Sets font style and color of menu item. `index` is the position @@ -193,24 +161,16 @@ impl Menu { } else { pancurses::A_BOLD }; - self.window.mvchgat(self.abs_y(index), self.abs_x(-1), - self.n_col + 3, - attr, - self.colors.get(color)); + self.panel.change_attr(index, -1, self.panel.get_cols() + 3, + attr, color); } /// Highlights the currently selected item in the menu, based on /// whether the menu is currently active or not. pub fn highlight_selected(&mut self, active_menu: bool) { - let mut is_played = None; - { - let borrow = self.items.borrow(); - let selected = borrow.get((self.top_row + self.selected) as usize); - - if let Some(el) = selected { - is_played = Some(el.is_played()); - } - } + let is_played = self.items + .map_single((self.top_row + self.selected) as usize, + |el| el.is_played()); if let Some(played) = is_played { if active_menu { @@ -218,49 +178,35 @@ impl Menu { } else { self.set_attrs(self.selected, played, ColorType::Highlighted); } - self.window.refresh(); + self.panel.refresh(); } } /// Controls how the window changes when it is active (i.e., available /// for user input to modify state). pub fn activate(&mut self) { - let played; - { - let borrow = self.items.borrow(); - if borrow.is_empty() { - return; - } - played = borrow.get(self.selected as usize).unwrap().is_played(); + // if list is empty, will return None + if let Some(played) = self.items + .map_single((self.top_row + self.selected) as usize, + |el| el.is_played()) { + + self.set_attrs(self.selected, played, ColorType::HighlightedActive); + self.panel.refresh(); } - self.set_attrs(self.selected, played, ColorType::HighlightedActive); - self.window.refresh(); } /// Updates window size - pub fn resize(&mut self, n_row: i32, n_col: i32) { - self.n_row = n_row; - self.n_col = n_col; + pub fn resize(&mut self, n_row: i32, n_col: i32, start_y: i32, start_x: i32) { + self.panel.resize(n_row, n_col, start_y, start_x); + let n_row = self.panel.get_rows(); // if resizing moves selected item off screen, scroll the list // upwards to keep same item selected - if self.selected > (self.n_row - 1) { - self.top_row = self.top_row + self.selected - (self.n_row - 1); - self.selected = self.n_row - 1; + if self.selected > (n_row - 1) { + self.top_row = self.top_row + self.selected - (n_row - 1); + self.selected = n_row - 1; } } - - /// Calculates the y-value relative to the window rather than to the - /// menu (i.e., taking into account borders and margins). - fn abs_y(&self, y: i32) -> i32 { - return y + 1; - } - - /// Calculates the x-value relative to the window rather than to the - /// menu (i.e., taking into account borders and margins). - fn abs_x(&self, x: i32) -> i32 { - return x + 2; - } } @@ -276,16 +222,14 @@ impl Menu { /// Controls how the window changes when it is inactive (i.e., not /// available for user input to modify state). pub fn deactivate(&mut self) { - let played; - { - let borrow = self.items.borrow(); - if borrow.is_empty() { - return; - } - played = borrow.get(self.selected as usize).unwrap().is_played(); + // if list is empty, will return None + if let Some(played) = self.items + .map_single((self.top_row + self.selected) as usize, + |el| el.is_played()) { + + self.set_attrs(self.selected, played, ColorType::Highlighted); + self.panel.refresh(); } - self.set_attrs(self.selected, played, ColorType::Highlighted); - self.window.refresh(); } } @@ -293,10 +237,164 @@ impl Menu { /// Controls how the window changes when it is inactive (i.e., not /// available for user input to modify state). pub fn deactivate(&mut self) { - if !self.items.borrow().is_empty() { - let played = self.items.borrow().get(self.selected as usize).unwrap().is_played(); + // if list is empty, will return None + if let Some(played) = self.items + .map_single((self.top_row + self.selected) as usize, + |el| el.is_played()) { + self.set_attrs(self.selected, played, ColorType::Normal); + self.panel.refresh(); + } + } +} + + +// TESTS ----------------------------------------------------------------- +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + + fn create_menu(n_row: i32, n_col: i32, top_row: i32, selected: i32) -> Menu { + let titles = vec![ + "A Very Cool Episode", + "This is a very long episode title but we'll get through it together", + "An episode with le Unicodé", + "How does an episode with emoji sound? 😉", + "Here's another title", + "Un titre, c'est moi!", + "One more just for good measure" + ]; + let mut items = Vec::new(); + for (i, t) in titles.iter().enumerate() { + let played = i % 2 == 0; + items.push(Episode { + id: None, + pod_id: None, + title: t.to_string(), + url: String::new(), + description: String::new(), + pubdate: Some(Utc::now()), + duration: Some(12345), + path: None, + played: played, + }); } - self.window.refresh(); + + let panel = Panel::new( + crate::ui::colors::set_colors(), + "Episodes".to_string(), + 1, + n_row, n_col, + 0, 0 + ); + return Menu { + panel: panel, + items: LockVec::new(items), + top_row: top_row, + selected: selected, + }; + } + + #[test] + fn scroll_up() { + let real_rows = 5; + let real_cols = 65; + let mut menu = create_menu(real_rows+2, real_cols+5, 2, 0); + menu.update_items(); + + menu.scroll(-1); + + let borrow = menu.items.borrow(); + let expected_top = borrow[1].get_title(real_cols as usize); + let expected_bot = borrow[5].get_title(real_cols as usize); + + assert_eq!(menu.panel.get_row(0).0, expected_top); + assert_eq!(menu.panel.get_row(4).0, expected_bot); + } + + #[test] + fn scroll_down() { + let real_rows = 5; + let real_cols = 65; + let mut menu = create_menu(real_rows+2, real_cols+5, 0, 4); + menu.update_items(); + + menu.scroll(1); + + let borrow = menu.items.borrow(); + let expected_top = borrow[1].get_title(real_cols as usize); + let expected_bot = borrow[5].get_title(real_cols as usize); + + assert_eq!(menu.panel.get_row(0).0, expected_top); + assert_eq!(menu.panel.get_row(4).0, expected_bot); + } + + #[test] + fn resize_bigger() { + let real_rows = 5; + let real_cols = 65; + let mut menu = create_menu(real_rows+2, real_cols+5, 0, 4); + menu.update_items(); + + menu.resize(real_rows+2+5, real_cols+5+5, 0, 0); + menu.update_items(); + + assert_eq!(menu.top_row, 0); + assert_eq!(menu.selected, 4); + + let non_empty: Vec = menu.panel.window.iter() + .filter_map(|x| if x.0.is_empty() { + None + } else { + Some(x.0.clone()) + }).collect(); + assert_eq!(non_empty.len(), menu.items.len()); + } + + #[test] + fn resize_smaller() { + let real_rows = 7; + let real_cols = 65; + let mut menu = create_menu(real_rows+2, real_cols+5, 0, 6); + menu.update_items(); + + menu.resize(real_rows+2-2, real_cols+5-5, 0, 0); + menu.update_items(); + + assert_eq!(menu.top_row, 2); + assert_eq!(menu.selected, 4); + + let non_empty: Vec = menu.panel.window.iter() + .filter_map(|x| if x.0.is_empty() { + None + } else { + Some(x.0.clone()) + }).collect(); + assert_eq!(non_empty.len(), (real_rows-2) as usize); + } + + #[test] + fn chop_accent() { + let real_rows = 5; + let real_cols = 25; + let mut menu = create_menu(real_rows+2, real_cols+5, 0, 0); + menu.update_items(); + + let expected = "An episode with le Unicod".to_string(); + + assert_eq!(menu.panel.get_row(2).0, expected); + } + + #[test] + fn chop_emoji() { + let real_rows = 5; + let real_cols = 38; + let mut menu = create_menu(real_rows+2, real_cols+5, 0, 0); + menu.update_items(); + + let expected = "How does an episode with emoji sound? ".to_string(); + + assert_eq!(menu.panel.get_row(3).0, expected); } } \ No newline at end of file diff --git a/src/ui/mock_panel.rs b/src/ui/mock_panel.rs new file mode 100644 index 0000000..3bd85b6 --- /dev/null +++ b/src/ui/mock_panel.rs @@ -0,0 +1,183 @@ +use chrono::{DateTime, Utc}; +use super::{Colors, ColorType}; + +/// Struct holding the raw data used for building the details panel. +pub struct Details { + pub pod_title: Option, + pub ep_title: Option, + pub pubdate: Option>, + pub duration: Option, + pub explicit: Option, + pub description: Option, +} + +#[derive(Debug)] +pub struct Panel { + pub window: Vec<(String, pancurses::chtype, ColorType)>, + pub screen_pos: usize, + pub colors: Colors, + pub title: String, + pub n_row: i32, + pub n_col: i32, +} + +impl Panel { + pub fn new(colors: Colors, + title: String, screen_pos: usize, n_row: i32, n_col: i32, _start_y: i32, _start_x: i32) -> Self { + + // we represent the window as a vector of Strings instead of + // the pancurses window + let panel_win = vec![ + (String::new(), pancurses::A_NORMAL, ColorType::Normal); + (n_row-2) as usize]; + + return Panel { + window: panel_win, + screen_pos: screen_pos, + colors: colors, + title: title, + n_row: n_row, + n_col: n_col, + }; + } + + pub fn init(&self) {} + + pub fn refresh(&self) {} + + pub fn erase(&mut self) { + self.window = vec![ + (String::new(), pancurses::A_NORMAL, ColorType::Normal); + self.n_row as usize]; + } + + pub fn write_line(&mut self, y: i32, string: String) { + self.window[y as usize] = (string, pancurses::A_NORMAL, ColorType::Normal); + } + + pub fn insert_line(&mut self, y: i32, string: String) { + self.window.insert(y as usize, + (string, pancurses::A_NORMAL, ColorType::Normal)); + let _ = self.window.pop(); + } + + pub fn delete_line(&mut self, y: i32) { + let _ = self.window.remove(y as usize); + // add a new empty line to the end so the vector stays the + // same size + self.window.push((String::new(), pancurses::A_NORMAL, ColorType::Normal)); + } + + pub fn write_wrap_line(&mut self, start_y: i32, string: String) -> i32 { + let mut row = start_y; + let max_row = self.get_rows(); + let wrapper = textwrap::wrap_iter(&string, self.get_cols() as usize); + for line in wrapper { + self.write_line(row, line.to_string()); + row += 1; + + if row >= max_row { + break; + } + } + return row-1; + } + + pub fn details_template(&mut self, start_y: i32, details: Details) { + let mut row = start_y-1; + + // podcast title + match details.pod_title { + Some(t) => row = self.write_wrap_line(row+1, t), + None => row = self.write_wrap_line(row+1, "No title".to_string()), + } + + // episode title + match details.ep_title { + Some(t) => row = self.write_wrap_line(row+1, t), + None => row = self.write_wrap_line(row+1, "No title".to_string()), + } + + row += 1; // blank line + + // published date + if let Some(date) = details.pubdate { + let new_row = self.write_wrap_line(row+1, + format!("Published: {}", date.format("%B %-d, %Y").to_string())); + self.change_attr(row+1, 0, 10, + pancurses::A_UNDERLINE, ColorType::Normal); + row = new_row; + } + + // duration + if let Some(dur) = details.duration { + let new_row = self.write_wrap_line(row+1, + format!("Duration: {}", dur)); + self.change_attr(row+1, 0, 9, + pancurses::A_UNDERLINE, ColorType::Normal); + row = new_row; + } + + // explicit + if let Some(exp) = details.explicit { + let new_row = if exp { + self.write_wrap_line(row+1, "Explicit: Yes".to_string()) + } else { + self.write_wrap_line(row+1, "Explicit: No".to_string()) + }; + self.change_attr(row+1, 0, 9, + pancurses::A_UNDERLINE, ColorType::Normal); + row = new_row; + } + + row += 1; // blank line + + // description + match details.description { + Some(desc) => { + row = self.write_wrap_line(row+1, "Description:".to_string()); + let _row = self.write_wrap_line(row+1, desc); + }, + None => { + let _row = self.write_wrap_line(row+1, "No description.".to_string()); + }, + } + } + + // This doesn't fully replicate the functionality of Panel, as it + // only applies the attribute to the line as a whole, rather than + // specific characters. But I'm primarily using it to change whole + // lines anyway. + pub fn change_attr(&mut self, y: i32, _x: i32, _nchars: i32, attr: pancurses::chtype, color: ColorType) { + let current = &self.window[y as usize]; + self.window[y as usize] = (current.0.clone(), attr, color); + } + + pub fn resize(&mut self, n_row: i32, n_col: i32, _start_y: i32, _start_x: i32) { + self.n_row = n_row; + self.n_col = n_col; + + let new_len = (n_row-2) as usize; + let len = self.window.len(); + if new_len < len { + self.window.truncate(new_len); + } else if new_len > len { + for _ in (new_len - len)..new_len { + self.window.push((String::new(), pancurses::A_NORMAL, ColorType::Normal)); + } + } + } + + pub fn get_rows(&self) -> i32 { + return self.n_row - 2; // border on top and bottom + } + + pub fn get_cols(&self) -> i32 { + return self.n_col - 5; // 2 for border, 2 for margins, and 1 + // extra for some reason... + } + + pub fn get_row(&self, row: usize) -> (String, pancurses::chtype, ColorType) { + return self.window[row].clone(); + } +} \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3f2f210..9eb9482 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,18 +2,37 @@ use std::thread; use std::sync::mpsc; use std::time::Duration; +#[cfg_attr(not(test), path="panel.rs")] +#[cfg_attr(test, path="mock_panel.rs")] +mod panel; + mod menu; +mod notification; mod colors; +use self::panel::{Panel, Details}; use self::menu::Menu; +use self::notification::NotifWin; use self::colors::{Colors, ColorType}; -use pancurses::{Window, newwin, Input}; +use pancurses::{Window, Input}; +use lazy_static::lazy_static; +use regex::Regex; + use crate::config::Config; use crate::keymap::{Keybindings, UserAction}; use crate::types::*; use super::MainMessage; +lazy_static! { + /// Regex for finding HTML tags + static ref RE_HTML_TAGS: Regex = Regex::new(r"<[^<>]*>").unwrap(); + + /// Regex for finding more than two line breaks + static ref RE_MULT_LINE_BREAKS: Regex = Regex::new(r"((\r\n)|\r|\n){3,}").unwrap(); +} + + /// Enum used for communicating back to the main controller after user /// input has been captured by the UI. usize values always represent the /// selected podcast, and (if applicable), the selected episode, in that @@ -58,7 +77,9 @@ pub struct UI<'a> { podcast_menu: Menu, episode_menu: Menu, active_menu: ActiveMenu, - welcome_win: Option, + details_panel: Option, + notif_win: NotifWin, + welcome_win: Option, } impl<'a> UI<'a> { @@ -67,10 +88,14 @@ impl<'a> UI<'a> { pub fn spawn(config: Config, items: LockVec, rx_from_main: mpsc::Receiver, tx_to_main: mpsc::Sender) -> thread::JoinHandle<()> { return thread::spawn(move || { let mut ui = UI::new(&config, &items); + ui.init(); let mut message_iter = rx_from_main.try_iter(); - // on each loop, we check for user input, then we process - // any messages from the main thread + // this is the main event loop: on each loop, we update + // any messages at the bottom, check for user input, and + // then process any messages from the main thread loop { + ui.notif_win.check_notifs(); + match ui.getch() { UiMsg::Noop => (), input => tx_to_main.send(Message::Ui(input)).unwrap(), @@ -79,7 +104,9 @@ impl<'a> UI<'a> { if let Some(message) = message_iter.next() { match message { MainMessage::UiUpdateMenus => ui.update_menus(), - MainMessage::UiSpawnMsgWin(msg, duration, error) => ui.spawn_msg_win(msg, duration, error), + MainMessage::UiSpawnNotif(msg, duration, error) => ui.timed_notif(msg, error, duration), + MainMessage::UiSpawnPersistentNotif(msg, error) => ui.persistent_notif(msg, error), + MainMessage::UiClearPersistentNotif => ui.clear_persistent_notif(), MainMessage::UiTearDown => { ui.tear_down(); break; @@ -113,58 +140,59 @@ impl<'a> UI<'a> { let colors = self::colors::set_colors(); let (n_row, n_col) = stdscr.get_max_yx(); - - let pod_col = n_col / 2; - let ep_col = n_col - pod_col + 1; - - let podcast_menu_win = newwin(n_row - 1, pod_col, 0, 0); - let mut podcast_menu = Menu { - window: podcast_menu_win, - screen_pos: 0, - colors: colors.clone(), - title: "Podcasts".to_string(), + let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); + + let podcast_panel = Panel::new( + colors.clone(), + "Podcasts".to_string(), + 0, + n_row - 1, pod_col, + 0, 0 + ); + let podcast_menu = Menu { + panel: podcast_panel, items: items.clone(), - n_row: n_row - 3, // 2 for border and 1 for messages at bottom - n_col: pod_col - 5, // 2 for border, 2 for margins top_row: 0, selected: 0, }; - stdscr.noutrefresh(); - podcast_menu.init(); - podcast_menu.activate(); - podcast_menu.window.noutrefresh(); - - let episode_menu_win = newwin(n_row - 1, ep_col, 0, pod_col - 1); + let episode_panel = Panel::new( + colors.clone(), + "Episodes".to_string(), + 1, + n_row - 1, ep_col, + 0, pod_col - 1 + ); let first_pod: LockVec = match items.borrow().get(0) { Some(pod) => pod.episodes.clone(), None => LockVec::new(Vec::new()), }; - let mut episode_menu = Menu { - window: episode_menu_win, - screen_pos: 1, - colors: colors.clone(), - title: "Episodes".to_string(), + let episode_menu = Menu { + panel: episode_panel, items: first_pod, - n_row: n_row - 3, // 2 for border and 1 for messages at bottom - n_col: ep_col - 5, // 2 for border, 2 for margins, and... - // 1 more for luck? I have no idea why - // this needs an extra 1, but it works top_row: 0, selected: 0, }; - episode_menu.init(); - episode_menu.window.noutrefresh(); + + let details_panel = if n_col > crate::config::DETAILS_PANEL_LENGTH { + Some(Self::make_details_panel( + colors.clone(), + n_row-1, det_col, + 0, pod_col + ep_col - 2)) + } else { + None + }; + + let notif_win = NotifWin::new(colors.clone(), + n_row, n_col); // welcome screen if user does not have any podcasts yet let welcome_win = if items.borrow().is_empty() { - Some(UI::make_welcome_win(&config.keybindings, n_row, n_col)) + Some(UI::make_welcome_win(colors.clone(), &config.keybindings, n_row-1, n_col)) } else { None }; - pancurses::doupdate(); - return UI { stdscr, n_row, @@ -174,10 +202,27 @@ impl<'a> UI<'a> { podcast_menu: podcast_menu, episode_menu: episode_menu, active_menu: ActiveMenu::PodcastMenu, + details_panel: details_panel, + notif_win: notif_win, welcome_win: welcome_win, }; } + /// This should be called immediately after creating the UI, in order + /// to draw everything to the screen. + pub fn init(&mut self) { + self.stdscr.refresh(); + self.podcast_menu.init(); + self.podcast_menu.activate(); + self.episode_menu.init(); + self.update_details_panel(); + + if self.welcome_win.is_some() { + let ww = self.welcome_win.as_mut().unwrap(); + ww.refresh(); + } + } + /// Waits for user input and, where necessary, provides UiMessages /// back to the main controller. /// @@ -194,23 +239,25 @@ impl<'a> UI<'a> { self.n_row = n_row; self.n_col = n_col; - let pod_col = n_col / 2; - let ep_col = n_col - pod_col; - self.podcast_menu.resize(n_row-3, pod_col-5); - self.episode_menu.resize(n_row-3, ep_col-5); - - // apparently pancurses does not implement `wresize()` - // from ncurses, so instead we create an entirely new - // window every time the terminal is resized...not ideal, - // but c'est la vie - let pod_oldwin = std::mem::replace( - &mut self.podcast_menu.window, - newwin(n_row-1, pod_col, 0, 0)); - let ep_oldwin = std::mem::replace( - &mut self.episode_menu.window, - newwin(n_row-1, ep_col, 0, pod_col-1)); - pod_oldwin.delwin(); - ep_oldwin.delwin(); + let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); + + self.podcast_menu.resize(n_row-1, pod_col, 0, 0); + self.episode_menu.resize(n_row-1, ep_col, 0, pod_col - 1); + + if self.details_panel.is_some() { + if det_col > 0 { + let det = self.details_panel.as_mut().unwrap(); + det.resize(n_row-1, det_col, 0, pod_col+ep_col-2); + } else { + self.details_panel = None; + } + } else if det_col > 0 { + self.details_panel = Some(Self::make_details_panel( + self.colors.clone(), + n_row-1, det_col, + 0, pod_col + ep_col - 2)); + } + self.stdscr.refresh(); self.update_menus(); @@ -222,14 +269,21 @@ impl<'a> UI<'a> { }, } + if self.details_panel.is_some() { + self.update_details_panel(); + } + // resize welcome window, if it exists if self.welcome_win.is_some() { - let oldwwin = std::mem::replace( + let _ = std::mem::replace( &mut self.welcome_win, - Some(UI::make_welcome_win(&self.keymap, n_row, n_col))); + Some(UI::make_welcome_win(self.colors.clone(), &self.keymap, n_row-1, n_col))); - oldwwin.unwrap().delwin(); + let ww = self.welcome_win.as_mut().unwrap(); + ww.refresh(); } + + self.notif_win.resize(n_row, n_col); self.stdscr.refresh(); }, @@ -244,8 +298,7 @@ impl<'a> UI<'a> { // get rid of the "welcome" window once the podcast list // is no longer empty if self.welcome_win.is_some() && pod_len > 0 { - let ww = self.welcome_win.take().unwrap(); - ww.delwin(); + self.welcome_win = None; } match self.keymap.get_from_input(input) { @@ -261,11 +314,13 @@ impl<'a> UI<'a> { // update episodes menu with new list self.episode_menu.items = self.podcast_menu.get_episodes(); self.episode_menu.update_items(); + self.update_details_panel(); } }, ActiveMenu::EpisodeMenu => { if ep_len > 0 { self.episode_menu.scroll(1); + self.update_details_panel(); } }, } @@ -283,11 +338,13 @@ impl<'a> UI<'a> { // update episodes menu with new list self.episode_menu.items = self.podcast_menu.get_episodes(); self.episode_menu.update_items(); + self.update_details_panel(); } }, ActiveMenu::EpisodeMenu => { if pod_len > 0 { self.episode_menu.scroll(-1); + self.update_details_panel(); } }, } @@ -320,7 +377,7 @@ impl<'a> UI<'a> { }, Some(UserAction::AddFeed) => { - let url = &self.spawn_input_win("Feed URL: "); + let url = &self.spawn_input_notif("Feed URL: "); if !url.is_empty() { return UiMsg::AddFeed(url.to_string()); } @@ -419,7 +476,7 @@ impl<'a> UI<'a> { } if any_downloaded { - let ask_delete = self.spawn_yes_no_win("Delete local files too?"); + let ask_delete = self.spawn_yes_no_notif("Delete local files too?"); delete = match ask_delete { Some(val) => val, None => false, // default not to delete @@ -441,7 +498,7 @@ impl<'a> UI<'a> { .path.is_some(); } if is_downloaded { - let ask_delete = self.spawn_yes_no_win("Delete local file too?"); + let ask_delete = self.spawn_yes_no_notif("Delete local file too?"); delete = match ask_delete { Some(val) => val, None => false, // default not to delete @@ -473,7 +530,7 @@ impl<'a> UI<'a> { } if any_downloaded { - let ask_delete = self.spawn_yes_no_win("Delete local files too?"); + let ask_delete = self.spawn_yes_no_notif("Delete local files too?"); delete = match ask_delete { Some(val) => val, None => false, // default not to delete @@ -497,101 +554,45 @@ impl<'a> UI<'a> { return UiMsg::Noop; } - /// Adds a one-line pancurses window to the bottom of the screen to - /// solicit user text input. A prefix can be specified as a prompt - /// for the user at the beginning of the input line. This returns the - /// user's input; if the user cancels their input, the String will be - /// empty. - pub fn spawn_input_win(&self, prefix: &str) -> String { - let input_win = newwin(1, self.n_col, self.n_row-1, 0); - // input_win.overlay(&self.podcast_menu.window); - input_win.mv(self.n_row-1, 0); - input_win.addstr(&prefix); - input_win.keypad(true); - input_win.refresh(); - pancurses::curs_set(2); - - let mut inputs = String::new(); - let mut cancelled = false; - - let min_x = prefix.len() as i32; - let mut current_x = prefix.len() as i32; - let mut cursor_x = prefix.len() as i32; - loop { - match input_win.getch() { - // Cancel input - Some(Input::KeyExit) | - Some(Input::Character('\u{1b}')) => { - cancelled = true; - break; - }, - // Complete input - Some(Input::KeyEnter) | - Some(Input::Character('\n')) => { - break; - }, - Some(Input::KeyBackspace) | - Some(Input::Character('\u{7f}')) => { - if current_x > min_x { - current_x -= 1; - cursor_x -= 1; - let _ = inputs.remove((cursor_x as usize) - prefix.len()); - input_win.mv(0, cursor_x); - input_win.delch(); - } - }, - Some(Input::KeyDC) => { - if cursor_x < current_x { - let _ = inputs.remove((cursor_x as usize) - prefix.len()); - input_win.delch(); - } - }, - Some(Input::KeyLeft) => { - if cursor_x > min_x { - cursor_x -= 1; - input_win.mv(0, cursor_x); - } - }, - Some(Input::KeyRight) => { - if cursor_x < current_x { - cursor_x += 1; - input_win.mv(0, cursor_x); - } - }, - Some(Input::Character(c)) => { - current_x += 1; - cursor_x += 1; - input_win.insch(c); - input_win.mv(0, cursor_x); - inputs.push(c); - }, - Some(_) => (), - None => (), - } - input_win.refresh(); + /// Calculates the number of columns to allocate for each of the + /// main panels: podcast menu, episodes menu, and details panel; if + /// the screen is too small to display the details panel, this size + /// will be 0 + #[allow(clippy::useless_let_if_seq)] + pub fn calculate_sizes(n_col: i32) -> (i32, i32, i32) { + let pod_col; + let ep_col; + let det_col; + if n_col > crate::config::DETAILS_PANEL_LENGTH { + pod_col = n_col / 3; + ep_col = n_col / 3 + 1; + det_col = n_col - pod_col - ep_col + 2; + } else { + pod_col = n_col / 2; + ep_col = n_col - pod_col + 1; + det_col = 0; } + return (pod_col, ep_col, det_col); + } - pancurses::curs_set(0); - input_win.deleteln(); - input_win.refresh(); - input_win.delwin(); - - if cancelled { - return String::from(""); - } - return inputs; + /// Adds a notification to the bottom of the screen that solicits + /// user text input. A prefix can be specified as a prompt for the + /// user at the beginning of the input line. This returns the user's + /// input; if the user cancels their input, the String will be empty. + pub fn spawn_input_notif(&self, prefix: &str) -> String { + return self.notif_win.input_notif(prefix); } - /// Adds a one-line pancurses window to the bottom of the screen to - /// solicit user for a yes/no input. A prefix can be specified as a - /// prompt for the user at the beginning of the input line. "(y/n)" - /// will automatically be appended to the end of the prefix. If the - /// user types 'y' or 'n', the boolean will represent this value. If - /// the user cancels the input or types anything else, the function - /// will return None. - pub fn spawn_yes_no_win(&self, prefix: &str) -> Option { + /// Adds a notification to the bottom of the screen that solicits + /// user for a yes/no input. A prefix can be specified as a prompt + /// for the user at the beginning of the input line. "(y/n)" will + /// automatically be appended to the end of the prefix. If the user + /// types 'y' or 'n', the boolean will represent this value. If the + /// user cancels the input or types anything else, the function will + /// return None. + pub fn spawn_yes_no_notif(&self, prefix: &str) -> Option { let mut out_val = None; - let input = self.spawn_input_win(&format!("{} {}", prefix, "(y/n) ")); + let input = self.notif_win.input_notif(&format!("{} {}", prefix, "(y/n) ")); if let Some(c) = input.trim().chars().next() { if c == 'Y' || c == 'y' { out_val = Some(true); @@ -602,42 +603,36 @@ impl<'a> UI<'a> { return out_val; } - /// Adds a one-line pancurses window to the bottom of the screen for - /// displaying messages to the user. `duration` indicates how long - /// (in milliseconds) this message will remain on screen. Useful for - /// presenting error messages, among other things. - pub fn spawn_msg_win(&self, message: String, duration: u64, error: bool) { - let n_col = self.n_col; - let begy = self.n_row - 1; - let err_color = self.colors.get(ColorType::Error); - thread::spawn(move || { - let msg_win = newwin(1, n_col, begy, 0); - msg_win.mv(begy, 0); - msg_win.attrset(pancurses::A_NORMAL); - msg_win.addstr(message); - - if error { - msg_win.mvchgat(0, 0, -1, pancurses::A_BOLD, - err_color); - } - msg_win.refresh(); - - // TODO: This probably should be some async function, but this - // works for now - // pancurses::napms(duration); - thread::sleep(Duration::from_millis(duration)); - - msg_win.erase(); - msg_win.refresh(); - msg_win.delwin(); - }); + /// Adds a notification to the bottom of the screen for `duration` + /// time (in milliseconds). Useful for presenting error messages, + /// among other things. + pub fn timed_notif(&mut self, message: String, duration: u64, error: bool) { + self.notif_win.timed_notif(message, duration, error); + } + + /// Adds a notification to the bottom of the screen that will stay on + /// screen indefinitely. Must use `clear_persistent_msg()` to erase. + pub fn persistent_notif(&mut self, message: String, error: bool) { + self.notif_win.persistent_notif(message, error); + } + + /// Clears any persistent notification that is being displayed at the + /// bottom of the screen. Does not affect timed notifications, user + /// input notifications, etc. + pub fn clear_persistent_notif(&mut self) { + self.notif_win.clear_persistent_notif(); } /// Forces the menus to check the list of podcasts/episodes again and /// update. pub fn update_menus(&mut self) { self.podcast_menu.update_items(); - self.episode_menu.items = self.podcast_menu.get_episodes(); + + self.episode_menu.items = if self.podcast_menu.items.len() > 0 { + self.podcast_menu.get_episodes() + } else { + LockVec::new(Vec::new()) + }; self.episode_menu.update_items(); match self.active_menu { @@ -655,11 +650,82 @@ impl<'a> UI<'a> { pancurses::endwin(); } + /// Create a details panel. + pub fn make_details_panel(colors: Colors, n_row: i32, n_col: i32, start_y: i32, start_x: i32) -> Panel { + return Panel::new( + colors, + "Details".to_string(), + 2, + n_row, n_col, + start_y, start_x); + } + + /// Updates the details panel with information about the current + /// podcast and episode, and redraws to the screen. + pub fn update_details_panel(&mut self) { + if self.details_panel.is_some() { + let det = self.details_panel.as_mut().unwrap(); + det.erase(); + if self.episode_menu.items.len() > 0 { + // let det = self.details_panel.as_ref().unwrap(); + let current_pod = (self.podcast_menu.selected + + self.podcast_menu.top_row) as usize; + let current_ep = (self.episode_menu.selected + + self.episode_menu.top_row) as usize; + + // get a couple details from the current podcast + let mut pod_title = None; + let mut pod_explicit = None; + if let Some(pod) = self.podcast_menu.items.borrow().get(current_pod) { + pod_title = if pod.title.is_empty() { + None + } else { + Some(pod.title.clone()) + }; + pod_explicit = pod.explicit; + }; + + // the rest of the details come from the current episode + if let Some(ep) = self.episode_menu.items.borrow().get(current_ep) { + let ep_title = if ep.title.is_empty() { + None + } else { + Some(ep.title.clone()) + }; + + let desc = if ep.description.is_empty() { + None + } else { + // strip all HTML tags and excessive line breaks + let stripped_tags = RE_HTML_TAGS.replace_all(&ep.description, "").to_string(); + + // remove anything more than two line breaks (i.e., one blank line) + let no_line_breaks = RE_MULT_LINE_BREAKS.replace_all(&stripped_tags, "\n\n"); + + Some(no_line_breaks.to_string()) + }; + + let details = Details { + pod_title: pod_title, + ep_title: ep_title, + pubdate: ep.pubdate, + duration: Some(ep.format_duration()), + explicit: pod_explicit, + description: desc, + }; + det.details_template(0, details); + }; + + det.refresh(); + } + } + } + /// Creates a pancurses window with a welcome message for when users /// start the program for the first time. Responsibility for managing /// the window is given back to the main UI object. - pub fn make_welcome_win(keymap: &Keybindings, - n_row: i32, n_col:i32) -> Window { + pub fn make_welcome_win(colors: Colors, keymap: &Keybindings, + n_row: i32, n_col:i32) -> Panel { let add_keys = keymap.keys_for_action(UserAction::AddFeed); let quit_keys = keymap.keys_for_action(UserAction::Quit); @@ -698,22 +764,25 @@ impl<'a> UI<'a> { } }; - let welcome_win = newwin(n_row-1, n_col, 0, 0); - welcome_win.border( - pancurses::ACS_VLINE(), - pancurses::ACS_VLINE(), - pancurses::ACS_HLINE(), - pancurses::ACS_HLINE(), - pancurses::ACS_ULCORNER(), - pancurses::ACS_URCORNER(), - pancurses::ACS_LLCORNER(), - pancurses::ACS_LRCORNER()); - welcome_win.mvaddstr(0, 2, "Shellcaster"); - welcome_win.mvaddstr(2, 2, "Welcome to shellcaster!"); - welcome_win.mvaddstr(4, 2, format!("Your podcast list is currently empty. Press {} to add a new podcast feed, or {} to quit.", add_str, quit_str)); - welcome_win.mvaddstr(6, 2, "Other keybindings can be found on the Github repo readme:"); - welcome_win.mvaddstr(7, 2, "https://github.com/jeff-hughes/shellcaster"); - welcome_win.refresh(); + // the warning on the unused mut is a function of Rust getting + // confused between panel.rs and mock_panel.rs + #[allow(unused_mut)] + let mut welcome_win = Panel::new( + colors, + "Shellcaster".to_string(), + 0, + n_row, n_col, 0, 0 + ); + + let mut row = 0; + row = welcome_win.write_wrap_line(row+1, "Welcome to shellcaster!".to_string()); + + row = welcome_win.write_wrap_line(row+2, + format!("Your podcast list is currently empty. Press {} to add a new podcast feed, or {} to quit.", add_str, quit_str)); + + row = welcome_win.write_wrap_line(row+2, "Other keybindings can be found on the Github repo readme:".to_string()); + let _ = welcome_win.write_wrap_line(row+1, "https://github.com/jeff-hughes/shellcaster".to_string()); + return welcome_win; } } \ No newline at end of file diff --git a/src/ui/notification.rs b/src/ui/notification.rs new file mode 100644 index 0000000..20f1c92 --- /dev/null +++ b/src/ui/notification.rs @@ -0,0 +1,259 @@ +use std::time::{Instant, Duration}; + +use super::colors::{Colors, ColorType}; +use pancurses::{Window, Input}; + +/// Holds details of a notification message. +#[derive(Debug, Clone, PartialEq)] +struct Notification { + message: String, + error: bool, + expiry: Option, +} + +impl Notification { + /// Creates a new Notification. The `expiry` is optional, and is + /// used to create timed notifications -- `Instant` should refer + /// to the timestamp when the message should disappear. + pub fn new(message: String, error: bool, expiry: Option) -> Self { + return Self { + message: message, + error: error, + expiry: expiry + }; + } +} + +/// A struct handling the one-line message window at the bottom of the +/// screen. Holds state about the size of the window as well as any +/// persistent message text. +/// +/// The `msg_stack` holds a vector of all timed notifications, each +/// pushed on the end of the stack. The last notification on the stack +/// will be the one displayed; however, they will be removed from the +/// stack based on their expiry times. As such, it will generally be a +/// FIFO approach (older notifications will generally expire first), but +/// not necessarily. +#[derive(Debug)] +pub struct NotifWin { + window: Window, + colors: Colors, + total_rows: i32, + total_cols: i32, + msg_stack: Vec, + persistent_msg: Option, + current_msg: Option, +} + +impl NotifWin { + /// Creates a new NotifWin. + pub fn new(colors: Colors, total_rows: i32, total_cols: i32) -> Self { + let win = pancurses::newwin( + 1, + total_cols, + total_rows-1, + 0); + return Self { + window: win, + colors: colors, + total_rows: total_rows, + total_cols: total_cols, + msg_stack: Vec::new(), + persistent_msg: None, + current_msg: None, + }; + } + + /// Checks if the current notification needs to be changed, and + /// updates the message window accordingly. + pub fn check_notifs(&mut self) { + if !self.msg_stack.is_empty() { + // compare expiry times of all notifications to current time, + // remove expired ones + let now = Instant::now(); + self.msg_stack.retain(|x| now < x.expiry.unwrap()); + + if !self.msg_stack.is_empty() { + // check if last item changed, and update screen if it has + let last_item = self.msg_stack[self.msg_stack.len() - 1].clone(); + match &self.current_msg { + Some(curr) => { + if &last_item != curr { + self.display_notif(last_item.clone()); + } + }, + None => self.display_notif(last_item.clone()), + }; + self.current_msg = Some(last_item); + } else if let Some(msg) = &self.persistent_msg { + // if no other timed notifications exist, display a + // persistent notification if there is one + match &self.current_msg { + Some(curr) => { + if msg != curr { + self.display_notif(msg.clone()); + } + }, + None => self.display_notif(msg.clone()), + }; + self.current_msg = Some(msg.clone()); + } else { + // otherwise, there was a notification before but there + // isn't now, so erase + self.window.erase(); + self.window.refresh(); + self.current_msg = None; + } + } + } + + /// Adds a notification that solicits user text input. A prefix can + /// be specified as a prompt for the user at the beginning of the + /// input line. This returns the user's input; if the user cancels + /// their input, the String will be empty. + pub fn input_notif(&self, prefix: &str) -> String { + self.window.mv(self.total_rows-1, 0); + self.window.addstr(&prefix); + self.window.keypad(true); + self.window.refresh(); + pancurses::curs_set(2); + + let mut inputs = String::new(); + let mut cancelled = false; + + let min_x = prefix.len() as i32; + let mut current_x = prefix.len() as i32; + let mut cursor_x = prefix.len() as i32; + loop { + match self.window.getch() { + // Cancel input + Some(Input::KeyExit) | + Some(Input::Character('\u{1b}')) => { + cancelled = true; + break; + }, + // Complete input + Some(Input::KeyEnter) | + Some(Input::Character('\n')) => { + break; + }, + Some(Input::KeyBackspace) | + Some(Input::Character('\u{7f}')) => { + if current_x > min_x { + current_x -= 1; + cursor_x -= 1; + let _ = inputs.remove((cursor_x as usize) - prefix.len()); + self.window.mv(0, cursor_x); + self.window.delch(); + } + }, + Some(Input::KeyDC) => { + if cursor_x < current_x { + let _ = inputs.remove((cursor_x as usize) - prefix.len()); + self.window.delch(); + } + }, + Some(Input::KeyLeft) => { + if cursor_x > min_x { + cursor_x -= 1; + self.window.mv(0, cursor_x); + } + }, + Some(Input::KeyRight) => { + if cursor_x < current_x { + cursor_x += 1; + self.window.mv(0, cursor_x); + } + }, + Some(Input::Character(c)) => { + current_x += 1; + cursor_x += 1; + self.window.insch(c); + self.window.mv(0, cursor_x); + inputs.push(c); + }, + Some(_) => (), + None => (), + } + self.window.refresh(); + } + + pancurses::curs_set(0); + self.window.deleteln(); + self.window.refresh(); + + if cancelled { + return String::from(""); + } + return inputs; + } + + /// Prints a notification to the window. + fn display_notif(&self, notif: Notification) { + self.window.erase(); + self.window.mv(self.total_rows - 1, 0); + self.window.attrset(pancurses::A_NORMAL); + self.window.addstr(notif.message); + + if notif.error { + self.window.mvchgat(0, 0, -1, pancurses::A_BOLD, + self.colors.get(ColorType::Error)); + } + self.window.refresh(); + } + + /// Adds a notification to the user. `duration` indicates how long + /// (in milliseconds) this message will remain on screen. Useful for + /// presenting error messages, among other things. + pub fn timed_notif(&mut self, message: String, duration: u64, error: bool) { + let expiry = Instant::now() + Duration::from_millis(duration); + self.msg_stack.push(Notification::new(message, error, Some(expiry))); + } + + /// Adds a notification that will stay on screen indefinitely. Must + /// use `clear_persistent_notif()` to erase. If a persistent + /// notification is already being displayed, this method will + /// overwrite that message. + pub fn persistent_notif(&mut self, message: String, error: bool) { + let notif = Notification::new(message, error, None); + self.persistent_msg = Some(notif.clone()); + if self.msg_stack.is_empty() { + self.display_notif(notif.clone()); + self.current_msg = Some(notif); + } + } + + /// Clears any persistent notification that is being displayed. Does + /// not affect timed notifications, user input notifications, etc. + pub fn clear_persistent_notif(&mut self) { + self.persistent_msg = None; + if self.msg_stack.is_empty() { + self.window.erase(); + self.window.refresh(); + self.current_msg = None; + } + } + + /// Updates window size/location + pub fn resize(&mut self, total_rows: i32, total_cols: i32) { + self.total_rows = total_rows; + self.total_cols = total_cols; + + // apparently pancurses does not implement `wresize()` + // from ncurses, so instead we create an entirely new + // window every time the terminal is resized...not ideal, + // but c'est la vie + let oldwin = std::mem::replace( + &mut self.window, + pancurses::newwin( + 1, + total_cols, + total_rows-1, + 0)); + oldwin.delwin(); + + if let Some(curr) = &self.current_msg { + self.display_notif(curr.clone()); + } + } +} \ No newline at end of file diff --git a/src/ui/panel.rs b/src/ui/panel.rs new file mode 100644 index 0000000..f1587d4 --- /dev/null +++ b/src/ui/panel.rs @@ -0,0 +1,253 @@ +use pancurses::{Window, Attribute}; +use chrono::{DateTime, Utc}; + +use super::{Colors, ColorType}; + +/// Struct holding the raw data used for building the details panel. +pub struct Details { + pub pod_title: Option, + pub ep_title: Option, + pub pubdate: Option>, + pub duration: Option, + pub explicit: Option, + pub description: Option, +} + +/// Panels abstract away a pancurses window, and handles all methods +/// associated with writing data to that window. A panel includes a +/// border and margin around the edge of the window, and a title that +/// appears at the top. The Panel will translate the x and y coordinates +/// to account for the border and margins, so users of the methods can +/// calculate rows and columns relative to the Panel. +#[derive(Debug)] +pub struct Panel { + window: Window, + screen_pos: usize, + colors: Colors, + title: String, + n_row: i32, + n_col: i32, +} + +impl Panel { + /// Creates a new panel. + pub fn new(colors: Colors, + title: String, screen_pos: usize, n_row: i32, n_col: i32, start_y: i32, start_x: i32) -> Self { + + let panel_win = pancurses::newwin( + n_row, + n_col, + start_y, + start_x); + + return Panel { + window: panel_win, + screen_pos: screen_pos, + colors: colors, + title: title, + n_row: n_row, + n_col: n_col, + }; + } + + /// Initiates the menu -- primarily, draws borders on the window. + pub fn init(&self) { + self.draw_border(); + } + + /// Redraws borders and refreshes the window to display on terminal. + pub fn refresh(&self) { + self.draw_border(); + self.window.refresh(); + } + + /// Draws a border around the window. + fn draw_border(&self) { + let top_left; + let bot_left; + match self.screen_pos { + 0 => { + top_left = pancurses::ACS_ULCORNER(); + bot_left = pancurses::ACS_LLCORNER(); + } + _ => { + top_left = pancurses::ACS_TTEE(); + bot_left = pancurses::ACS_BTEE(); + } + } + self.window.border( + pancurses::ACS_VLINE(), + pancurses::ACS_VLINE(), + pancurses::ACS_HLINE(), + pancurses::ACS_HLINE(), + top_left, + pancurses::ACS_URCORNER(), + bot_left, + pancurses::ACS_LRCORNER()); + + self.window.mvaddstr(0, 2, self.title.clone()); + } + + /// Erases all content on the window, and redraws the border. Does + /// not refresh the screen. + pub fn erase(&self) { + self.window.erase(); + self.draw_border(); + } + + /// Writes a line of text to the window. Note that this does not do + /// checking for line length, so strings that are too long will end + /// up wrapping and may mess up the format. use `write_wrap_line()` + /// if you need line wrapping. + pub fn write_line(&self, y: i32, string: String) { + self.window.mvaddstr(self.abs_y(y), self.abs_x(0), string); + } + + /// Writes a line of text to the window, first moving all text on + /// line `y` and below down one row. + pub fn insert_line(&self, y: i32, string: String) { + self.window.mv(self.abs_y(y), 0); + self.window.insertln(); + self.window.mv(self.abs_y(y), self.abs_x(0)); + self.window.addstr(string); + } + + /// Deletes a line of text from the window. + pub fn delete_line(&self, y: i32) { + self.window.mv(self.abs_y(y), self.abs_x(-1)); + self.window.deleteln(); + } + + /// Writes one or more lines of text from a String, word wrapping + /// when necessary. `start_y` refers to the row to start at (word + /// wrapping makes it unknown where text will end). Returns the row + /// on which the text ended. + pub fn write_wrap_line(&self, start_y: i32, string: String) -> i32 { + let mut row = start_y; + let max_row = self.get_rows(); + let wrapper = textwrap::wrap_iter(&string, self.get_cols() as usize); + for line in wrapper { + self.window.mvaddstr(self.abs_y(row), self.abs_x(0), line.clone()); + row += 1; + + if row >= max_row { + break; + } + } + return row-1; + } + + /// Write the specific template used for the details panel. This is + /// not the most elegant code, but it works. + pub fn details_template(&self, start_y: i32, details: Details) { + let mut row = start_y-1; + + self.window.attron(Attribute::Bold); + // podcast title + match details.pod_title { + Some(t) => row = self.write_wrap_line(row+1, t), + None => row = self.write_wrap_line(row+1, "No title".to_string()), + } + + // episode title + match details.ep_title { + Some(t) => row = self.write_wrap_line(row+1, t), + None => row = self.write_wrap_line(row+1, "No title".to_string()), + } + self.window.attroff(Attribute::Bold); + + row += 1; // blank line + + // published date + if let Some(date) = details.pubdate { + let new_row = self.write_wrap_line(row+1, + format!("Published: {}", date.format("%B %-d, %Y").to_string())); + self.change_attr(row+1, 0, 10, + pancurses::A_UNDERLINE, ColorType::Normal); + row = new_row; + } + + // duration + if let Some(dur) = details.duration { + let new_row = self.write_wrap_line(row+1, + format!("Duration: {}", dur)); + self.change_attr(row+1, 0, 9, + pancurses::A_UNDERLINE, ColorType::Normal); + row = new_row; + } + + // explicit + if let Some(exp) = details.explicit { + let new_row = if exp { + self.write_wrap_line(row+1, "Explicit: Yes".to_string()) + } else { + self.write_wrap_line(row+1, "Explicit: No".to_string()) + }; + self.change_attr(row+1, 0, 9, + pancurses::A_UNDERLINE, ColorType::Normal); + row = new_row; + } + + row += 1; // blank line + + // description + match details.description { + Some(desc) => { + self.window.attron(Attribute::Bold); + row = self.write_wrap_line(row+1, "Description:".to_string()); + self.window.attroff(Attribute::Bold); + let _row = self.write_wrap_line(row+1, desc); + }, + None => { + let _row = self.write_wrap_line(row+1, "No description.".to_string()); + }, + } + } + + /// Changes the attributes (text style and color) for a line of + /// text. + pub fn change_attr(&self, y: i32, x: i32, nchars: i32, attr: pancurses::chtype, color: ColorType) { + self.window.mvchgat(self.abs_y(y), self.abs_x(x), nchars, + attr, self.colors.get(color)); + } + + /// Updates window size + pub fn resize(&mut self, n_row: i32, n_col: i32, start_y: i32, start_x: i32) { + self.n_row = n_row; + self.n_col = n_col; + + // apparently pancurses does not implement `wresize()` + // from ncurses, so instead we create an entirely new + // window every time the terminal is resized...not ideal, + // but c'est la vie + let oldwin = std::mem::replace( + &mut self.window, + pancurses::newwin(n_row, n_col, start_y, start_x)); + oldwin.delwin(); + } + + /// Returns the effective number of rows (accounting for borders + /// and margins). + pub fn get_rows(&self) -> i32 { + return self.n_row - 2; // border on top and bottom + } + + /// Returns the effective number of columns (accounting for + /// borders and margins). + pub fn get_cols(&self) -> i32 { + return self.n_col - 5; // 2 for border, 2 for margins, and 1 + // extra for some reason... + } + + /// Calculates the y-value relative to the window rather than to the + /// panel (i.e., taking into account borders and margins). + fn abs_y(&self, y: i32) -> i32 { + return y + 1; + } + + /// Calculates the x-value relative to the window rather than to the + /// panel (i.e., taking into account borders and margins). + fn abs_x(&self, x: i32) -> i32 { + return x + 2; + } +} \ No newline at end of file