├── .cargo └── config.toml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── dependabot.yml ├── labeler.yml ├── pull_request_template.md └── workflows │ ├── audit.yml │ ├── ci.yml │ ├── devskim.yml │ ├── greetings.yml │ ├── labeler.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── FAQ.md ├── LICENSE ├── README.md ├── SECURITY.md ├── _config.yml ├── assets ├── fonts │ └── RobotoMonoNerdFont-Regular.ttf └── images │ ├── dmg-background.png │ ├── logo.icns │ ├── logo.ico │ └── logo.png ├── build.rs ├── deny.toml ├── docs ├── README.md ├── TEMPLATE.md └── template │ ├── EXTRAS.md │ ├── FEATURES.md │ ├── MANUAL.md │ ├── REQ.md │ ├── START.md │ ├── UPDATE_DESC.md │ ├── UPDATE_LOGO.md │ └── WORKFLOW.md ├── flake.lock ├── flake.nix ├── fuzz ├── .gitignore ├── Cargo.toml ├── README.md └── fuzz_targets │ └── fuzz_ddg_get_links.rs ├── resources ├── USEFUL_SITES.md ├── linux │ ├── AUR │ │ ├── bin │ │ │ └── PKGBUILD │ │ ├── git │ │ │ └── PKGBUILD │ │ └── stable │ │ │ └── PKGBUILD │ ├── desktop │ │ ├── falion.desktop │ │ └── icons │ │ │ └── hicolor │ │ │ ├── 128x128 │ │ │ └── apps │ │ │ │ ├── .gitkeep │ │ │ │ └── falion.png │ │ │ ├── 16x16 │ │ │ └── apps │ │ │ │ ├── .gitkeep │ │ │ │ └── falion.png │ │ │ ├── 256x256 │ │ │ └── apps │ │ │ │ ├── .gitkeep │ │ │ │ └── falion.png │ │ │ ├── 32x32 │ │ │ └── apps │ │ │ │ ├── .gitkeep │ │ │ │ └── falion.png │ │ │ ├── 512x512 │ │ │ └── apps │ │ │ │ ├── .gitkeep │ │ │ │ └── falion.png │ │ │ └── 64x64 │ │ │ └── apps │ │ │ ├── .gitkeep │ │ │ └── falion.png │ ├── falion.AppDir │ │ ├── AppRun │ │ ├── falion.desktop │ │ ├── falion.png │ │ └── usr │ │ │ ├── bin │ │ │ └── .gitkeep │ │ │ └── share │ │ │ └── icons │ │ │ └── hicolor │ │ │ ├── 128x128 │ │ │ └── apps │ │ │ │ ├── .gitkeep │ │ │ │ └── falion.png │ │ │ ├── 16x16 │ │ │ └── apps │ │ │ │ ├── .gitkeep │ │ │ │ └── falion.png │ │ │ ├── 256x256 │ │ │ └── apps │ │ │ │ ├── .gitkeep │ │ │ │ └── falion.png │ │ │ ├── 32x32 │ │ │ └── apps │ │ │ │ ├── .gitkeep │ │ │ │ └── falion.png │ │ │ ├── 512x512 │ │ │ └── apps │ │ │ │ ├── .gitkeep │ │ │ │ └── falion.png │ │ │ └── 64x64 │ │ │ └── apps │ │ │ ├── .gitkeep │ │ │ └── falion.png │ └── falion.nix ├── macos │ ├── falion.app │ │ └── Contents │ │ │ ├── Info.plist │ │ │ ├── MacOS │ │ │ └── .gitkeep │ │ │ ├── PkgInfo │ │ │ └── Resources │ │ │ ├── .gitkeep │ │ │ └── AppIcon.icns │ ├── falion.rb │ └── make-dmg.sh └── windows │ ├── License.rtf │ ├── main.wxs │ └── resources.rc ├── rustfmt.toml ├── scripts ├── data │ ├── DESC │ ├── PMAIL │ └── SMAIL ├── lib │ ├── init_repo.py │ ├── update_desc.py │ └── update_logo.py └── repo.py ├── shell.nix ├── src ├── cli │ ├── content.rs │ ├── mod.rs │ ├── print.rs │ └── util.rs ├── lib.rs ├── main.rs ├── search │ ├── ddg.rs │ ├── ddg_search.rs │ ├── geeksforgeeks.rs │ ├── github_gist.rs │ ├── mod.rs │ ├── stackexchange.rs │ ├── stackoverflow.rs │ └── util.rs ├── ui │ ├── dyn_content │ │ ├── button.rs │ │ └── mod.rs │ ├── mod.rs │ ├── results │ │ ├── display.rs │ │ ├── helper.rs │ │ ├── index.rs │ │ └── mod.rs │ ├── static_content.rs │ └── util.rs └── util.rs └── ui ├── dyn_content.slint ├── main.slint ├── results.slint └── static_content.slint /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | # which theme to build the application with 3 | SLINT_STYLE = "fluent-dark" 4 | # Disable qt-backend, doesn't work with the way I built it 5 | SLINT_NO_QT = "1" 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ko_fi: Obscurely 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug Report 3 | title: "[bug] " 4 | description: Report a bug 5 | labels: ["type: bug", "status: needs triage"] 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## First of all 11 | 1. Please search for [existing issues](https://github.com/Obscurely/falion/issues?q=is%3Aissue) about this problem first. 12 | 2. Make sure `rustc` and all relevant falion packages are up to date. 13 | 3. Make sure it's an issue with falion and not something else you are using. 14 | 4. Remember to follow the community guidelines and be friendly. 15 | - type: textarea 16 | id: description 17 | attributes: 18 | label: Describe the bug 19 | description: A clear description of what the bug is. Include screenshots if applicable. 20 | placeholder: Bug description 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: reproduction 25 | attributes: 26 | label: Reproduction 27 | description: Steps to reproduce the behaviour. 28 | placeholder: | 29 | 1. Go to '...' 30 | 2. Click on '....' 31 | 3. Scroll down to '....' 32 | 4. See error 33 | - type: textarea 34 | id: expected-behavior 35 | attributes: 36 | label: Expected behavior 37 | description: A clear and concise description of what you expected to happen. 38 | - type: textarea 39 | id: info 40 | attributes: 41 | label: Platform and versions 42 | description: Platform, OS & Version used. 43 | validations: 44 | required: true 45 | - type: textarea 46 | id: log 47 | attributes: 48 | label: Log file 49 | description: | 50 | The log file from the session on which the issue happened. Available at: 51 | On windows: C:\users\yourname\AppData\Temp\falion\the_file.log 52 | On macOS: $HOME/Library/Caches/falion/the_file.log 53 | On Linux: $HOME/.cache/falion/the_file.log 54 | validations: 55 | required: true 56 | - type: textarea 57 | id: context 58 | attributes: 59 | label: Additional context 60 | description: Add any other context about the problem here. 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | contact_links: 3 | - name: 👨‍👨‍👦‍👦 Ask a question on github 4 | url: https://github.com/Obscurely/falion/discussions/ 5 | about: Ask questions and discuss with other community members. 6 | - name: 💬 Discord Chat 7 | url: https://discord.gg/ykp5kb46TY 8 | about: > 9 | Ask questions and talk to other falion users and the maintainers. 10 | 11 | - name: 📧 Email 12 | url: https://mailxto.com/98oqvzjr7k 13 | about: Send me an email if you wanna say more or talk privately. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | title: "[feat] " 4 | description: Suggest an idea for this project 5 | labels: "type: enhancement" 6 | body: 7 | - type: textarea 8 | id: problem 9 | attributes: 10 | label: Describe the problem 11 | description: > 12 | A clear and concise description of the problem this feature would solve 13 | 14 | placeholder: "I'm always frustrated when..." 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: solution 19 | attributes: 20 | label: "Describe the solution you'd like" 21 | description: A clear and concise description of what you want to happen. 22 | placeholder: "I would like to..." 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: alternatives 27 | attributes: 28 | label: Alternatives considered 29 | description: > 30 | A clear and concise description of any alternative solutions or features you've considered. 31 | 32 | - type: textarea 33 | id: context 34 | attributes: 35 | label: Additional context 36 | description: > 37 | Add any other context or screenshots about the feature request here. 38 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "cargo" 5 | # Look for `Cargo.toml` and `Cargo.lock` in the root directory 6 | directory: "/" 7 | # Check for updates every Monday 8 | schedule: 9 | interval: "weekly" 10 | open-pull-requests-limit: 10 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | # Check for updates every Monday 14 | schedule: 15 | interval: "weekly" 16 | open-pull-requests-limit: 10 17 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | assets: 3 | - any: ["assets/**"] 4 | repo: 5 | - "*" 6 | resources: 7 | - any: ["resources/**"] 8 | source: 9 | - any: ["src/**"] 10 | docs: 11 | - any: ["docs/**"] 12 | fuzzer: 13 | - any: ["fuzz/**"] 14 | scripts: 15 | - any: ["scripts/**"] 16 | tests: 17 | - any: ["tests/**"] 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request Template 2 | 3 | 16 | 17 | ## Fixes Issue 18 | 19 | 20 | 21 | 22 | 23 | ## Changes proposed 24 | 25 | 26 | 27 | 29 | 35 | 36 | ## Check List (Check all the applicable boxes) 37 | 38 | 39 | 40 | - [ ] My code follows the code style of this project. 41 | - [ ] My change requires changes to the documentation. 42 | - [ ] I have updated the documentation accordingly. 43 | - [ ] All new and existing tests passed. 44 | - [ ] This PR does not contain plagiarized content. 45 | - [ ] The title of my pull request is a short description of the requested 46 | changes. 47 | 48 | ## Screenshots 49 | 50 | 51 | 52 | ## Note to reviewers 53 | 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Security audit 3 | on: 4 | schedule: 5 | # Runs at 00:00 UTC everyday 6 | - cron: "0 0 * * *" 7 | push: 8 | paths: 9 | - "**/Cargo.toml" 10 | pull_request: 11 | paths: 12 | - "**/Cargo.toml" 13 | jobs: 14 | audit: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | # Ensure that the latest version of Cargo is installed 20 | - name: Install Rust toolchain 21 | uses: dtolnay/rust-toolchain@stable 22 | - uses: Swatinem/rust-cache@v2 23 | - uses: actions-rs/audit-check@v1 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rust 3 | on: 4 | push: 5 | branches: ["**"] 6 | pull_request: 7 | branches: ["**"] 8 | env: 9 | CARGO_TERM_COLOR: always 10 | jobs: 11 | cargo-deny: 12 | name: Cargo Deny 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: EmbarkStudios/cargo-deny-action@v1 17 | test: 18 | name: Test Suite 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | - name: Install Rust toolchain 24 | uses: dtolnay/rust-toolchain@stable 25 | - uses: Swatinem/rust-cache@v2 26 | - name: Run tests 27 | run: cargo test --all-features --workspace 28 | rustfmt: 29 | name: Rustfmt 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | - name: Install Rust toolchain 35 | uses: dtolnay/rust-toolchain@stable 36 | with: 37 | components: rustfmt 38 | - uses: Swatinem/rust-cache@v2 39 | - name: Check formatting 40 | run: cargo fmt --all --check 41 | clippy: 42 | name: Clippy 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Checkout repository 46 | uses: actions/checkout@v4 47 | - name: Install Rust toolchain 48 | uses: dtolnay/rust-toolchain@stable 49 | with: 50 | components: clippy 51 | - uses: Swatinem/rust-cache@v2 52 | - name: Clippy check 53 | run: | 54 | cargo clippy --all-targets --all-features --workspace 55 | run-lint: 56 | name: Super Linter 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Checkout code 60 | uses: actions/checkout@v4 61 | with: 62 | # Full git history is needed to get a proper list of changed files 63 | fetch-depth: 0 64 | - name: Lint Code Base 65 | uses: github/super-linter@v5 66 | env: 67 | VALIDATE_ALL_CODEBASE: false 68 | VALIDATE_RUST_2021: false 69 | VALIDATE_RUST_2018: false 70 | VALIDATE_RUST_2015: false 71 | VALIDATE_RUST_CLIPPY: false 72 | VALIDATE_JSCPD: false 73 | VALIDATE_RUBY: false 74 | FILTER_REGEX_EXCLUDE: "README.md" 75 | DEFAULT_BRANCH: "master" 76 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 77 | docs: 78 | name: Docs 79 | runs-on: ubuntu-latest 80 | steps: 81 | - name: Checkout repository 82 | uses: actions/checkout@v4 83 | - name: Install Rust toolchain 84 | uses: dtolnay/rust-toolchain@stable 85 | - uses: Swatinem/rust-cache@v2 86 | - name: Check documentation 87 | run: > 88 | cargo doc --no-deps --document-private-items --all-features --workspace --examples 89 | -------------------------------------------------------------------------------- /.github/workflows/devskim.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: DevSkim 3 | on: 4 | push: 5 | branches: ["**"] 6 | pull_request: 7 | branches: ["**"] 8 | schedule: 9 | - cron: "0 0 * * *" 10 | jobs: 11 | lint: 12 | name: DevSkim 13 | runs-on: ubuntu-latest 14 | permissions: 15 | actions: read 16 | contents: read 17 | security-events: write 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | - name: Run DevSkim scanner 22 | uses: microsoft/DevSkim-Action@v1 23 | - name: Upload DevSkim scan results to GitHub Security tab 24 | uses: github/codeql-action/upload-sarif@v3 25 | with: 26 | sarif_file: devskim-results.sarif 27 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Greetings 3 | on: [pull_request_target, issues] 4 | jobs: 5 | greeting: 6 | runs-on: ubuntu-latest 7 | permissions: 8 | issues: write 9 | pull-requests: write 10 | steps: 11 | - uses: actions/first-interaction@v1 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | issue-message: "Hi, I am really greatful you want to contribute tomy project. I will check this as soon as I can." 15 | pr-message: "Hi, thank you so much for contributing to my project,it means a lot. I will check this as soon as I can." 16 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Labeler 3 | on: [pull_request_target] 4 | jobs: 5 | label: 6 | runs-on: ubuntu-latest 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | steps: 11 | - uses: actions/labeler@v4 12 | with: 13 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | debug/ 3 | target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | 8 | # These are backup files generated by rustfmt 9 | **/*.rs.bk 10 | 11 | # MSVC Windows builds of rustc generate these, which store debugging information 12 | *.pdb 13 | 14 | # Additional system files 15 | .DS_Store 16 | .cargo-ok 17 | 18 | # VSCode configuration 19 | .vscode/ 20 | 21 | # Sublime Text configuration 22 | *.sublime-* 23 | 24 | # IntelliJ IDE configuration 25 | .idea/ 26 | /*.iml 27 | 28 | # Vim swap files 29 | *.swp 30 | 31 | # Folder config file 32 | [Dd]esktop.ini 33 | 34 | # Where I keep the video showcase project and files for syncthing to push them to my home server 35 | showcase/ 36 | 37 | # Python 38 | .mypy_cache/ 39 | scripts/.mypy_cache/ 40 | scripts/__pycache__/ 41 | scripts/lib/.mypy_cache/ 42 | scripts/lib/__pycache__/ 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | --- 4 | 5 | ## Master 6 | 7 | ### Breaking Changes 8 | 9 | None 10 | 11 | ### Added 12 | 13 | None 14 | 15 | ### Changed 16 | 17 | None 18 | 19 | ### Deprecated 20 | 21 | None 22 | 23 | ### Removed 24 | 25 | None 26 | 27 | ### Fixed 28 | 29 | None 30 | 31 | ### Security 32 | 33 | None 34 | 35 | ## v1.0.2 36 | 37 | ### Changed v1.0.2 38 | 39 | - Bump chrono to 0.4.38 40 | - Bump clap to 4.5.4 41 | - Bump hashbrown to 0.14.5 42 | - Bump rayon to 0.10.0 43 | - Bump reqwest to 0.12.4 44 | - Bump slint to 1.5.1 45 | - Bump this error to 1.0.56 46 | - Bump tokio to 1.37.0 47 | - As reult CWE-400 and CWE-770 were fixed. 48 | 49 | ## v1.0.1 50 | 51 | ### Fixed v1.0.1 52 | 53 | - Fix builds on some platforms not being done without qt. 54 | - Fix arch pkg thanks to setting the backend in ./cargo/config.toml 55 | - Force the theme to be dark. In the future it will adapt to the system and look 56 | prettier for now this is it 57 | - Fix homebrew pkg 58 | - Fix windows installer not creating desktop or start menu shortcuts 59 | - Fix appimage not taking CLI args and consequently fixing the nix file 60 | 61 | ### Security v1.0.1 62 | 63 | - Fix multiple security issues from creates 64 | 65 | ## v1.0.0-stable 66 | 67 | ### Highlights v1.0.0 68 | 69 | - Whole project rewritten, now with more than year more of experience, learning 70 | and building with rust. The cli part of the project provides almost the same 71 | experience, except for the keybinds which are more VIM like now. 72 | - Heavy, heavy performance improvements across the board. 73 | - Added a GUI that is automatically launched when not running from the cli. 74 | - Program is actually fully cross-platform now. 75 | - Many more checks in place preventing bad results, errors and other ux 76 | problems. Also results are much better now. 77 | - Client is configured to mimic a browser and a limit of five results per 78 | resource is set in order to prevent any sort of rate limiting in normal use. 79 | - Added unit tests, fuzzers and tested the program under many conditions to make 80 | sure it works as intended. 81 | - Bumped crates and rust version to the latest. 82 | - Fixed security issues. 83 | - Better repository structure, thanks to 84 | [my rust template](https://github.com/Obscurely/RustTemplate). 85 | - Code is much more maintainable with added code documentation, more idiomatic 86 | design and logging for easier debugging. 87 | 88 | ### Breaking Changes v1.0.0 89 | 90 | - End user experience is as close as to the original as possible, but the whole 91 | application has been rewritten entirely. 92 | - The whole structure of this repository has changed since I rewritten it using 93 | [my rust template](https://github.com/Obscurely/RustTemplate). 94 | 95 | ### Added v1.0.0 96 | 97 | - Checks in duckduckgo search with more meaningful errors making it way more 98 | robust. 99 | - Random generated user-agent for reqwest to avoid getting blocked by 100 | duckduckgo. 101 | - All of the features present on the 102 | [features list](https://obscurely.github.io/RustTemplate/template/FEATURES.html) 103 | of [my rust template](https://github.com/Obscurely/RustTemplate). 104 | - Added cargo fuzz harness for the duckduckgo get_links function in order to 105 | make sure it will not fail making requests over time. 106 | - General performance improvements. 107 | - Added logging across the whole application. Not too much, just enough to be 108 | able to debug eventual errors. I tried to keep it simple. 109 | - Added code documenation. 110 | - Added tests. 111 | - Added a GUI made with slint that is used when not running the program from the 112 | cli. 113 | 114 | ### Changed v1.0.0 115 | 116 | - Bumped rust version to 1.76.0 117 | - Bumped all the crates to the latest version. 118 | - Instead of the many different functions that handled the duckduckgo searches 119 | now there is one with half the code, way faster and more robust. 120 | - There is one global client that is used across all objects and threads .This 121 | makes the program way faster than before. 122 | - Regular expressions are not used anymore since all that look back crap is 123 | slow. Switched to plain splitting the content. This improves the performance 124 | by quite a bit. 125 | - Rewrote ddg.rs completely. Performance improvements and way better results. 126 | - Rewrote stackoverflow.rs completely. Many performance improvements, using only 127 | one global client, many checks in place. 128 | - Rewrote stackexchange.rs completely. Same as Stack Overflow, many performance 129 | improvements, one global client and multiple checks in place. 130 | - Rewrote geeksforgeeks.rs completely. Same as Stack Overflow, performance 131 | improvements, one global client, multiple checks in place and the page is 132 | rendered better. 133 | - Rewrote ddg_search.rs completely. Same as Stack Overflow, performance 134 | improvements, one global client, multiple checks in place and the pages are 135 | rendered better. 136 | - Rewrote github_gist.rs completely. Same as Stack Overflow, performance 137 | improvements, one global client, multiple checks in place + actual parallel 138 | requesting the gist files instead of concurent and simplified process. 139 | - Better error handling. 140 | - Better argument parsing using clap instead of arg_parse. 141 | - Rewrote the cli. More efficient, less error-prone and just better in general. 142 | Note: it works almost the same, this is just the backend. 143 | - Made code way more maintainable. 144 | - Actual cli code is in it's own module. 145 | - Replaced IndexMap everywhere with either a vector of tuples or dashmap in ui 146 | and hashbrown in cli for improved performance. 147 | 148 | ### Fixed v1.0.0 149 | 150 | - Fix duckduckgo results, now they are actually good. 151 | - Compiling to windows & macOS doesn't fail now. 152 | - GeeksForGeeks pages don't contain the extra crap now. 153 | 154 | ### Security v1.0.0 155 | 156 | - Fixed a bunch of security issues that appeared over time in the last version. 157 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | 4 | 5 | - [Contributor Covenant Code of Conduct](#contributor-covenant-code-of-conduct) 6 | - [Our Pledge](#our-pledge) 7 | - [Our Standards](#our-standards) 8 | - [Enforcement Responsibilities](#enforcement-responsibilities) 9 | - [Scope](#scope) 10 | - [Enforcement](#enforcement) 11 | - [Enforcement Guidelines](#enforcement-guidelines) 12 | - [1. Correction](#1-correction) 13 | - [2. Warning](#2-warning) 14 | - [3. Temporary Ban](#3-temporary-ban) 15 | - [4. Permanent Ban](#4-permanent-ban) 16 | - [Attribution](#attribution) 17 | 18 | 19 | 20 | ## Our Pledge 21 | 22 | We as members, contributors, and leaders pledge to make participation in our 23 | community a harassment-free experience for everyone, regardless of age, body 24 | size, visible or invisible disability, ethnicity, sex characteristics, gender 25 | identity and expression, level of experience, education, socio-economic status, 26 | nationality, personal appearance, race, religion, or sexual identity and 27 | orientation. 28 | 29 | We pledge to act and interact in ways that contribute to an open, welcoming, 30 | diverse, inclusive, and healthy community. 31 | 32 | ## Our Standards 33 | 34 | Examples of behavior that contributes to a positive environment for our 35 | community include: 36 | 37 | - Demonstrating empathy and kindness toward other people 38 | - Being respectful of differing opinions, viewpoints, and experiences 39 | - Giving and gracefully accepting constructive feedback 40 | - Accepting responsibility and apologizing to those affected by our mistakes, 41 | and learning from the experience 42 | - Focusing on what is best not just for us as individuals, but for the overall 43 | community 44 | 45 | Examples of unacceptable behavior include: 46 | 47 | - The use of sexualized language or imagery, and sexual attention or advances of 48 | any kind 49 | - Trolling, insulting or derogatory comments, and personal or political attacks 50 | - Public or private harassment 51 | - Publishing others' private information, such as a physical or email address, 52 | without their explicit permission 53 | - Other conduct which could reasonably be considered inappropriate in a 54 | professional setting 55 | 56 | ## Enforcement Responsibilities 57 | 58 | Community leaders are responsible for clarifying and enforcing our standards of 59 | acceptable behavior and will take appropriate and fair corrective action in 60 | response to any behavior that they deem inappropriate, threatening, offensive, 61 | or harmful. 62 | 63 | Community leaders have the right and responsibility to remove, edit, or reject 64 | comments, commits, code, wiki edits, issues, and other contributions that are 65 | not aligned to this Code of Conduct, and will communicate reasons for moderation 66 | decisions when appropriate. 67 | 68 | ## Scope 69 | 70 | This Code of Conduct applies within all community spaces, and also applies when 71 | an individual is officially representing the community in public spaces. 72 | Examples of representing our community include using an official email address, 73 | posting via an official social media account, or acting as an appointed 74 | representative at an online or offline event. 75 | 76 | ## Enforcement 77 | 78 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 79 | reported to the community leaders responsible for enforcement at 80 | adrian.obscurely@protonmail.com. All complaints will be reviewed and 81 | investigated promptly and fairly. 82 | 83 | All community leaders are obligated to respect the privacy and security of the 84 | reporter of any incident. 85 | 86 | ## Enforcement Guidelines 87 | 88 | Community leaders will follow these Community Impact Guidelines in determining 89 | the consequences for any action they deem in violation of this Code of Conduct: 90 | 91 | ### 1. Correction 92 | 93 | **Community Impact**: Use of inappropriate language or other behavior deemed 94 | unprofessional or unwelcome in the community. 95 | 96 | **Consequence**: A private, written warning from community leaders, providing 97 | clarity around the nature of the violation and an explanation of why the 98 | behavior was inappropriate. A public apology may be requested. 99 | 100 | ### 2. Warning 101 | 102 | **Community Impact**: A violation through a single incident or series of 103 | actions. 104 | 105 | **Consequence**: A warning with consequences for continued behavior. No 106 | interaction with the people involved, including unsolicited interaction with 107 | those enforcing the Code of Conduct, for a specified period of time. This 108 | includes avoiding interactions in community spaces as well as external channels 109 | like social media. Violating these terms may lead to a temporary or permanent 110 | ban. 111 | 112 | ### 3. Temporary Ban 113 | 114 | **Community Impact**: A serious violation of community standards, including 115 | sustained inappropriate behavior. 116 | 117 | **Consequence**: A temporary ban from any sort of interaction or public 118 | communication with the community for a specified period of time. No public or 119 | private interaction with the people involved, including unsolicited interaction 120 | with those enforcing the Code of Conduct, is allowed during this period. 121 | Violating these terms may lead to a permanent ban. 122 | 123 | ### 4. Permanent Ban 124 | 125 | **Community Impact**: Demonstrating a pattern of violation of community 126 | standards, including sustained inappropriate behavior, harassment of an 127 | individual, or aggression toward or disparagement of classes of individuals. 128 | 129 | **Consequence**: A permanent ban from any sort of public interaction within the 130 | community. 131 | 132 | ## Attribution 133 | 134 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 135 | version 2.0, available at 136 | [code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). 137 | 138 | Community Impact Guidelines were inspired by 139 | [Mozilla's code of conduct 140 | enforcement ladder](https://github.com/mozilla/diversity). 141 | 142 | [homepage]: https://www.contributor-covenant.org 143 | 144 | For answers to common questions about this code of conduct, see the FAQ at 145 | [faq](https://www.contributor-covenant.org/faq). Translations are available at 146 | [translations](https://www.contributor-covenant.org/translations). 147 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 4 | 5 | - [Contributing](#contributing) 6 | - [Pull Request Process](#pull-request-process) 7 | - [Code of Conduct](#code-of-conduct) 8 | - [Our Pledge](#our-pledge) 9 | - [Our Standards](#our-standards) 10 | - [Our Responsibilities](#our-responsibilities) 11 | - [Scope](#scope) 12 | - [Enforcement](#enforcement) 13 | - [Attribution](#attribution) 14 | 15 | 16 | 17 | When contributing to this repository, please first discuss the change you wish 18 | to make via issue, email, or any other method with the owners of this repository 19 | before making a change. 20 | 21 | Please note we have a code of conduct, please follow it in all your interactions 22 | with the project. 23 | 24 | ## Pull Request Process 25 | 26 | 1. Ensure any install or build dependencies are removed before the end of the 27 | layer when doing a build. 28 | 1. Update the README.md with details of changes to the interface, this includes 29 | new environment variables, useful file locations and everything else. 30 | 1. Increase the version numbers in any examples files and the README.md to the 31 | new version that this Pull Request would represent. The versioning scheme we 32 | use is [SemVer](http://semver.org/). 33 | 1. You may merge the Pull Request in once you have the sign-off, or if you do 34 | not have permission to do that, you may request the second reviewer to merge 35 | it for you. 36 | 37 | ## Code of Conduct 38 | 39 | ### Our Pledge 40 | 41 | In the interest of fostering an open and welcoming environment, we as 42 | contributors and maintainers pledge to making participation in our project and 43 | our community a harassment-free experience for everyone, regardless of age, body 44 | size, disability, ethnicity, gender identity and expression, level of 45 | experience, nationality, personal appearance, race, religion, or sexual identity 46 | and orientation. 47 | 48 | ### Our Standards 49 | 50 | Examples of behavior that contributes to creating a positive environment 51 | include: 52 | 53 | - Using welcoming and inclusive language 54 | - Being respectful of differing viewpoints and experiences 55 | - Gracefully accepting constructive criticism 56 | - Focusing on what is best for the community 57 | - Showing empathy towards other community members 58 | 59 | Examples of unacceptable behavior by participants include: 60 | 61 | - The use of sexualized language or imagery and unwelcome sexual attention or 62 | advances 63 | - Trolling, insulting/derogatory comments, and personal or political attacks 64 | - Public or private harassment 65 | - Publishing others' private information, such as a physical or electronic 66 | address, without explicit permission 67 | - Other conduct which could reasonably be considered inappropriate in a 68 | professional setting 69 | 70 | ### Our Responsibilities 71 | 72 | Project maintainers are responsible for clarifying the standards of acceptable 73 | behavior and are expected to take appropriate and fair corrective action in 74 | response to any instances of unacceptable behavior. 75 | 76 | Project maintainers have the right and responsibility to remove, edit, or reject 77 | comments, commits, code, wiki edits, issues, and other contributions that are 78 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 79 | contributor for other behaviors that they deem inappropriate, threatening, 80 | offensive, or harmful. 81 | 82 | ### Scope 83 | 84 | This Code of Conduct applies both within project spaces and in public spaces 85 | when an individual is representing the project or its community. Examples of 86 | representing a project or community include using an official project e-mail 87 | address, posting via an official social media account, or acting as an appointed 88 | representative at an online or offline event. Representation of a project may be 89 | further defined and clarified by project maintainers. 90 | 91 | ### Enforcement 92 | 93 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 94 | reported by contacting the project team at `adrian.obscurely@protonmail.com`. 95 | All complaints will be reviewed and investigated and will result in a response 96 | that is deemed necessary and appropriate to the circumstances. The project team 97 | is obligated to maintain confidentiality with the reporter of an incident. 98 | Further details of specific enforcement policies may be posted separately. 99 | 100 | Project maintainers who do not follow or enforce the Code of Conduct in good 101 | faith may face temporary or permanent repercussions as determined by other 102 | members of the project's leadership. 103 | 104 | ### Attribution 105 | 106 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 107 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 108 | 109 | [homepage]: http://contributor-covenant.org 110 | [version]: http://contributor-covenant.org/version/1/4/ 111 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "falion" 3 | authors = ["Obscurely 4 | 5 | - [Security](#security) 6 | - [Reporting Security Issues](#reporting-security-issues) 7 | - [Preferred Languages](#preferred-languages) 8 | - [Policy](#policy) 9 | 10 | 11 | 12 | Computer security, cybersecurity (cyber security), digital security or 13 | information technology security (IT security) is the protection of computer 14 | systems and networks from attack by malicious actors that may result in 15 | unauthorized information disclosure, theft of, or damage to hardware, software, 16 | or data, as well as from the disruption or misdirection of the services they 17 | provide. (Source: [Wikipedia](https://en.wikipedia.org/wiki/Computer_security)) 18 | 19 | If you believe you have found a security vulnerability in any of 20 | [Obscurely's](https://github.com/Obscurely) repository that meets the definition 21 | below please report it to us as described below. 22 | 23 | Vulnerabilities are flaws in a computer system that weaken the overall security 24 | of the device/system. Vulnerabilities can be weaknesses in either the hardware 25 | itself, or the software that runs on the hardware. Vulnerabilities can be 26 | exploited by a threat actor, such as an attacker, to cross privilege boundaries 27 | (i.e. perform unauthorized actions) within a computer system. To exploit a 28 | vulnerability, an attacker must have at least one applicable tool or technique 29 | that can connect to a system weakness. In this frame, vulnerabilities are also 30 | known as the attack surface. (Source: 31 | [Wikipedia]()) 32 | 33 | ## Reporting Security Issues 34 | 35 | **Please do not report security vulnerabilities through public GitHub issues.** 36 | 37 | Instead, please report it by sending an email to 38 | [obscurely.message@protonmail.com](mailto:obscurely.message@protonmail.com) 39 | 40 | You should receive a response within 24 hours. If for some reason you do not, 41 | please resend that email to ensure I received your original message. I may not 42 | be available, if so please wait and **do not make the vulnerability public** 43 | 44 | Please include the requested information listed below (as much as you can 45 | provide) to help me better understand the nature and scope of the possible 46 | issue: 47 | 48 | - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, 49 | etc.) 50 | - Full paths of source file(s) related to the manifestation of the issue 51 | - The location of the affected source code (tag/branch/commit or direct URL) 52 | - Any special configuration required to reproduce the issue 53 | - Step-by-step instructions to reproduce the issue 54 | - Proof-of-concept or exploit code (if possible) 55 | - Impact of the issue, including how an attacker might exploit the issue 56 | 57 | This information will help me triage your report more quickly. 58 | 59 | ## Preferred Languages 60 | 61 | I prefer all communications to be in English, but we have translation software 62 | nowdays so you do you. 63 | 64 | ## Policy 65 | 66 | In computer security, coordinated vulnerability disclosure, or "CVD" (formerly 67 | known as responsible disclosure) is a vulnerability disclosure model in which a 68 | vulnerability or an issue is disclosed to the public only after the responsible 69 | parties have been allowed sufficient time to patch or remedy the vulnerability 70 | or issue. This coordination distinguishes the CVD model from the "full 71 | disclosure" model. (Source: 72 | [Wikipedia](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure)) 73 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | title: falion's homepage 3 | description: Bookmark this to keep an eye on my project updates! 4 | -------------------------------------------------------------------------------- /assets/fonts/RobotoMonoNerdFont-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/assets/fonts/RobotoMonoNerdFont-Regular.ttf -------------------------------------------------------------------------------- /assets/images/dmg-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/assets/images/dmg-background.png -------------------------------------------------------------------------------- /assets/images/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/assets/images/logo.icns -------------------------------------------------------------------------------- /assets/images/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/assets/images/logo.ico -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/assets/images/logo.png -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(windows)] 3 | { 4 | embed_resource::compile("resources\\windows\\resources.rc", embed_resource::NONE); 5 | } 6 | slint_build::compile("./ui/main.slint").unwrap(); 7 | } 8 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | **[Link to project on GitHub](https://github.com/Obscurely/falion)** 4 | 5 | - [Template](TEMPLATE.md) - Instructions to use the template this project is 6 | based on 7 | -------------------------------------------------------------------------------- /docs/TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Template Documentaion 2 | 3 | This template works by using a set of CHANGEME tags across the whole repository 4 | and using a python script it changes all of those tags to match your repository, 5 | making it seem just as if you built everything from the ground up specifically 6 | for your project. For a full list of features this template has read 7 | [Features](template/FEATURES.md). 8 | 9 | **For this template to work with no changes make sure to choose a repository 10 | name that is not present on crates.io or in the AUR** 11 | 12 | **Required to follow**: [How to Start](template/START.md) & 13 | [Requirements](template/REQ.md) 14 | 15 | **For proper use give a read**: [Features](template/FEATURES.md) & 16 | [Workflow](template/WORKFLOW.md) 17 | 18 | **Before first release**: you should read the rest 19 | 20 | - [How to Start](template/START.md) - Instructions on the first things you 21 | should do using this template 22 | - [Requirements](template/REQ.md) - Things you need to change to make this 23 | repository fully work 24 | - [Features](template/FEATURES.md) - Features of this template 25 | - [Workflow](template/WORKFLOW.md) - Instructions on the proper use of this 26 | template and what [I](https://github.com/Obscurely) think is the "best" way to 27 | program using RUST 28 | - [Update the Logo](template/UPDATE_LOGO.md) - Instructions on how to update the 29 | logo using the provided script. 30 | - [Update the Description](template/UPDATE_DESC.md) - Instructions on how to 31 | update the _short_ description using the provided script. 32 | - [Manual Changes](template/MANUAL.md) - Things you have to change manually, 33 | like features of your project, **not required** 34 | - [Extras](template/EXTRAS.md) - Extra things to change if you wanna make your 35 | repository better, **not required/just 2 sections if you want to follow 36 | [my](https://github.com/Obscurely) [Workflow](template/WORKFLOW.md)** 37 | -------------------------------------------------------------------------------- /docs/template/EXTRAS.md: -------------------------------------------------------------------------------- 1 | # Extras 2 | 3 | 4 | 5 | - [Extras](#extras) 6 | - [Signing commits](#signing-commits) (required to follow 7 | [my](https://github.com/Obscurely) Workflow) 8 | - [Repository Settings](#repository-settings) 9 | - [General Section](#general-section) 10 | - [Social Preview](#social-preview) 11 | - [Enable Wikis](#enable-wikis) 12 | - [Sponsorships](#sponsorships) 13 | - [Discussion](#discussion) 14 | - [Branches section](#branches-section) 15 | - [Branch protection rules](#branch-protection-rules) (required to follow 16 | [my](https://github.com/Obscurely) Workflow) 17 | - [Rules](#rules) 18 | - [Rulesets](#rulesets) (required to follow 19 | [my](https://github.com/Obscurely) Workflow) 20 | - [Pages](#pages) 21 | - [Code security and analysis](#code-security-and-analysis) 22 | 23 | 24 | 25 | ## Signing commits 26 | 27 | You have to setup commit signing if you want to use 28 | [my](https://github.com/Obscurely) [Workflow](WORKFLOW.md) and in general you 29 | should set this up as it proves your work is actually yours. To actually do this 30 | you can follow the whole _GPG commit signature verification_ section on this 31 | [GitHub article](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#gpg-commit-signature-verification) 32 | 33 | ## Repository Settings 34 | 35 | All of the following assume you are in the Settings page of your repository. 36 | Just click settings on the toolbar up top on your repository page. 37 | 38 | ### General Section 39 | 40 | #### Social Preview 41 | 42 | After [I](https://github.com/Obscurely)'ve got a repository about description, 43 | [I](https://github.com/Obscurely) like to generate a social preview with this 44 | [tool](https://www.bannerbear.com/demos/github-social-preview-generator-tool/), 45 | edit it to remove how many stars the repository has, so 46 | [I](https://github.com/Obscurely) don't have to update it, and then add it on 47 | GitHub. 48 | 49 | #### Enable Wikis 50 | 51 | Enable Wikis and any extra documentation you may have written in the `docs/` 52 | folder publish it here too. 53 | 54 | #### Sponsorships 55 | 56 | If you haven't added a way to get donations when setting up the template this 57 | might be a good time to do so. 58 | 59 | #### Discussion 60 | 61 | This is a great way to stay connected with your community. Enable this and just 62 | start by opening a discussion on the general topic beginning with a message like 63 | this: 64 | 65 | `Welcome to the General Discussions page! Please don't forget to read our CODE_OF_CONDUCT.md` 66 | 67 | ### Branches section 68 | 69 | #### Branch protection rules 70 | 71 | In order to properly follow [my](https://github.com/Obscurely) workflow you will 72 | have to create a new rule with the following settings: 73 | 74 | - Branch name pattern: master 75 | - [x] Require a pull request before merging 76 | - [x] Require approvals (1) 77 | - [x] Dismiss stale pull request approvals when new commits are pushed 78 | - [x] Require status checks to pass before merging 79 | 80 | - [x] Require branches to be up to date before merging 81 | 82 | Status checks that are required: 83 | 84 | - DevSkim 85 | - Cargo Deny 86 | - Test Suite 87 | - Rustfmt 88 | - Clippy 89 | - Super Linter 90 | - Miri 91 | - Docs 92 | 93 | - [x] Require conversation resolution before merging 94 | - [x] Require signed commits 95 | - [x] Require linear history 96 | - [x] Require deployments to succeed before merging 97 | - [x] github-pages 98 | - [x] Do not allow bypassing the above settings 99 | 100 | ### Rules 101 | 102 | #### Rulesets 103 | 104 | This is going put some restrictions on the branches you work on to then merge to 105 | master, like requiring a linear history and signed commits. So create a new rule 106 | with following options: 107 | 108 | - name: Main (or whatever you want) 109 | - Enforcement status: Active 110 | - Target branches: All branches 111 | - [x] Require linear history 112 | - [x] Require signed commits 113 | - [x] Block force pushes 114 | 115 | ### Pages 116 | 117 | The following are settings for the site hosted with the content from the `docs/` 118 | folder 119 | 120 | - Source: Deploy from a branch 121 | - Branch: master & `docs/` 122 | 123 | ### Code security and analysis 124 | 125 | - Private vulnerability reporting: Enabled 126 | - Dependency graph: Enabled (it's a must if your repository is public) 127 | - Dependabot alerts: Enabled 128 | - Dismiss low impact alerts: Enabled 129 | - Dependabot security updates: Enabled 130 | - Pull request check failure: High or Higher / Only errors 131 | - Secret scanning: Enabled 132 | - Push protection: Enabled 133 | -------------------------------------------------------------------------------- /docs/template/FEATURES.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | 4 | 5 | - [Features](#features) 6 | - [Automatic](#automatic) 7 | - [GitHub automatic](#github-automatic) 8 | - [Rust automatic](#rust-automatic) 9 | - [Static](#static) 10 | - [GitHub static](#github-static) 11 | - [Rust static](#rust-static) 12 | 13 | 14 | 15 | ## Automatic 16 | 17 | ### GitHub automatic 18 | 19 | - Python script that automatically initializes the repository according to your 20 | username and project name 21 | - Python script that automatically updates the logo by converting & copying it 22 | - Python script that automatically updates the short description (GitHub about 23 | description) 24 | - Automatic issue labeling according to which folders have changes 25 | - Automatically greeting users creating issues and PRs 26 | - Weekly scheduled Dependabot checks 27 | 28 | ### Rust automatic 29 | 30 | - Automatic checks running on each push 31 | - Cargo deny 32 | - Cargo test 33 | - Rustfmt check 34 | - Clippy 35 | - Super linter 36 | - Cargo miri 37 | - Rust docs check 38 | - Automatic releases when pushing a new tag 39 | - Linux 40 | - Binary 41 | - AppImage 42 | - AUR (stable pkgs only if the tag contains stable) 43 | - Nix file 44 | - Deb file 45 | - MacOS 46 | - Binary 47 | - App Folder 48 | - DMG installer 49 | - Homebrew (only if the tag contains stable) 50 | - Windows 51 | - Executable 52 | - Msi installer 53 | - All Platforms 54 | - Crates.io (only if the tag contains stable) 55 | - Daily scheduled cargo audit runs 56 | - Daily scheduled DevSkim runs 57 | 58 | ## Static 59 | 60 | ### GitHub static 61 | 62 | - Repository is under [MIT license](https://mit-license.org/) 63 | - One of the best READMEs for Rust projects (based on 64 | [this README](https://github.com/othneildrew/Best-README-Template)) 65 | - Issue Templates (written in modern yml format) 66 | - Bug report 67 | - Feature request 68 | - Config with links to discussion, discord & email 69 | - Pull Request Template 70 | - CHANGELOG template 71 | - CODE OF CONDUCT file 72 | - CONTRIBUTING file 73 | - SECURITY file 74 | - GitHub pages set up 75 | 76 | ### Rust static 77 | 78 | - Cargo.toml fully configured 79 | - Complete information about package 80 | - Best binary performance settings 81 | - A list of amazing crates 82 | - Cargo deny setup 83 | - Only allow permissive licenses for crates 84 | - Only allow crates that run at least on x86_64 linux, macOS & windows 85 | - And all the other default cargo deny checks 86 | - Rustfmt config to enable more formatting options (doesn't change the defaults) 87 | - Cargo-fuzz set up with a special README & an example 88 | - Rust build file that integrates the logo into the Windows binary 89 | - Rust integration & unit test examples 90 | - Shell.nix file for nix users with everything needed 91 | -------------------------------------------------------------------------------- /docs/template/MANUAL.md: -------------------------------------------------------------------------------- 1 | # Manual Changes 2 | 3 | 4 | 5 | - [Manual Changes](#manual-changes) 6 | - [GitHub Issue Template Config](#github-issue-template-config) 7 | - [Discord Server (line 7)](#discord-server-line-7) 8 | - [Email (line 12)](#email-line-12) 9 | - [Cargo.toml](#cargotoml) 10 | - [Categories (line 12)](#categories-line-12) 11 | - [Keywords (line 13)](#keywords-line-13) 12 | - [Debian section (line 27)](#debian-section-line-27) 13 | - [README](#readme) 14 | - [Project about (line 99)](#project-about-line-99) 15 | - [Features (line 107)](#features-line-107) 16 | - [Video showcase (line 113)](#video-showcase-line-113) 17 | - [Third-party libraries (line 125)](#third-party-libraries-line-125) 18 | - [Usage basics (line 477)](#usage-basics-line-477) 19 | - [Usage advanced (line 481)](#usage-advanced-line-481) 20 | - [Road map link (line 488)](#road-map-link-line-488) 21 | - [FAQ (line 511)](#faq-line-511) 22 | - [fuzz/README.md](#fuzzreadmemd) 23 | - [Fuzz harnesses (line 65)](#fuzz-harnesses-line-65) 24 | 25 | 26 | 27 | ## GitHub Issue Template Config 28 | 29 | ### Discord Server (line 7) 30 | 31 | Ideally you should have a link a server/group on something like discord or 32 | matrix. Discord would be preferred since is more used. 33 | 34 | ### Email (line 12) 35 | 36 | Ideally you should also have an email. Unfortunately GitHub doesn't let you put 37 | emails directly or mailto links so you have to use a site like the one I used 38 | to generate a special link with your email. 39 | 40 | ## Cargo.toml 41 | 42 | ### Categories (line 12) 43 | 44 | Fill the array with any of these [categories](https://crates.io/category_slugs) 45 | with a maximum of 5. Just make sure they represent your project. 46 | 47 | ### Keywords (line 13) 48 | 49 | Fill the array with any keywords that represent your project, the maximum is 5. 50 | For maximum reach I recommend you write 5 distinct ones. 51 | 52 | ### Debian section (line 27) 53 | 54 | Change changeme tag with your desired section. Go to this 55 | [debian page](https://packages.debian.org/bookworm/) click on a section you want 56 | and up top you'll see _Section_… something, that something is what you need to 57 | put here. 58 | 59 | ## README 60 | 61 | ### Project about (line 99) 62 | 63 | This is the section where you provide your long description explaining how cool 64 | your project is, what are the highlights of it and problems it fixes. 65 | 66 | ### Features (line 107) 67 | 68 | This is the list of features your project has or will have, written using a 69 | checkbox list. Here you should put all your long time goals, things you want 70 | your project to be able to do eventually in order to attract people to keep 71 | following the progress and give you a star. 72 | 73 | ### Video showcase (line 113) 74 | 75 | You should record a video of you using the project, cut out the useless parts 76 | and speed it up, in order to show a user exactly what it does. This will help 77 | people save time and help you get more users, someone may not think they need 78 | your project, but after they see the showcase they might change their minds. 79 | 80 | After you have your video go to GitHub to edit your README, delete the changeme 81 | text and drag your video over inside the parenthesis. The video will get 82 | uploaded to GitHub's servers and a direct link to it will be placed there. 83 | 84 | ### Third-party libraries (line 125) 85 | 86 | Any external crates you're using with a hyperlink to them (on lib.rs or 87 | crates.io) and a short phrase explaining why. 88 | 89 | ### Usage basics (line 477) 90 | 91 | Basic concepts a user needs to know in order to use your app. The basics should 92 | be intuitive, if they aren't you should rethink your interface, this is just in 93 | case some users are in doubt. 94 | 95 | ### Usage advanced (line 481) 96 | 97 | Advanced concepts a user might want to know to take full advantage of your app. 98 | 99 | ### Road map link (line 488) 100 | 101 | The ID of the road map associated with your project on GitHub. If you don't have 102 | one go to your repository page, click on _Projects_ up top and create a new one. 103 | 104 | ### FAQ (line 511) 105 | 106 | The _frequently asked questions_, usual questions the users might have about 107 | your project and their answers. You can write them in the template I gave you or 108 | any other way. 109 | 110 | ## fuzz/README.md 111 | 112 | ### Fuzz harnesses (line 65) 113 | 114 | Information about every fuzz harness you have, what is tests. 115 | -------------------------------------------------------------------------------- /docs/template/REQ.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | 4 | 5 | - [Requirements](#requirements) 6 | - [Public GitHub repository](#public-github-repository) 7 | - [GitHub actions permissions](#github-actions-permissions) 8 | - [Version scheme](#version-scheme) 9 | - [homebrew-tap repository](#homebrew-tap-repository) 10 | - [Action secrets](#action-secrets) 11 | - [API_TOKEN_GITHUB](#api_token_github) 12 | - [AUR](#aur) 13 | - [AUR_USERNAME](#aur_username) 14 | - [AUR_EMAIL](#aur_email) 15 | - [AUR_SSH_PRIVATE_KEY](#aur_ssh_private_key) 16 | - [CRATES_TOKEN](#crates_token) 17 | 18 | 19 | 20 | All of the following have to be met in order for this template to work to its 21 | full potential and not encounter any issues. If you for example don't want to 22 | upload to the AUR just edit `.github/workflows/release.yml` accordingly. 23 | 24 | ## Public GitHub repository 25 | 26 | For all of the actions, branch rules etc. to be available (if you are a free 27 | GitHub user) the repository has to be public. 28 | 29 | ## GitHub actions permissions 30 | 31 | On your GitHub repository page go to Settings, Actions, General (the link should 32 | look like this: `https://github.com/{USER}/{REPO}/settings/actions`), scroll 33 | down to `Workflow permissions` and make sure `Read and write permissions` is 34 | selected. This is so the actions can create new releases. 35 | 36 | ## Version scheme 37 | 38 | You must use [SemVer](https://semver.org/). Every release you make should look 39 | something like this: `v0.1.0-stable` or `v0.1.0-beta`. Note, only stable 40 | releases will make external changes like updating AUR stable pkgs or homebrew 41 | pkg. 42 | 43 | ## homebrew-tap repository 44 | 45 | You will need to have public repository on your account named `homebrew-tap`. 46 | When the you create a new stable release the GitHub actions will update/publish 47 | the homebrew version for macOS. 48 | 49 | ## Action secrets 50 | 51 | All of the following have to be added in the secrets tab for actions. To get 52 | here go to your GitHub repository page -> Settings -> Secrets and variables -> 53 | Actions -> Secrets tab (the link should look like this: 54 | `https://github.com/{USER}/{REPO}/settings/secrets/actions`). In order to add a 55 | secret click `New repository secret`. 56 | 57 | ### API_TOKEN_GITHUB 58 | 59 | [Create a new access token](https://github.com/settings/tokens/new), copy it and 60 | save it somewhere safe, you will not able to after. Create a new secret with the 61 | name `API_TOKEN_GITHUB` and paste the token in the secret box (second box). The 62 | token you create will be of type classic and have only the repository scope 63 | selected. 64 | 65 | ### AUR 66 | 67 | You'll need an AUR account with ssh keys for the next steps, if you don't have 68 | one: 69 | 70 | 1. [Create a new account](https://aur.archlinux.org/register), you only need to 71 | provide a username, email and password (on the next page). Unfortunately you 72 | will need to boot an Arch Linux iso (if you don't use Arch) to complete the 73 | captcha verification, but it's only a one time thing. 74 | 75 | 1. To generate a new key pair run: 76 | `ssh-keygen -t ed25519 -C "your_email@example.com"`, replacing the example 77 | email with the one you used for your AUR account. 78 | 79 | 1. Open you _.pub_ file in `~/.ssh` and copy everything besides your email, go 80 | to the _My account_ page on AUR and paste it in the _SSH Public Key_ section. 81 | Scroll down fill in your password and hit update. 82 | 83 | #### AUR_USERNAME 84 | 85 | Create a new secret with the name `AUR_USERNAME` fill in your AUR username in 86 | the secret box (second box). 87 | 88 | #### AUR_EMAIL 89 | 90 | Create a new secret with the name `AUR_EMAIL` fill in your AUR email in the 91 | secret box (second box). 92 | 93 | #### AUR_SSH_PRIVATE_KEY 94 | 95 | Open your ssh private key file in `~/.ssh` (the one that doesn't end in .pub) 96 | and copy everything. Create a new secret with the name `AUR_SSH_PRIVATE_KEY` and 97 | paste your key in the secret box (second box). 98 | 99 | ### CRATES_TOKEN 100 | 101 | You'll need a crates.io token for the following steps, if you don't have one: 102 | 103 | 1. Login on crates.io using GitHub. 104 | 1. Go to [API tokens pages](https://crates.io/settings/tokens). 105 | 1. Click _New Token_ and save this token somewhere safe, you'll not be able 106 | after. 107 | 108 | Create a new secret with the name `CRATES_TOKEN` fill in your crates.io token in 109 | the secret box (second box). 110 | -------------------------------------------------------------------------------- /docs/template/START.md: -------------------------------------------------------------------------------- 1 | # How to Start 2 | 3 | 1. Create a new GitHub (only GitHub works) repository from 4 | [this template](https://docs.github.com/en/enterprise-cloud@latest/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template#creating-a-repository-from-a-template) 5 | 6 | 1. Clone your new repository & cd into it 7 | 8 | ```shell 9 | git clone https://github.com/{YOUR_USERNAME}/{YOUR_REPOSITORY_NAME} 10 | cd {YOUR_REPOSITORY_NAME} 11 | ``` 12 | 13 | 1. Install python (if you don't have it) 14 | 15 | - On Windows 16 | 17 | a. Download & run the installer from their 18 | [official site](https://www.python.org/downloads/windows/) 19 | 20 | b. Install it using winget (replace 3.11 with whatever version is the 21 | latest) 22 | 23 | ```shell 24 | winget install -e --id Python.Python.3.11 25 | ``` 26 | 27 | Note: This may or may not add python3 to path, if not do it 28 | [manually](https://realpython.com/add-python-to-path/). 29 | 30 | - On Linux 31 | 32 | Use your distribution's package manager, for example on Arch Linux: 33 | 34 | ```shell 35 | sudo pacman -Sy python3 36 | ``` 37 | 38 | or if you are on NixOS just do the following and skip the next step 39 | 40 | ```shell 41 | nix-shell 42 | ``` 43 | 44 | - On macOS 45 | 46 | a. Download & run the installer from their 47 | [official site](https://www.python.org/downloads/macos/) 48 | 49 | b. Install it using homebrew ([install homebrew](https://brew.sh/)) 50 | 51 | ```shell 52 | brew install python3 53 | ``` 54 | 55 | 1. Install/Upgrade pip and install the 56 | [Pillow](https://pypi.org/project/Pillow/) library 57 | 58 | ```shell 59 | python3 -m pip install --upgrade pip 60 | python3 -m pip install --upgrade Pillow 61 | ``` 62 | 63 | 1. Replace the default emails in the scripts/data/PMAIL & scripts/data/SMAIL 64 | files 65 | 66 | PMAIL - primary email address associated with your user. 67 | 68 | SMAIL - secondary email where you should receive ISSUE & SECURITY reports 69 | from users 70 | 71 | You could just use the same email in both files, I just like a little 72 | fragmentation. 73 | 74 | 1. Run the following command in the root of your project 75 | 76 | ```shell 77 | python3 scripts/repo.py init 78 | ``` 79 | 80 | This will initialize the repository by replacing falion, falion, Obscurely, 81 | PMAIL, SMAIL placeholders in every file with the according values and rename 82 | all the folders & files that have a CHANGEME tag as well as converting and 83 | copying the default logo.png where needed. 84 | 85 | 1. Finish by pushing your changes 86 | 87 | ```shell 88 | git commit -am "init repo" 89 | git push 90 | ``` 91 | 92 | ## Additional Resources 93 | 94 | - [Update the Logo](UPDATE_LOGO.md) 95 | - [Update the Description](UPDATE_DESC.md) 96 | -------------------------------------------------------------------------------- /docs/template/UPDATE_DESC.md: -------------------------------------------------------------------------------- 1 | # Update the Description (short) 2 | 3 | **This section assumes you followed [How to Start](START.md) completely which you 4 | should.** 5 | 6 | **!!! This command can only be run once, after that you'll have to change it 7 | manually !!!** 8 | 9 | This is the short description that you would put in your GitHub repository about 10 | section. 11 | 12 | 1. Put your desired description in scripts/data/DESC. 13 | 14 | It shouldn't contain any new lines, they will get replaced anyway. 15 | 16 | 1. In the root of your repository run the following command 17 | 18 | ```shell 19 | python3 scripts/repo.py udesc 20 | ``` 21 | 22 | This will replace the placeholder for the description everywhere. 23 | -------------------------------------------------------------------------------- /docs/template/UPDATE_LOGO.md: -------------------------------------------------------------------------------- 1 | # Update the Logo 2 | 3 | **This section assumes you followed [How to Start](START.md) completley which you 4 | should.** 5 | 6 | This command can be run as many times as you want. 7 | 8 | 1. Add your logo by replacing the assets/images/logo.png file. 9 | 10 | The logo has to be named logo.png and be in a 1:1 ratio (usually 512×512 11 | pixels) 12 | 13 | 1. In the root of your repository run the following command 14 | 15 | ```shell 16 | python3 scripts/repo.py ulogo 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/template/WORKFLOW.md: -------------------------------------------------------------------------------- 1 | # Workflow 2 | 3 | 4 | 5 | - [Workflow](#workflow) 6 | - [Git commands](#git-commands) 7 | - [Project start](#project-start) 8 | - [Planning](#planning) 9 | - [Beginning development](#beginning-development) 10 | - [Usual development](#usual-development) 11 | - [Making a release](#making-a-release) 12 | - [Keeping a changelog](#keeping-a-changelog) 13 | - [Coding workflow](#coding-workflow) 14 | - [Extras](#extras) 15 | - [Road map](#road-map) 16 | - [Third-party libraries](#third-party-libraries) 17 | - [Fuzzing](#fuzzing) 18 | - [Advice](#advice) 19 | - [Commits](#commits) 20 | - [Multiple branches](#multiple-branches) 21 | 22 | 23 | 24 | This section assumes you have fully followed [START.md](START.md), 25 | [REQ.md](REQ.md) and the 2 required sections for the Workflow in 26 | [EXTRAS.md](EXTRAS.md) 27 | 28 | If don't have a first version follow [project start](#project-start) after that 29 | you should follow [usual development](#usual-development). 30 | 31 | Keep in mind [extras](#extras) and [advice](#advice). 32 | 33 | For an optimal coding experience I suggest you follow a 34 | [test driven development model](#coding-workflow). 35 | 36 | ## Git commands 37 | 38 | This are the used git commands in [my](https://github.com/Obscurely) Workflow or 39 | some that you may need and an explanation to what they do.\ 40 | unstaged - before doing `git add .`\ 41 | staged/index - after doing `git add .`\ 42 | remote - the target branch on GitHub 43 | 44 | - `git add .` Stages all changed files to the index. 45 | - `git status` See what files you've changed 46 | - `git commit -m "message"` commits the changes to the repository. 47 | - `git push` or `git push -u origin some-branch` pushes the changes to the 48 | remote (on GitHub) 49 | - `git diff` view all the unstaged changes you've made; add `--staged` to 50 | compare to index; also you can do `git diff some_file` to view changes made to 51 | only one file 52 | - 'git restore some_file' to restore a file, add `--staged` to restore a staged 53 | file 54 | - 'git clean -fd' to completly remove any changes you've made that **are not** 55 | staged 56 | - `git log --oneline` to nicely view the log of commits 57 | - `git ls-tree commit-id` to view what files a commit changed, where `commit-id` 58 | is the ID of the target commit or if you want the commit before the last one 59 | use `HEAD~1` (can be a different number) 60 | - `git show HEAD~1:some_file` view the stage of a file in the commit before the 61 | last commit, of course `HEAD~1` can be any commit ID and you don't have to 62 | specify a file. 63 | - `git restore --source=HEAD~1 some_file` restore some_file to the state it was 64 | in 2 commits ago, of course `HEAD~1` can be any commit ID 65 | - `git checkout -b next` create a new branch next and swtich to it 66 | - `git switch master` to switch back to master 67 | - `git diff next` to see the differences between the next branch and master 68 | - `git branch -d next` to delete the next branch 69 | - `git revert HEAD~1` to revert to the changes made to 2 commits ago, you may 70 | encounter conflicts and have to do something like 71 | `git rm some_file && git revert --continue`, of course `HEAD~1` can be any 72 | commit ID. 73 | 74 | ## Project start 75 | 76 | ### Planning 77 | 78 | When beginning a new project you should start by writting your 79 | [project about](MANUAL.md#project-about-line-99) to reflect and get an idea of 80 | what you want it to do. Don't go overkill just write enough to solve the problem 81 | you had originally. Also write the [features](MANUAL.md#features-line-107) your 82 | project needs to have in order to solve your original problem. 83 | 84 | The above is the basic idea. I like to go beyond that and write personal, more 85 | detailed docs and make Excalidraw diagrams to help me see my vision better. 86 | 87 | ### Beginning development 88 | 89 | For the first basic version of the software, my workflow is basically the 90 | following 91 | 92 | 1. Checkout the code to a branch called v0.1.0 93 | 94 | ```shell 95 | git checkout -b v0.1.0 96 | ``` 97 | 98 | 1. Start making changes and with every new meaningful change do the following 99 | 100 | - Complete your [features](MANUAL.md#features-line-107) list if you add any 101 | new. 102 | - Write any new [usage](MANUAL.md#usage-basics-line-477) the app has 103 | available. 104 | - Add a new q&a on your [FAQ](MANUAL.md#faq-line-511) if needed. 105 | 106 | If all is good commmit the new changes. 107 | 108 | ```shell 109 | git add . 110 | git diff --staged # check changes 111 | git commit -m "add new feature" 112 | git push 113 | ``` 114 | 115 | 1. When you are done with making v0.1.0 do the following 116 | 117 | First make a [video showcase](MANUAL.md#video-showcase-line-113) for your 118 | project.\ 119 | Then merge the branch v0.1.0 into master by doing a PR on GitHub.\ 120 | And then in order to make a release: 121 | 122 | ```shell 123 | git switch master 124 | git pull 125 | git checkout -d v0.1.0 126 | git tag v0.1.0-stable # it's important your tag has stable in it 127 | git push --tags 128 | ``` 129 | 130 | This will create a new release and CD pipelines will start compiling, making 131 | & publishing your releases. More about [making releases](#making-a-release) 132 | 133 | ## Usual development 134 | 135 | After the first version this is how I would continue 136 | 137 | 1. Checkout the code to a new branch, like feature-x 138 | 139 | ```shell 140 | git checkout -b feature-x 141 | ``` 142 | 143 | 1. Add the new feature on the [features](MANUAL.md#features-line-107) list 144 | 145 | 1. With every meaningful change 146 | 147 | - Write any new [usage](MANUAL.md#usage-basics-line-477) the app has 148 | available. 149 | - Add a new q&a on your [FAQ](MANUAL.md#faq-line-511) if needed. 150 | 151 | Check the new changes 152 | 153 | ```shell 154 | git add . 155 | git diff --staged 156 | ``` 157 | 158 | If everything is good follow [keeping a changelog](#keeping-a-changelog) and 159 | once making a changelog push the changes. 160 | 161 | ```shell 162 | git add CHANGELOG.md 163 | git commit -m "change something" 164 | git push 165 | ``` 166 | 167 | 1. Once done implementing the feature do a PR on GitHub to master 168 | 169 | 1. Once the PR was accepted create a new release 170 | 171 | ```shell 172 | git switch master 173 | git pull 174 | git checkout -d feature-x 175 | git tag v0.2.0-stable 176 | git push --tags 177 | ``` 178 | 179 | More info about [making a release](#making-a-release) 180 | 181 | ## Making a release 182 | 183 | Because of all the automation this template has you don't have to fear or be 184 | tired of making new releases just because of a tiny bug since everything is 185 | really fast and simple. 186 | 187 | First you have to keep in mind release tags have to look like the following.\ 188 | `v0.1.0-stable` - for a stable release where homebrew pkg, AUR stable pkgs & 189 | crates.io get updated\ 190 | `v0.1.0-beta` - where beta can be anything and the only external thing that gets 191 | updated is the AUR git pkg. 192 | 193 | Second, while mostly optional since there are things in the CD pipelines that 194 | take care of this to an extent is updating the version of your software inside 195 | Cargo.toml to match the one you are releasing, don't worry if you make a 196 | mistake, the CD pipeline will fix it for you. 197 | 198 | Third once you have all the code you want for your next release on the master 199 | branch run the following 200 | 201 | ```shell 202 | git tag v0.1.0-stable 203 | git push --tags 204 | ``` 205 | 206 | This will make a new tag which will trigger the CD pipeline and start making the 207 | releases for you. Now that you have a new release on GitHub you should copy the 208 | contents of your CHANGELOG for the latest version and dump them in your release 209 | by doing an edit. 210 | 211 | ## Keeping a changelog 212 | 213 | While the release system generates a simple changelog from PRs and commits it's 214 | not enough, we want our own, human written, changelog so our users can easily 215 | and exactly know what changed. [I](https://github.com/Obscurely) know there are 216 | also GitHub action that beautify this way of making changelog, but still it's 217 | not good enough. 218 | 219 | So what I do is everytime I have some changes staged 220 | 221 | ```shell 222 | git add . 223 | ``` 224 | 225 | I run the following 226 | 227 | ```shell 228 | git diff --staged 229 | ``` 230 | 231 | Look at all the changes I've made and start filling the CHANGELOG file 232 | 233 | Once I'm about to release a new version I will copy the CHANGELOG and make it 234 | fresh (no changes written) and keep the tag master, while for the current 235 | version one I will delete any unfilled section and change the text from master 236 | to whatever the current version tag is. 237 | 238 | ## Coding workflow 239 | 240 | Coding in rust is all about functions so the way I do it is the following: 241 | 242 | 1. Write a test. 243 | 1. Write just enough code, no matter how bad, just to pass the test. 244 | 1. Refactor the code 245 | 1. Improve performance 246 | 247 | Where needed I will also write integration tests and [fuzz harnesses](#fuzzing) 248 | 249 | ## Extras 250 | 251 | Things to do while programming. 252 | 253 | ### Road map 254 | 255 | As you think of a new feature you want your project to have don't hesitate to 256 | add it on the road map so you eventually get to making it. Also add that feature 257 | as unchecked on your README in the [features](MANUAL.md#features-line-107) 258 | section. 259 | 260 | ### Third-party libraries 261 | 262 | When adding a new crate to your project the first thing you should do is run 263 | `cargo deny check` to see if the crate complies with your requirements. Second 264 | thing is adding the crates to your README 265 | [third-party libraries](MANUAL.md#third-party-libraries-line-125) section. 266 | 267 | ### Fuzzing 268 | 269 | Any time you have a crucial function that you want to make sure it works 270 | properly under any circumstances you will want to create a fuzz harness and run 271 | it for some time. For this you can read the README inside the fuzz folder and 272 | look at the example. Additionally there are a few YouTube videos you can watch 273 | about cargo-fuzz. 274 | 275 | ## Advice 276 | 277 | ### Commits 278 | 279 | Every commit should solve **one type of issue and one only**. You shouldn't have 280 | commits fixing typos, bugs and making code changes together. 281 | 282 | One common example would be you are making some code changes and then you 283 | suddenly find a bug or a typo, what you should do is fix that and then do 284 | something like this: 285 | 286 | ```shell 287 | git add file_with_bug 288 | git commit -m "fix bug x" 289 | git push 290 | ``` 291 | 292 | After which you can continue with making your code changes and then commit those 293 | separately. 294 | 295 | ### Multiple branches 296 | 297 | You should use multiple branches to separate your work (hence why if you use the 298 | settings proposed you can't even push to master directly). 299 | 300 | One example of why you should do this is for example you are working on 301 | feature-x and then you suddenly find a bug or god forbids a vulnerability in 302 | your production code. What you can now do is create a new branch to fix that and 303 | merge it fast into master and make a new hot fix release while not having to 304 | commit any unfinished feature. Also you would merge that bugfix into your 305 | feature branch as well (or not if you prefer). 306 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils_2": { 22 | "inputs": { 23 | "systems": "systems_2" 24 | }, 25 | "locked": { 26 | "lastModified": 1705309234, 27 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "numtide", 35 | "repo": "flake-utils", 36 | "type": "github" 37 | } 38 | }, 39 | "nixpkgs": { 40 | "locked": { 41 | "lastModified": 1714253743, 42 | "narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=", 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "rev": "58a1abdbae3217ca6b702f03d3b35125d88a2994", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "NixOS", 50 | "ref": "nixos-unstable", 51 | "repo": "nixpkgs", 52 | "type": "github" 53 | } 54 | }, 55 | "nixpkgs_2": { 56 | "locked": { 57 | "lastModified": 1706487304, 58 | "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", 59 | "owner": "NixOS", 60 | "repo": "nixpkgs", 61 | "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "owner": "NixOS", 66 | "ref": "nixpkgs-unstable", 67 | "repo": "nixpkgs", 68 | "type": "github" 69 | } 70 | }, 71 | "root": { 72 | "inputs": { 73 | "flake-utils": "flake-utils", 74 | "nixpkgs": "nixpkgs", 75 | "rust-overlay": "rust-overlay" 76 | } 77 | }, 78 | "rust-overlay": { 79 | "inputs": { 80 | "flake-utils": "flake-utils_2", 81 | "nixpkgs": "nixpkgs_2" 82 | }, 83 | "locked": { 84 | "lastModified": 1714616033, 85 | "narHash": "sha256-JcWAjIDl3h0bE/pII0emeHwokTeBl+SWrzwrjoRu7a0=", 86 | "owner": "oxalica", 87 | "repo": "rust-overlay", 88 | "rev": "3e416d5067ba31ff8ac31eeb763e4388bdf45089", 89 | "type": "github" 90 | }, 91 | "original": { 92 | "owner": "oxalica", 93 | "repo": "rust-overlay", 94 | "type": "github" 95 | } 96 | }, 97 | "systems": { 98 | "locked": { 99 | "lastModified": 1681028828, 100 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 101 | "owner": "nix-systems", 102 | "repo": "default", 103 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "nix-systems", 108 | "repo": "default", 109 | "type": "github" 110 | } 111 | }, 112 | "systems_2": { 113 | "locked": { 114 | "lastModified": 1681028828, 115 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 116 | "owner": "nix-systems", 117 | "repo": "default", 118 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 119 | "type": "github" 120 | }, 121 | "original": { 122 | "owner": "nix-systems", 123 | "repo": "default", 124 | "type": "github" 125 | } 126 | } 127 | }, 128 | "root": "root", 129 | "version": 7 130 | } 131 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Rust devshell"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | rust-overlay.url = "github:oxalica/rust-overlay"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = { 11 | nixpkgs, 12 | rust-overlay, 13 | flake-utils, 14 | ... 15 | }: 16 | flake-utils.lib.eachDefaultSystem ( 17 | system: let 18 | overlays = [(import rust-overlay)]; 19 | pkgs = import nixpkgs { 20 | inherit system overlays; 21 | }; 22 | in 23 | with pkgs; { 24 | devShells.default = mkShell rec { 25 | buildInputs = [ 26 | rust-bin.stable.latest.default 27 | llvmPackages_latest.llvm 28 | llvmPackages_latest.bintools 29 | llvmPackages_latest.lld 30 | openssl 31 | pkg-config 32 | fd 33 | zlib.out 34 | xorriso 35 | grub2 36 | cargo-audit # audit dependencies in order to scan for supply chain attacks 37 | cargo-fuzz # fuzzing tool 38 | cargo-deny # tool to deny crates based on checks. 39 | cargo-edit # manage cargo dependencies 40 | cargo-deb # pkg rust apps for debian 41 | cmake 42 | git 43 | gcc 44 | python311 45 | python311Packages.pillow # this is for python repo script 46 | openssl 47 | # falion specific 48 | libxkbcommon 49 | libGL 50 | # WINIT_UNIX_BACKEND=wayland 51 | wayland 52 | # WINIT_UNIX_BACKEND=x11 53 | xorg.libXcursor 54 | xorg.libXrandr 55 | xorg.libXi 56 | xorg.libX11 57 | # fonts 58 | fontconfig 59 | # Extra possible dependencies 60 | expat 61 | freetype 62 | freetype.dev 63 | ]; 64 | 65 | shellHook = '' 66 | alias find=fd 67 | ''; 68 | 69 | RUST_BACKTRACE = 1; 70 | # falion specific for ui (iced) 71 | LD_LIBRARY_PATH = builtins.foldl' (a: b: "${a}:${b}/lib") "${pkgs.vulkan-loader}/lib" buildInputs; 72 | }; 73 | } 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | # cargo fuzz 2 | target/ 3 | corpus/ 4 | artifacts/ 5 | debug/ 6 | 7 | # cargo lock 8 | Cargo.lock 9 | 10 | # rustfmt backup files 11 | **/*.rs.bk 12 | 13 | # system files 14 | *.pdb 15 | .DS_Store 16 | .cargo-ok 17 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "falion-fuzz" 3 | version = "0.0.0" 4 | authors = ["Automatically generated"] 5 | publish = false 6 | edition = "2018" 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies] 12 | libfuzzer-sys = "0.4" 13 | tokio = "1.29.1" 14 | 15 | [dependencies.falion] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "fuzz_ddg_get_links" 24 | path = "fuzz_targets/fuzz_ddg_get_links.rs" 25 | test = false 26 | doc = false 27 | -------------------------------------------------------------------------------- /fuzz/README.md: -------------------------------------------------------------------------------- 1 | # Fuzzing 2 | 3 | 4 | 5 | - [Fuzzing](#fuzzing) 6 | - [Usage](#usage) 7 | - [Debugging a crash](#debugging-a-crash) 8 | - [A brief introduction to fuzzers](#a-brief-introduction-to-fuzzers) 9 | - [Each fuzzer harness in detail](#each-fuzzer-harness-in-detail) 10 | - [`fuzz_target_1`](#fuzztarget1) 11 | - [Acknowledgments](#acknowledgments) 12 | 13 | 14 | 15 | [Fuzz testing](https://en.wikipedia.org/wiki/Fuzzing) is a software testing 16 | technique used to find security and stability issues by providing pseudo-random 17 | data as input to the software. 18 | 19 | ## Usage 20 | 21 | To use the fuzzers provided in this directory, start by installing cargo-fuzz: 22 | 23 | ```bash 24 | cargo install cargo-fuzz 25 | ``` 26 | 27 | Once you have installed the cargo-fuzz, you can then execute any fuzzer with: 28 | 29 | ```bash 30 | cargo fuzz run name_of_fuzzer 31 | ``` 32 | 33 | ### Debugging a crash 34 | 35 | Once you've found a crash, you'll need to debug it. The easiest first step in 36 | this process is to minimise the input such that the crash is still triggered 37 | with a smaller input. `cargo-fuzz` supports this out of the box with: 38 | 39 | ```bash 40 | cargo fuzz tmin name_of_fuzzer artifacts/name_of_fuzzer/crash-... 41 | ``` 42 | 43 | From here, you will need to analyse the input and potentially the behaviour of 44 | the program. The debugging process from here is unfortunately less well-defined, 45 | so you will need to apply some expertise here. Happy hunting! 46 | 47 | ## A brief introduction to fuzzers 48 | 49 | Fuzzing, or fuzz testing, is the process of providing generated data to a 50 | program under test. The most common variety of fuzzers are mutational fuzzers; 51 | given a set of existing inputs (a "corpus"), it will attempt to slightly change 52 | (or "mutate") these inputs into new inputs that cover parts of the code that 53 | haven't yet been observed. Using this strategy, we can quite efficiently 54 | generate test cases which cover significant portions of the program, both with 55 | expected and unexpected data. 56 | [This is really quite effective for finding bugs.](https://github.com/rust-fuzz/trophy-case) 57 | 58 | The fuzzers here use [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz), a 59 | utility which allows Rust to integrate with 60 | [libFuzzer](https://llvm.org/docs/LibFuzzer.html), the fuzzer library built into 61 | LLVM. Each source file present in [`fuzz_targets`](fuzz_targets) is a harness, 62 | which is, in effect, a unit test which can handle different inputs. When an 63 | input is provided to a harness, the harness processes this data and libFuzzer 64 | observes the code coverage and any special values used in comparisons over the 65 | course of the run. Special values are preserved for future mutations and inputs 66 | which cover new regions of code are added to the corpus. 67 | 68 | ## Each fuzzer harness in detail 69 | 70 | Each fuzzer harness in [`fuzz_targets`](fuzz_targets) targets a different aspect 71 | of falion and tests them in different ways. While there is 72 | implementation-specific documentation in the source code itself, each harness is 73 | briefly described below. 74 | 75 | ### `fuzz_ddg_get_links` 76 | 77 | This fuzz harness checks to see if using random queries reqwest will fail over 78 | time because of either an error in the encoding or an error with it imposing I 79 | should add additional checks on the queries. None of this seems to happen! 80 | 81 | ## Acknowledgments 82 | 83 | - [Original README this is based on](https://github.com/astral-sh/ruff/blob/main/fuzz/README.md) 84 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_ddg_get_links.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | use falion::search::ddg; 4 | 5 | fuzz_target!(|data: &[u8]| { 6 | // fuzzed code goes here 7 | let ddg = ddg::Ddg::new(); 8 | 9 | let input: String = data.iter().map(|b| char::from_u32(*b as u32).unwrap()).collect(); 10 | 11 | tokio::runtime::Builder::new_multi_thread() 12 | .enable_all() 13 | .build() 14 | .unwrap() 15 | .block_on(async { 16 | match ddg.get_links(&input, None, None).await { 17 | Ok(_) => (), 18 | Err(ddg::DdgError::NoResults) => (), 19 | Err(ddg::DdgError::QueryTooLong) => (), 20 | Err(_) => panic!("error"), 21 | } 22 | }) 23 | }); 24 | -------------------------------------------------------------------------------- /resources/USEFUL_SITES.md: -------------------------------------------------------------------------------- 1 | # A list of useful sites 2 | 3 | ## GitHub 4 | 5 | - **[GitHub Banner Generator](https://www.bannerbear.com/demos/github-social-preview-generator-tool/)** 6 | 7 | ## Rust 8 | 9 | - **[The Book](https://doc.rust-lang.org/book/)** 10 | - **[Rust By Example](https://doc.rust-lang.org/stable/rust-by-example/)** 11 | - **[The Rust Reference](https://doc.rust-lang.org/reference/index.html)** 12 | - **[Rust Atomics](https://marabos.nl/atomics/)** 13 | -------------------------------------------------------------------------------- /resources/linux/AUR/bin/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Obscurely 2 | pkgname=falion-bin 3 | pkgver=PKGVER-PLACEHOLDER 4 | pkgrel=1 5 | epoch= 6 | pkgdesc="An open source, programmed in rust, privacy focused tool and crate for interacting with programming resources (like stackoverflow) fast, efficiently and asynchronously/parallel using the CLI or GUI." 7 | arch=('x86_64') 8 | url="https://github.com/Obscurely/falion" 9 | license=('MIT') 10 | provides=('falion-bin') 11 | conflicts=('falion' 'falion-git') 12 | source=("https://github.com/Obscurely/falion/releases/download/v${pkgver}-stable/falion-linux.tar.gz") 13 | sha256sums=("SHA-PLACEHOLDER") 14 | 15 | package() { 16 | cd "$srcdir/" 17 | 18 | install -Dm755 falion -t "${pkgdir}/usr/bin/" 19 | install -Dm644 README.md -t "${pkgdir}/usr/share/doc/${pkgname%-bin}/" 20 | install -Dm0644 -t "$pkgdir/usr/share/applications/" "linux/desktop/falion.desktop" 21 | for size in 16x16 32x32 64x64 128x128 256x256 512x512; do 22 | install -Dm0644 "linux/desktop/icons/hicolor/$size/apps/falion.png" \ 23 | "$pkgdir/usr/share/icons/hicolor/$size/apps/falion.png" 24 | done 25 | } 26 | -------------------------------------------------------------------------------- /resources/linux/AUR/git/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Obscurely 2 | pkgname=falion-git 3 | pkgver=PKGVER-PLACEHOLDER 4 | pkgrel=1 5 | pkgdesc="An open source, programmed in rust, privacy focused tool for scraping programming resources (like stackoverflow) fast, efficient and asynchronous/parallel using the CLI or GUI. " 6 | arch=('x86_64') 7 | url="https://github.com/Obscurely/falion" 8 | license=('GPL3') 9 | makedepends=('git' 'rust' 'cargo' 'binutils') 10 | provides=('falion-git') 11 | conflicts=('falion-bin' 'falion') 12 | source=('git+https://github.com/Obscurely/falion') 13 | sha256sums=('SKIP') 14 | 15 | pkgver() { 16 | cd "$srcdir/${pkgname%-git}" 17 | printf "%s" "$(git describe --long --tags | sed 's/v//;s/\([^-]*-\)g/r\1/;s/-/./g')" 18 | } 19 | 20 | prepare() { 21 | export RUSTUP_TOOLCHAIN=stable 22 | export CARGO_TARGET_DIR=target 23 | 24 | cd "$srcdir/${pkgname%-git}" 25 | cargo fetch --locked --target "$CARCH-unknown-linux-gnu" 26 | } 27 | 28 | build() { 29 | export RUSTUP_TOOLCHAIN=stable 30 | export CARGO_TARGET_DIR=target 31 | 32 | cd "$srcdir/${pkgname%-git}" 33 | cargo build --release --frozen 34 | strip target/release/falion 35 | } 36 | 37 | package() { 38 | cd "$srcdir/${pkgname%-git}" 39 | 40 | install -Dm755 target/release/falion -t "${pkgdir}/usr/bin/" 41 | 42 | install -Dm644 LICENSE -t "${pkgdir}/usr/share/licenses/${pkgname%-git}/" 43 | install -Dm644 README.md -t "${pkgdir}/usr/share/doc/${pkgname%-git}/" 44 | install -Dm0644 -t "$pkgdir/usr/share/applications/" "resources/linux/desktop/falion.desktop" 45 | for size in 16x16 32x32 64x64 128x128 256x256 512x512; do 46 | install -Dm0644 "resources/linux/desktop/icons/hicolor/$size/apps/falion.png" \ 47 | "$pkgdir/usr/share/icons/hicolor/$size/apps/falion.png" 48 | done 49 | } 50 | -------------------------------------------------------------------------------- /resources/linux/AUR/stable/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Obscurely 2 | pkgname=falion 3 | pkgver=PKGVER-PLACEHOLDER 4 | pkgrel=1 5 | pkgdesc="An open source, programmed in rust, privacy focused tool for scraping programming resources (like stackoverflow) fast, efficient and asynchronous/parallel using the CLI or GUI. " 6 | arch=('x86_64') 7 | url="https://github.com/Obscurely/falion" 8 | license=('GPL3') 9 | makedepends=('git' 'rust' 'cargo' 'binutils') 10 | source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver-stable.tar.gz") 11 | sha256sums=('SKIP') 12 | 13 | prepare() { 14 | cd "$srcdir/falion-$pkgver-stable" 15 | cargo fetch --locked --target "$CARCH-unknown-linux-gnu" 16 | } 17 | 18 | build() { 19 | cd "$srcdir/falion-$pkgver-stable" 20 | export RUSTUP_TOOLCHAIN=stable 21 | export CARGO_TARGET_DIR=target 22 | cargo build --release --frozen 23 | } 24 | 25 | package() { 26 | cd "$srcdir/falion-$pkgver-stable" 27 | install -Dm 755 "target/release/falion" -t "$pkgdir/usr/bin" 28 | install -Dm 644 README.md -t "$pkgdir/usr/share/doc/$pkgname" 29 | install -Dm0644 -t "$pkgdir/usr/share/applications/" "resources/linux/desktop/falion.desktop" 30 | for size in 16x16 32x32 64x64 128x128 256x256 512x512; do 31 | install -Dm0644 "resources/linux/desktop/icons/hicolor/$size/apps/falion.png" \ 32 | "$pkgdir/usr/share/icons/hicolor/$size/apps/falion.png" 33 | done 34 | } 35 | -------------------------------------------------------------------------------- /resources/linux/desktop/falion.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=falion 3 | Exec=falion 4 | Icon=falion 5 | Type=Application 6 | Categories=Utility 7 | -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/128x128/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/128x128/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/desktop/icons/hicolor/128x128/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/16x16/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/16x16/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/desktop/icons/hicolor/16x16/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/256x256/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/256x256/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/desktop/icons/hicolor/256x256/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/32x32/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/32x32/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/desktop/icons/hicolor/32x32/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/512x512/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/512x512/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/desktop/icons/hicolor/512x512/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/64x64/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/desktop/icons/hicolor/64x64/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/desktop/icons/hicolor/64x64/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/AppRun: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | HERE="$(dirname "$(readlink -f "${0}")")" 4 | EXEC="${HERE}/usr/bin/falion" 5 | 6 | exec "${EXEC}" "$@" 7 | -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/falion.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=falion 3 | Exec=falion 4 | Icon=falion 5 | Type=Application 6 | Categories=Utility 7 | -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/falion.AppDir/falion.png -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/bin/.gitkeep: -------------------------------------------------------------------------------- 1 | bin file for linux goes here. 2 | -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/128x128/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/128x128/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/falion.AppDir/usr/share/icons/hicolor/128x128/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/16x16/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/16x16/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/falion.AppDir/usr/share/icons/hicolor/16x16/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/256x256/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/256x256/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/falion.AppDir/usr/share/icons/hicolor/256x256/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/32x32/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/32x32/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/falion.AppDir/usr/share/icons/hicolor/32x32/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/512x512/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/512x512/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/falion.AppDir/usr/share/icons/hicolor/512x512/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/64x64/apps/.gitkeep: -------------------------------------------------------------------------------- 1 | resized logo goes here 2 | -------------------------------------------------------------------------------- /resources/linux/falion.AppDir/usr/share/icons/hicolor/64x64/apps/falion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/linux/falion.AppDir/usr/share/icons/hicolor/64x64/apps/falion.png -------------------------------------------------------------------------------- /resources/linux/falion.nix: -------------------------------------------------------------------------------- 1 | { lib, stdenv, appimageTools, desktop-file-utils, fetchurl }: 2 | 3 | let 4 | version = "VERSION_PLACEHOLDER"; 5 | name = "falion-${version}"; 6 | 7 | plat = { 8 | x86_64-linux = ""; 9 | }.${stdenv.hostPlatform.system}; 10 | 11 | sha256 = { 12 | x86_64-linux = "SHA_PLACEHOLDER"; 13 | }.${stdenv.hostPlatform.system}; 14 | 15 | src = fetchurl { 16 | url = "https://github.com/Obscurely/falion/releases/download/v${version}-stable/falion-linux.AppImage"; 17 | inherit sha256; 18 | }; 19 | 20 | appimageContents = appimageTools.extractType2 { 21 | inherit name src; 22 | }; 23 | in 24 | appimageTools.wrapType2 rec { 25 | inherit name src; 26 | 27 | extraInstallCommands = '' 28 | mkdir -p $out/share/pixmaps $out/share/licenses/falion 29 | cp ${appimageContents}/falion.png $out/share/pixmaps/ 30 | cp ${appimageContents}/falion.desktop $out 31 | cp ${appimageContents}/LICENSE $out/share/licenses/falion/LICENSE 32 | mv $out/bin/${name} $out/bin/falion 33 | ${desktop-file-utils}/bin/desktop-file-install --dir $out/share/applications \ 34 | --set-key Exec --set-value $out/bin/falion \ 35 | --set-key Comment --set-value "falion Linux" \ 36 | --delete-original $out/falion.desktop 37 | ''; 38 | 39 | meta = { 40 | homepage = "https://github.com/Obscurely/falion"; 41 | description = "An open source, programmed in rust, privacy focused tool for scraping programming resources (like stackoverflow) fast, efficient and asynchronous/parallel using the CLI or GUI. "; 42 | license = lib.licenses.mit; 43 | platforms = lib.platforms.linux; 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /resources/macos/falion.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 21G217 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | falion 11 | CFBundleIconFile 12 | AppIcon 13 | CFBundleIconName 14 | AppIcon 15 | CFBundleIdentifier 16 | com.falion.macos 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleLocalizations 20 | 21 | en 22 | 23 | CFBundleName 24 | falion 25 | CFBundlePackageType 26 | APPL 27 | CFBundleShortVersionString 28 | VERSION_PLACEHOLDER 29 | CFBundleSupportedPlatforms 30 | 31 | MacOSX 32 | 33 | CFBundleVersion 34 | 1 35 | DTCompiler 36 | com.apple.compilers.llvm.clang.1_0 37 | DTPlatformBuild 38 | 14A400 39 | DTPlatformName 40 | macosx 41 | DTPlatformVersion 42 | 12.3 43 | DTSDKBuild 44 | 21E226 45 | DTSDKName 46 | macosx12.3 47 | DTXcode 48 | 1401 49 | DTXcodeBuild 50 | 14A400 51 | LSMinimumSystemVersion 52 | 10.11 53 | NSAppTransportSecurity 54 | 55 | NSAllowsArbitraryLoads 56 | 57 | 58 | NSHumanReadableCopyright 59 | Copyright © 2024 Obscurely (github.com/Obscurely). All rights reserved. 60 | NSMainNibFile 61 | MainMenu 62 | NSPrincipalClass 63 | NSApplication 64 | 65 | 66 | -------------------------------------------------------------------------------- /resources/macos/falion.app/Contents/MacOS/.gitkeep: -------------------------------------------------------------------------------- 1 | macos executable goes here 2 | -------------------------------------------------------------------------------- /resources/macos/falion.app/Contents/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? 2 | -------------------------------------------------------------------------------- /resources/macos/falion.app/Contents/Resources/.gitkeep: -------------------------------------------------------------------------------- 1 | here goes AppIcon.icns (the logo) 2 | -------------------------------------------------------------------------------- /resources/macos/falion.app/Contents/Resources/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Obscurely/falion/83e8217141d78d497c7924d58dbd0f0fa67cd0c2/resources/macos/falion.app/Contents/Resources/AppIcon.icns -------------------------------------------------------------------------------- /resources/macos/falion.rb: -------------------------------------------------------------------------------- 1 | class Falion < Formula 2 | desc "An open source, programmed in rust, privacy focused tool for scraping programming resources (like stackoverflow) fast, efficient and asynchronous/parallel using the CLI or GUI. " 3 | homepage "https://github.com/Obscurely/falion" 4 | url "https://github.com/Obscurely/falion/releases/download/vVERSION_PLACEHOLDER-stable/falion-macos.tar.gz" 5 | sha256 "SHA_PLACEHOLDER" 6 | version "VERSION_PLACEHOLDER" 7 | 8 | def install 9 | bin.install "falion" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /resources/macos/make-dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | create-dmg \ 3 | --volname falion \ 4 | --volicon "../../assets/images/logo.icns" \ 5 | --hide-extension "falion.app" \ 6 | --background "../../assets/images/dmg-background.png" \ 7 | --window-size 600 450 \ 8 | --icon-size 94 \ 9 | --icon "falion.app" 141 249 \ 10 | --app-drop-link 458 249 \ 11 | ../../target/osx/falion-macos-installer.dmg \ 12 | ./falion.app 13 | -------------------------------------------------------------------------------- /resources/windows/License.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\deff0\nouicompat{\fonttbl{\f0\fnil\fcharset0 Arial;}{\f1\fnil\fcharset0 Courier New;}} 2 | {\*\generator Riched20 10.0.15063}\viewkind4\uc1 3 | \pard\sa180\fs24\lang9 Copyright (c) 2023 Obscurely\par 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\par 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\par 6 | \f1 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\f0\par 7 | } 8 | 9 | -------------------------------------------------------------------------------- /resources/windows/resources.rc: -------------------------------------------------------------------------------- 1 | tsfh ICON "assets\images\logo.ico" 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | format_code_in_doc_comments = true 3 | condense_wildcard_suffixes = true 4 | use_field_init_shorthand = true 5 | normalize_doc_attributes = true 6 | imports_granularity = "Module" 7 | normalize_comments = true 8 | reorder_impl_items = true 9 | use_try_shorthand = true 10 | newline_style = "Unix" 11 | format_strings = true 12 | wrap_comments = true 13 | -------------------------------------------------------------------------------- /scripts/data/DESC: -------------------------------------------------------------------------------- 1 | An open source, programmed in rust, privacy focused tool for scraping programming resources (like stackoverflow) fast, efficient and asynchronous/parallel using the CLI or GUI. 2 | -------------------------------------------------------------------------------- /scripts/data/PMAIL: -------------------------------------------------------------------------------- 1 | adrian.obscurely@protonmail.com 2 | -------------------------------------------------------------------------------- /scripts/data/SMAIL: -------------------------------------------------------------------------------- 1 | obscurely.message@protonmail.com 2 | -------------------------------------------------------------------------------- /scripts/lib/init_repo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from lib.update_logo import update_logo 4 | 5 | 6 | def init_repo(path_sep: str, cwd: str): 7 | # Directories we can ignore 8 | skip_dirs = [ 9 | f"{cwd}{path_sep}.git", 10 | f"{cwd}{path_sep}.mypy_cache", 11 | f"{cwd}{path_sep}assets", 12 | f"{cwd}{path_sep}fuzz{path_sep}artifacts", 13 | f"{cwd}{path_sep}fuzz{path_sep}corpus", 14 | f"{cwd}{path_sep}fuzz{path_sep}target", 15 | f"{cwd}{path_sep}scripts", 16 | f"{cwd}{path_sep}target", 17 | ] 18 | 19 | # Get all the files we want to manipulate with their full paths. 20 | target_files = [] 21 | 22 | for path, _, files in os.walk(cwd): 23 | stop = 0 24 | for dir in skip_dirs: 25 | if dir in path: 26 | stop = 1 27 | continue 28 | 29 | if stop == 1: 30 | continue 31 | 32 | for file in files: 33 | if ( 34 | file != "Cargo.lock" 35 | and ".png" not in file 36 | and ".ico" not in file 37 | and ".icns" not in file 38 | ): 39 | target_files.append(os.path.join(path, file)) 40 | 41 | # Get user name and repo name 42 | repo_name = "" 43 | user_name = "" 44 | with open(f"{cwd}{path_sep}.git{path_sep}config", "r") as f: 45 | content = f.read() 46 | user_name = content.split("url = https://github.com/")[1].split("/")[0] 47 | repo_name = content.split(f"url = https://github.com/{user_name}/")[1].split( 48 | "\n" 49 | )[0] 50 | 51 | # Get primary email address 52 | pmail = "" 53 | with open(f"{cwd}{path_sep}scripts{path_sep}data{path_sep}PMAIL", "r") as f: 54 | pmail = f.readlines()[0].replace("\n", "") 55 | 56 | # Get secondary email address 57 | smail = "" 58 | with open(f"{cwd}{path_sep}scripts{path_sep}data{path_sep}SMAIL", "r") as f: 59 | smail = f.readlines()[0].replace("\n", "") 60 | 61 | # Create a dictionary with the vars 62 | vars = { 63 | "CHANGEME_USER": user_name, 64 | "CHANGEME_NAME": repo_name, 65 | "CHANGEME_BIN": repo_name.lower(), 66 | "changeme_bin": repo_name.lower(), 67 | "CHANGEME_PMAIL": pmail, 68 | "CHANGEME_SMAIL": smail, 69 | } 70 | 71 | # Replace the content 72 | for file in target_files: 73 | content = "" 74 | with open(file, "r") as f: 75 | content = f.read() 76 | 77 | for var in vars: 78 | content = content.replace(var, vars[var]) 79 | 80 | with open(file, "w") as f: 81 | f.write(content) 82 | 83 | # Get all dirs list to rename the ones needed 84 | target_dirs = [] 85 | 86 | for path, dirs, files in os.walk(cwd): 87 | stop = 0 88 | for dir in skip_dirs: 89 | if dir in path: 90 | stop = 1 91 | continue 92 | 93 | if stop == 1: 94 | continue 95 | 96 | for dir in dirs: 97 | target_dirs.append(os.path.join(path, dir)) 98 | 99 | # Rename folders & files 100 | for file in target_files: 101 | file_split = file.split(path_sep) 102 | if "CHANGEME" in file_split[-1]: 103 | file_split[-1] = ( 104 | file_split[-1] 105 | .replace("CHANGEME_BIN", vars["CHANGEME_BIN"]) 106 | .replace("CHANGEME", vars["CHANGEME_NAME"]) 107 | ) 108 | new_file = path_sep.join(file_split) 109 | os.rename(file, new_file) 110 | 111 | for dir in target_dirs: 112 | dir_split = dir.split(path_sep) 113 | if "CHANGEME" in dir_split[-1]: 114 | dir_split[-1] = dir_split[-1].replace("CHANGEME", vars["CHANGEME_NAME"]) 115 | new_dir = path_sep.join(dir_split) 116 | os.rename(dir, new_dir) 117 | 118 | # Run update logo to place the default logo in the project 119 | update_logo(path_sep, cwd) 120 | -------------------------------------------------------------------------------- /scripts/lib/update_desc.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def update_desc(path_sep: str, cwd: str): 5 | # Load desc 6 | desc = "" 7 | with open(f"scripts{path_sep}data{path_sep}DESC", "r") as f: 8 | desc = f.read().replace("\n", "") 9 | 10 | # Directories we can ignore 11 | skip_dirs = [ 12 | f"{cwd}{path_sep}.git", 13 | f"{cwd}{path_sep}.mypy_cache", 14 | f"{cwd}{path_sep}assets", 15 | f"{cwd}{path_sep}fuzz{path_sep}artifacts", 16 | f"{cwd}{path_sep}fuzz{path_sep}corpus", 17 | f"{cwd}{path_sep}fuzz{path_sep}target", 18 | f"{cwd}{path_sep}scripts", 19 | f"{cwd}{path_sep}target", 20 | ] 21 | 22 | # Get all the files we want to manipulate with their full paths. 23 | target_files = [] 24 | 25 | for path, _, files in os.walk(cwd): 26 | stop = 0 27 | for dir in skip_dirs: 28 | if dir in path: 29 | stop = 1 30 | continue 31 | 32 | if stop == 1: 33 | continue 34 | 35 | for file in files: 36 | if ( 37 | file != "Cargo.lock" 38 | and ".png" not in file 39 | and ".ico" not in file 40 | and ".icns" not in file 41 | ): 42 | target_files.append(os.path.join(path, file)) 43 | 44 | for file in target_files: 45 | content = "" 46 | 47 | with open(file, "r") as f: 48 | content = f.read() 49 | 50 | content = content.replace("CHANGEME_DESC", desc) 51 | 52 | with open(file, "w") as f: 53 | f.write(content) 54 | -------------------------------------------------------------------------------- /scripts/lib/update_logo.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import shutil 3 | 4 | from PIL import Image 5 | 6 | 7 | def update_logo(path_sep: str, cwd: str): 8 | # Get bin name 9 | bin_name = "" 10 | with open(f"{cwd}{path_sep}.git{path_sep}config", "r") as f: 11 | content = f.read() 12 | user_name = content.split("url = https://github.com/")[1].split("/")[0] 13 | bin_name = ( 14 | content.split(f"url = https://github.com/{user_name}/")[1] 15 | .split("\n")[0] 16 | .lower() 17 | ) 18 | 19 | # Open base logo png file 20 | logo = Image.open(f"assets{path_sep}images{path_sep}logo.png") 21 | 22 | # Save it as ico 23 | logo.save( 24 | f"assets{path_sep}images{path_sep}logo.ico", format="ICO", size=[(256, 256)] 25 | ) 26 | 27 | # Save it as icns 28 | logo.save( 29 | f"assets{path_sep}images{path_sep}logo.icns", 30 | format="ICNS", 31 | size=[(256, 256, 2)], 32 | ) 33 | 34 | # Copy icns to macos folder 35 | macos_app_folder = glob.glob(f"resources{path_sep}macos{path_sep}*.app")[0] 36 | shutil.copyfile( 37 | f"assets{path_sep}images{path_sep}logo.icns", 38 | f"{macos_app_folder}{path_sep}Contents{path_sep}Resources" 39 | f"{path_sep}AppIcon.icns", 40 | ) 41 | 42 | # Copy png logo to appdir linux folder 43 | linux_appdir = glob.glob(f"resources{path_sep}linux{path_sep}*.AppDir")[0] 44 | shutil.copyfile( 45 | f"assets{path_sep}images{path_sep}logo.png", 46 | f"{linux_appdir}{path_sep}{bin_name}.png", 47 | ) 48 | 49 | # Resize png logo and copy to appdir & desktop folders 50 | sizes = [16, 32, 64, 128, 256, 512] 51 | 52 | # appdir folder 53 | with Image.open(f"assets{path_sep}images{path_sep}logo.png") as logo: 54 | for size in sizes: 55 | resized = logo.resize((size, size)) 56 | resized.save( 57 | f"{linux_appdir}{path_sep}usr{path_sep}share{path_sep}icons" 58 | f"{path_sep}hicolor{path_sep}{size}x{size}{path_sep}apps" 59 | f"{path_sep}{bin_name}.png" 60 | ) 61 | resized.save( 62 | f"resources{path_sep}linux{path_sep}desktop{path_sep}icons" 63 | f"{path_sep}hicolor{path_sep}{size}x{size}{path_sep}apps" 64 | f"{path_sep}{bin_name}.png" 65 | ) 66 | -------------------------------------------------------------------------------- /scripts/repo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import sys 4 | 5 | from lib.init_repo import init_repo 6 | from lib.update_desc import update_desc 7 | from lib.update_logo import update_logo 8 | 9 | # Get current directory 10 | cwd = os.getcwd() 11 | 12 | # Make sure we are in the root folder and not in the scripts folder 13 | # + get the platform path separator \ for Windows and / for Linux. 14 | path_sep = "" 15 | if platform.system() == "Windows": 16 | path_split = cwd.split("\\") 17 | path_sep = "\\" 18 | if "scripts" == path_split[-1]: 19 | path_split.pop() 20 | os.chdir("\\".join(path_split)) 21 | else: 22 | path_split = cwd.split("/") 23 | path_sep = "/" 24 | if "scripts" == path_split[-1]: 25 | path_split.pop() 26 | os.chdir("/".join(path_split)) 27 | 28 | # Reset the current directory 29 | cwd = os.getcwd() 30 | print(cwd) 31 | 32 | # get arguments 33 | try: 34 | run_arg = sys.argv[1] 35 | except IndexError: 36 | print( 37 | """ 38 | Available arguments: 39 | 40 | init - change variables according to your repo. 41 | ulogo - take assets/logo.png as base, convert it and copy it anywhere needed 42 | udesc - take the description in scripts/data/DESC and update it everywhere. 43 | """ 44 | ) 45 | exit() 46 | 47 | # Main script run 48 | if run_arg == "init": 49 | init_repo(path_sep, cwd) 50 | elif run_arg == "ulogo": 51 | update_logo(path_sep, cwd) 52 | elif run_arg == "udesc": 53 | update_desc(path_sep, cwd) 54 | else: 55 | print("Argument not recognized!") 56 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | {pkgs ? import {}}: 2 | pkgs.mkShell rec { 3 | buildInputs = with pkgs; [ 4 | llvmPackages_latest.llvm 5 | llvmPackages_latest.bintools 6 | zlib.out 7 | xorriso 8 | grub2 9 | llvmPackages_latest.lld 10 | rustc 11 | cargo 12 | rustfmt 13 | clippy 14 | cargo-audit # audit dependencies in order to scan for supply chain attacks 15 | cargo-fuzz # fuzzing tool 16 | cargo-deny # tool to deny crates based on checks. 17 | cargo-edit # manage cargo dependencies 18 | cargo-deb # pkg rust apps for debian 19 | cmake 20 | git 21 | gcc 22 | pkg-config 23 | python311 24 | python311Packages.pillow # this is for python repo script 25 | openssl 26 | # falion specific, for ui (iced) 27 | libxkbcommon 28 | libGL 29 | # WINIT_UNIX_BACKEND=wayland 30 | wayland 31 | # WINIT_UNIX_BACKEND=x11 32 | xorg.libXcursor 33 | xorg.libXrandr 34 | xorg.libXi 35 | xorg.libX11 36 | # fonts 37 | fontconfig 38 | # Extra iced dependencies 39 | expat 40 | freetype 41 | freetype.dev 42 | pkgconfig 43 | ]; 44 | 45 | RUST_BACKTRACE = 1; 46 | # falion specific for ui (iced) 47 | LD_LIBRARY_PATH = builtins.foldl' (a: b: "${a}:${b}/lib") "${pkgs.vulkan-loader}/lib" buildInputs; 48 | } 49 | -------------------------------------------------------------------------------- /src/cli/content.rs: -------------------------------------------------------------------------------- 1 | use hashbrown::HashMap; 2 | use tokio::task::JoinHandle; 3 | 4 | type ResultsStaticType = Vec<(String, JoinHandle>)>; 5 | type ResultsDynType = Vec<(String, JoinHandle, E>>)>; 6 | 7 | /// Get dynamic type content (in form of a vector). Either await it if it wasn't already, it it was 8 | /// get it from the awaited list. 9 | /// 10 | /// # Arguments 11 | /// 12 | /// `results_ref` - mutable reference to results. 13 | /// `results_awaited_ref` - mutable reference to the object keeping already awaited resources. 14 | /// `results_index` - which result to get back, it's index. 15 | #[tracing::instrument(skip_all)] 16 | pub async fn get_dyn_result_content<'a, E>( 17 | results_ref: &'a mut Result, E>, 18 | results_awaited_ref: &'a mut HashMap>, 19 | results_index: usize, 20 | ) -> Option<&'a Vec> 21 | where 22 | E: std::fmt::Display, 23 | { 24 | match results_ref { 25 | Ok(res) => { 26 | if let Some(unawaited_res) = res.get_mut(results_index) { 27 | let (title, handle) = unawaited_res; 28 | if results_awaited_ref.contains_key(title) { 29 | match results_awaited_ref.get(title) { 30 | Some(res) => Some(res), 31 | None => None, 32 | } 33 | } else { 34 | let awaited = match handle.await { 35 | Ok(handled) => match handled { 36 | Ok(content) => content, 37 | Err(error) => { 38 | tracing::error!("There was an error getting the contetn for this a result. Error: {}", error); 39 | vec![format!("There has been an error getting the content for this result. Error: {}", error)] 40 | } 41 | }, 42 | Err(error) => { 43 | tracing::error!( 44 | "There was an error handeling the future for a result. Error: {}", 45 | error 46 | ); 47 | vec![format!("There has been an error handeling the future for this result. Error: {}", error)] 48 | } 49 | }; 50 | 51 | // save already awaited 52 | results_awaited_ref.insert(title.to_owned(), awaited); 53 | 54 | // unwrap is safe since we just inserted this element 55 | results_awaited_ref.get(title) 56 | } 57 | } else { 58 | None 59 | } 60 | } 61 | Err(error) => { 62 | tracing::info!( 63 | "User tryed accessing a resource that has been deemed unavailable. Error: {}", 64 | error 65 | ); 66 | None 67 | } 68 | } 69 | } 70 | 71 | /// Get the static content for a result (String type). Either await it or get it from the list of 72 | /// already awaited. 73 | /// 74 | /// # Arguments 75 | /// 76 | /// `results_ref` - mutable reference to results. 77 | /// `results_awaited_ref` - mutable reference to the object keeping already awaited resources. 78 | /// `results_index` - which result to get back, it's index. 79 | #[tracing::instrument(skip_all)] 80 | pub async fn get_static_result_content<'a, E>( 81 | results_ref: &'a mut Result, E>, 82 | results_awaited_ref: &'a mut HashMap, 83 | results_index: usize, 84 | ) -> Option<&'a String> 85 | where 86 | E: std::fmt::Display, 87 | { 88 | match results_ref { 89 | Ok(res) => { 90 | if let Some(unawaited_res) = res.get_mut(results_index) { 91 | let (title, handle) = unawaited_res; 92 | if results_awaited_ref.contains_key(title) { 93 | match results_awaited_ref.get(title) { 94 | Some(res) => Some(res), 95 | None => None, 96 | } 97 | } else { 98 | let awaited = match handle.await { 99 | Ok(handled) => match handled { 100 | Ok(content) => content, 101 | Err(error) => { 102 | tracing::error!("There was an error getting the contetn for this a result. Error: {}", error); 103 | format!("There has been an error getting the content for this result. Error: {}", error) 104 | } 105 | }, 106 | Err(error) => { 107 | tracing::error!( 108 | "There was an error handeling the future for a result. Error: {}", 109 | error 110 | ); 111 | format!("There has been an error handeling the future for this result. Error: {}", error) 112 | } 113 | }; 114 | 115 | // save already awaited 116 | results_awaited_ref.insert(title.to_owned(), awaited); 117 | 118 | // unwrap is safe since we just inserted this element 119 | results_awaited_ref.get(title) 120 | } 121 | } else { 122 | None 123 | } 124 | } 125 | Err(error) => { 126 | tracing::info!( 127 | "User tryed accessing a resource that has been deemed unavailable. Error: {}", 128 | error 129 | ); 130 | None 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/cli/print.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event; 2 | use crossterm::style; 3 | use crossterm::style::Stylize; 4 | use std::io::Write; 5 | use tokio::task::JoinHandle; 6 | 7 | type ResultsType = Vec<(String, JoinHandle>)>; 8 | 9 | /// Print the given print followed by the title of the given index result. 10 | /// 11 | /// # Arguments 12 | /// 13 | /// `stdout` - std::io::stdout() you should have one in main you reference across functions. It's 14 | /// used to manipulate the terminal. 15 | /// `resource_index` - The index of the given resource to print. 16 | /// `resource_results` - Actual results of the resource you want to print. 17 | #[tracing::instrument(skip_all)] 18 | pub fn print_resource( 19 | stdout: &mut std::io::Stdout, 20 | resource_index: usize, 21 | resource_print: &str, 22 | resource_results: &Result, S>, 23 | ) where 24 | S: std::string::ToString, 25 | { 26 | match resource_results { 27 | Ok(results) => { 28 | // get the current result 29 | let current_result = match results.get(resource_index) { 30 | Some(res) => res, 31 | None => { 32 | // this should never happen 33 | super::util::clean(stdout); 34 | tracing::error!("User tried to get content from a resource with a index that is lower than the lenth - 1 of the resource, but for some reason it still failed!"); 35 | panic!("This should never have happened. Please create a new issue on github and post latest.log file.") 36 | } 37 | }; 38 | // display the current result with the given print 39 | if let Err(error) = crossterm::queue!( 40 | stdout, 41 | style::PrintStyledContent( 42 | (resource_print.to_string() + ¤t_result.0).stylize() 43 | ), 44 | style::Print("\n\r") 45 | ) { 46 | tracing::warn!("There was an error printing some text. Error: {}", error); 47 | } 48 | } 49 | Err(error) => { 50 | if let Err(error) = crossterm::queue!( 51 | stdout, 52 | style::PrintStyledContent(error.to_string().red()), 53 | style::Print("\n\r") 54 | ) { 55 | tracing::warn!("There was an error printing some text. Error: {}", error); 56 | } 57 | } 58 | } 59 | } 60 | 61 | /// Create a screen similar to the cli one where you go through a content that is iterable. 62 | /// 63 | /// # Arguments 64 | /// 65 | /// `stdout` - std::io::stdout() you should have one in main you reference across functions. It's 66 | /// used to manipulate the terminal. 67 | /// `content` - the iterable content to display 68 | /// `is_thread` - Specify if the content is thread type, so the first element is gonna be tagged as 69 | /// question and the rest as answers, if not each element is gonna be tagged a file. 70 | #[tracing::instrument(skip_all)] 71 | pub fn print_dyn_content( 72 | stdout: &mut std::io::Stdout, 73 | content: &[String], 74 | is_thread: bool, 75 | ) -> bool { 76 | let mut current_index = 0; 77 | let max_index = content.len() - 1; 78 | // depending on is_thread set to either question or file 1 as for the first element. 79 | let question_title = if is_thread { 80 | "Question:".green().bold() 81 | } else { 82 | "File 1:".green().bold() 83 | }; 84 | // cli for the given content 85 | loop { 86 | // print content 87 | let content = match content.get(current_index) { 88 | // replace \n to \n\r because in terminal raw mode a new line doesn't bring you the 89 | // beginning of the row, it only goes down one line literally. 90 | Some(content) => content.replace('\n', "\n\r"), 91 | None => "There has been error getting the contents for this result".to_string(), 92 | }; 93 | // print first element tag or not 94 | if current_index == 0 { 95 | if let Err(error) = crossterm::queue!( 96 | stdout, 97 | style::PrintStyledContent(question_title), 98 | style::Print("\n\r\n\r") 99 | ) { 100 | tracing::warn!( 101 | "There was an error printing the title of thread's current entry. Error: {}", 102 | error 103 | ); 104 | } 105 | 106 | if let Err(error) = crossterm::queue!(stdout, style::Print(content)) { 107 | tracing::warn!( 108 | "There was an error printing a thread's content. Error: {}", 109 | error 110 | ); 111 | } 112 | } else { 113 | if let Err(error) = crossterm::queue!( 114 | stdout, 115 | style::PrintStyledContent(format!("Answer {}:", current_index).green()), 116 | style::Print("\n\r\n\r") 117 | ) { 118 | tracing::warn!( 119 | "There was an error printing the title of thread's current entry. Error: {}", 120 | error 121 | ); 122 | } 123 | 124 | if let Err(error) = crossterm::queue!(stdout, style::Print(content)) { 125 | tracing::warn!( 126 | "There was an error printing a thread's content. Error: {}", 127 | error 128 | ); 129 | } 130 | } 131 | 132 | // flush stdout queued commands 133 | if let Err(error) = stdout.flush() { 134 | tracing::warn!( 135 | "There was an error flushing stdout in order to print thread's content. Error: {}", 136 | error 137 | ); 138 | } 139 | 140 | // listen for key presses 141 | let event_read = match event::read() { 142 | Ok(ev) => ev, 143 | Err(error) => { 144 | tracing::warn!("There was an error reading the input event... going to the next iteration. If this continue please post an issue on github with the specific log file. Error: {}", error); 145 | continue; 146 | } 147 | }; 148 | 149 | match event_read { 150 | // go to next content 151 | event::Event::Key(event::KeyEvent { 152 | code: event::KeyCode::Char('n'), 153 | kind: event::KeyEventKind::Press, 154 | modifiers: event::KeyModifiers::NONE, 155 | .. 156 | }) => { 157 | if current_index < max_index { 158 | current_index += 1; 159 | } 160 | } 161 | // go to previous content 162 | event::Event::Key(event::KeyEvent { 163 | code: event::KeyCode::Char('N'), 164 | kind: event::KeyEventKind::Press, 165 | modifiers: event::KeyModifiers::SHIFT, 166 | .. 167 | }) => { 168 | current_index = current_index.saturating_sub(1); 169 | } 170 | event::Event::Key(event::KeyEvent { 171 | code: event::KeyCode::Char('q'), 172 | kind: event::KeyEventKind::Press, 173 | modifiers: event::KeyModifiers::NONE, 174 | .. 175 | }) => { 176 | return false; 177 | } 178 | event::Event::Key(event::KeyEvent { 179 | code: event::KeyCode::Char('c'), 180 | kind: event::KeyEventKind::Press, 181 | modifiers: event::KeyModifiers::CONTROL, 182 | .. 183 | }) => { 184 | tracing::info!("Exit app on user command!"); 185 | return true; 186 | } 187 | _ => (), 188 | } 189 | 190 | // clear terminal 191 | super::util::clear_terminal(stdout); 192 | } 193 | } 194 | 195 | /// Create a screen similar to the cli one to print static content. Content that is only one page. 196 | /// 197 | /// # Arguments 198 | /// 199 | /// `stdout` - std::io::stdout() you should have one in main you reference across functions. It's 200 | /// used to manipulate the terminal. 201 | /// `content` - the content to create the cli for. 202 | #[tracing::instrument(skip_all)] 203 | pub fn print_static_content(stdout: &mut std::io::Stdout, content: &str) -> bool { 204 | // replace \n to \n\r because in terminal raw mode a new line doesn't bring you the 205 | // beginning of the row, it only goes down one line literally. 206 | let content = content.replace('\n', "\n\r"); 207 | let page_title = "Page:".green().bold(); 208 | loop { 209 | // print content 210 | if let Err(error) = crossterm::queue!( 211 | stdout, 212 | style::PrintStyledContent(page_title), 213 | style::Print("\n\r\n\r") 214 | ) { 215 | tracing::warn!( 216 | "There was an error printing the title of thread's current entry. Error: {}", 217 | error 218 | ); 219 | } 220 | 221 | if let Err(error) = crossterm::queue!(stdout, style::Print(&content)) { 222 | tracing::warn!( 223 | "There was an error printing a thread's content. Error: {}", 224 | error 225 | ); 226 | } 227 | 228 | if let Err(error) = stdout.flush() { 229 | tracing::warn!( 230 | "There was an error flushing stdout in order to print thread's content. Error: {}", 231 | error 232 | ); 233 | } 234 | 235 | let event_read = match event::read() { 236 | Ok(ev) => ev, 237 | Err(error) => { 238 | tracing::warn!("There was an error reading the input event... going to the next iteration. If this continue please post an issue on github with the specific log file. Error: {}", error); 239 | continue; 240 | } 241 | }; 242 | 243 | // listen to key presses 244 | match event_read { 245 | // return to main menu 246 | event::Event::Key(event::KeyEvent { 247 | code: event::KeyCode::Char('q'), 248 | kind: event::KeyEventKind::Press, 249 | modifiers: event::KeyModifiers::NONE, 250 | .. 251 | }) => { 252 | return false; 253 | } 254 | // quit app 255 | event::Event::Key(event::KeyEvent { 256 | code: event::KeyCode::Char('c'), 257 | kind: event::KeyEventKind::Press, 258 | modifiers: event::KeyModifiers::CONTROL, 259 | .. 260 | }) => { 261 | tracing::info!("Exit app on user command!"); 262 | return true; 263 | } 264 | _ => (), 265 | } 266 | 267 | // clear terminal 268 | super::util::clear_terminal(stdout); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/cli/util.rs: -------------------------------------------------------------------------------- 1 | use crate::util::setup_logs; 2 | use clap::Parser; 3 | use crossterm::terminal; 4 | use std::io::Write; 5 | 6 | /// Reset the terminal basically. Disable raw mode, reset colors, show cursor, clear screen 7 | /// scroll up the terminal, move the cursor to the beginning. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `stdout` - std::io::stdout(), you should have one in main that you reference to all your 12 | /// functions for ideal performance and queue commands to it. 13 | #[tracing::instrument(skip_all)] 14 | pub fn clean(stdout: &mut std::io::Stdout) { 15 | if let Err(error) = crossterm::terminal::disable_raw_mode() { 16 | tracing::warn!("Failed to disable termial raw mode! Error: {}", error); 17 | } 18 | if let Err(error) = crossterm::queue!(stdout, crossterm::style::ResetColor) { 19 | tracing::warn!("Failed to reset term collor. Error: {}", error); 20 | } 21 | if let Err(error) = crossterm::queue!(stdout, crossterm::cursor::Show) { 22 | tracing::warn!("Failed to show back cursor. Error: {}", error); 23 | } 24 | if let Err(error) = crossterm::queue!(stdout, terminal::Clear(terminal::ClearType::Purge)) { 25 | tracing::warn!("Failed to clear terminal. Error: {}", error); 26 | } 27 | if let Err(error) = crossterm::queue!(stdout, terminal::ScrollUp(u16::MAX)) { 28 | tracing::warn!("Failed to scroll up the terminal. Error: {}", error); 29 | } 30 | if let Err(error) = crossterm::queue!(stdout, crossterm::cursor::MoveTo(0, 0)) { 31 | tracing::warn!("Failed to move terminal cursor. Error: {}", error); 32 | } 33 | 34 | if let Err(error) = stdout.flush() { 35 | tracing::warn!("Failed to flush stdout. Error: {}", error); 36 | }; 37 | } 38 | 39 | /// Clear the terminal, scroll up, and move cursor to the beginning. 40 | /// 41 | /// # Arguments 42 | /// 43 | /// * `stdout` - std::io::stdout() you should have one in main that you reference to all your 44 | /// functions for ideal performance and queue commands to it. 45 | #[tracing::instrument(skip_all)] 46 | pub fn clear_terminal(stdout: &mut std::io::Stdout) { 47 | if let Err(error) = crossterm::queue!(stdout, terminal::Clear(terminal::ClearType::Purge)) { 48 | tracing::warn!("Failed to clear terminal. Error: {}", error); 49 | } 50 | if let Err(error) = crossterm::queue!(stdout, terminal::ScrollUp(u16::MAX)) { 51 | tracing::warn!("Failed to scroll up the terminal. Error: {}", error); 52 | } 53 | if let Err(error) = crossterm::queue!(stdout, crossterm::cursor::MoveTo(0, 0)) { 54 | tracing::warn!("Failed to move terminal cursor. Error: {}", error); 55 | } 56 | 57 | if let Err(error) = stdout.flush() { 58 | tracing::warn!("Failed to flush stdout. Error: {}", error); 59 | } 60 | } 61 | 62 | /// Setup the cli. Setup the arguments for bin, get the given values and panic if a query equal or 63 | /// long to 5 in length hasn't been given. Enable terminal raw mode, hide the cursor, setup 64 | /// logging. Create an std::io::Stdout instance. 65 | /// 66 | /// # Errors 67 | /// 68 | /// If the user hasn't provided a query shorter than 5 chars or none at all 69 | pub fn setup_cli() -> Result { 70 | // initiate cli 71 | let cli = super::Cli::parse(); 72 | 73 | // first check for ui 74 | if cli.ui { 75 | crate::ui::ui(); 76 | return Err(std::io::Error::new( 77 | std::io::ErrorKind::Other, 78 | "User chose to run gui from cli", 79 | )); 80 | } 81 | 82 | // next check if for keybinds 83 | if cli.keybinds { 84 | print_keybindings(); 85 | return Err(std::io::Error::new( 86 | std::io::ErrorKind::Other, 87 | "User chose to print the keybinds list", 88 | )); 89 | } 90 | 91 | // get values 92 | let query = cli.query.join(" "); 93 | let verbose = cli.verbose; 94 | let disable_logs = cli.disable_logs; 95 | 96 | // check if query is not shorter than 5 characters 97 | if query.len() < 5 { 98 | return Err(std::io::Error::new( 99 | std::io::ErrorKind::NotFound, 100 | "query shorter than 5 chars provided", 101 | )); 102 | } 103 | 104 | // Pre-setup 105 | // enable terminal raw mode 106 | if let Err(err) = terminal::enable_raw_mode() { 107 | panic!("Failed to enable raw mode: {}", err); 108 | } 109 | 110 | // enable (or not) logs based on flag 111 | if !disable_logs { 112 | setup_logs(verbose); 113 | } 114 | 115 | Ok(query) 116 | } 117 | 118 | /// Simple println statement to print the keybinds for the cli 119 | pub fn print_keybindings() { 120 | let keybinds_list = r#" 121 | Keybinds list for falion. 122 | Note: where you see ".." it means from that to that i.e "1..5" would mean from 1 to 5. 123 | 124 | Main menu: 125 | [1..5] = Access that resource. 126 | SHIFT + [1..5] = Go to the next element in the list of that resource. 127 | ALT + [1..5] = Go to the previous element in the list of that resource. 128 | n = Move to the next element in the list of every resource. 129 | SHIFT + n = Move back to the previous element in the list of every resource. 130 | CTRL + c = Clear terminal and exit. 131 | 132 | Sub menus for the resources: 133 | n = Move to the next element in the content list (like questions & answers). 134 | SHIFT + n = Move back to the previous element in the content list. 135 | q = Go back to the main menu. 136 | CTRL + c = Clear terminal and exit. 137 | "#; 138 | println!("{keybinds_list}"); 139 | } 140 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod search; 2 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::{stdout, IsTerminal}; 2 | mod cli; 3 | mod search; 4 | mod ui; 5 | mod util; 6 | 7 | /// Main Falion execution 8 | #[tokio::main] 9 | async fn main() { 10 | // If the app is run from a terminal run the cli, otherwise the gui 11 | if stdout().is_terminal() { 12 | match util::is_parent_explorer() { 13 | Some(explorer) => { 14 | if explorer { 15 | util::hide_console_window(); 16 | ui::ui(); 17 | } else { 18 | cli::cli().await; 19 | } 20 | } 21 | None => cli::cli().await, 22 | } 23 | } else { 24 | ui::ui(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/search/ddg_search.rs: -------------------------------------------------------------------------------- 1 | use super::ddg; 2 | use super::util; 3 | use thiserror::Error; 4 | 5 | type DdgPage = Result; 6 | 7 | /// These are the errors the functions associated with DdgSearch will return. 8 | /// 9 | /// * `InvalidRequest` - Reqwest returned an error when processing the request. This can be 10 | /// due to rate limiting, bad internet etc. 11 | /// * `InvalidResponseBody` - The response content you got back is corrupted, usually bad 12 | /// internet. 13 | /// * `ErrorCode` - The website returned an error code 14 | /// * `DdgError` - error with getting results from DuckDuckGO. (ddg::DdgError) 15 | #[derive(Error, Debug)] 16 | pub enum DdgSearchError { 17 | #[error("Failed to make a request with the provided query/url: {0}")] 18 | InvalidRequest(reqwest::Error), 19 | #[error("A request has been successfully made, but there was an error getting the response body: {0}")] 20 | InvalidReponseBody(reqwest::Error), 21 | #[error("The request was successful, but the response wasn't 200 OK, it was: {0}")] 22 | ErrorCode(reqwest::StatusCode), 23 | #[error("There was an error retrieving search results from duckduckgo: {0}")] 24 | DdgError(ddg::DdgError), 25 | } 26 | 27 | /// Scrape pages returned by ddg 28 | #[derive(std::fmt::Debug)] 29 | pub struct DdgSearch { 30 | client: reqwest::Client, 31 | ddg: ddg::Ddg, 32 | } 33 | 34 | impl DdgSearch { 35 | /// Create a new DdgSearch instance with a custom client that generates UA (user-agent in 36 | /// order to avoid getting rate limited by DuckDuckGO). 37 | /// 38 | /// # Examples 39 | /// 40 | /// ``` 41 | /// use falion::search::ddg_search; 42 | /// 43 | /// let ddg_search = ddg_search::DdgSearch::new(); 44 | /// ``` 45 | pub fn new() -> Self { 46 | Self { 47 | client: util::client_with_special_settings(), 48 | ddg: ddg::Ddg::new(), 49 | } 50 | } 51 | 52 | /// Create a new DdgSearch instance with a provided client. 53 | /// Note: DuckDuckGO will limit your requests if you don't provide a user-agent. 54 | /// 55 | /// ``` 56 | /// use falion::search::ddg_search; 57 | /// 58 | /// let ddg_search = ddg_search::DdgSearch::with_client(reqwest::Client::new()); 59 | /// ``` 60 | pub fn with_client(client: reqwest::Client) -> Self { 61 | Self { 62 | client: client.clone(), 63 | ddg: ddg::Ddg::with_client(client), 64 | } 65 | } 66 | 67 | /// Get the contents of a page inside a String. 68 | /// 69 | /// # Arguments 70 | /// 71 | /// * `page_url` - The absolute url to the page. 72 | /// 73 | /// # Examples 74 | /// 75 | /// ``` 76 | /// use falion::search::ddg; 77 | /// use falion::search::ddg_search; 78 | /// 79 | /// # async fn run() -> Result<(), ddg_search::DdgSearchError> { 80 | /// let ddg = ddg::Ddg::new(); 81 | /// let ddg_search = ddg_search::DdgSearch::new(); 82 | /// let link = &ddg.get_links("Rust basics", None, None, None, Some(1)).await.unwrap()[0]; 83 | /// 84 | /// let page_content = ddg_search.get_page_content(&link).await.unwrap(); 85 | /// # Ok(()) 86 | /// # } 87 | /// ``` 88 | /// 89 | /// # Errors 90 | /// 91 | /// returns ddg_search::DdgSearchError; 92 | /// 93 | /// * `InvalidRequest` - Reqwest returned an error when processing the request. This can be 94 | /// due to rate limiting, bad internet etc. 95 | /// * `InvalidResponseBody` - The response content you got back is corrupted, usually bad 96 | /// internet. 97 | /// * `InvalidPageContent` - Usually this means the content returned by the website is 98 | /// corrupted because it did return 200 OK. 99 | /// * `ErrorCode` - The website returned an error code 100 | #[tracing::instrument(skip_all)] 101 | pub async fn get_page_content(&self, page_url: &str) -> DdgPage { 102 | tracing::info!("Get page content for: {}", &page_url); 103 | // set term width 104 | let term_width: usize = match crossterm::terminal::size() { 105 | Ok(size) => size.0.into(), 106 | Err(_) => 100, 107 | }; 108 | 109 | // get page 110 | let response_body = match self.client.get(page_url).send().await { 111 | Ok(res) => { 112 | if res.status() != reqwest::StatusCode::OK { 113 | tracing::error!( 114 | "Get request to {} returned status code: {}", 115 | &page_url, 116 | &res.status() 117 | ); 118 | return Err(DdgSearchError::ErrorCode(res.status())); 119 | } 120 | 121 | match res.text().await { 122 | Ok(body) => body, 123 | Err(err) => { 124 | tracing::error!( 125 | "The response body recieved from {} is invalid. Error: {}", 126 | &page_url, 127 | &err 128 | ); 129 | return Err(DdgSearchError::InvalidReponseBody(err)); 130 | } 131 | } 132 | } 133 | Err(err) => { 134 | tracing::error!( 135 | "Failed to make a get request to {}. Error {}", 136 | &page_url, 137 | &err 138 | ); 139 | return Err(DdgSearchError::InvalidRequest(err)); 140 | } 141 | }; 142 | 143 | // return page 144 | Ok(util::html_to_text(&response_body, term_width)) 145 | } 146 | 147 | /// Search for results using duckduckgo and a provided query. This function will 148 | /// go through ALL of those results and crate a future for each one which will start getting 149 | /// the content asynchronously for ALL of them. Each of this Futures is associated with the 150 | /// title of the page and returned inside a Vec for preserved order. 151 | /// 152 | /// PLEASE READ: While setting a limit is optional, doing multiple requests to possibly the 153 | /// same site at once will probably get you rate limited. 154 | /// 155 | /// # Arguments 156 | /// 157 | /// * `query` - The query to search for. 158 | /// * `limit` - Optional, but doing multiple requests to possibly the same site at once will 159 | /// probably get you rate limited. A recommended value is something like 10 for enough results 160 | /// and still good results. 161 | /// 162 | /// # Examples 163 | /// 164 | /// ```no_run // don't run because it fails github code action 165 | /// use falion::search::ddg_search; 166 | /// 167 | /// # async fn run() -> Result<(), ddg_search::DdgSearchError> { 168 | /// let ddg_search = ddg_search::DdgSearch::new(); 169 | /// let page_content = ddg_search 170 | /// .get_multiple_pages_content("Rust basics", Some(1)) 171 | /// .await 172 | /// .unwrap(); 173 | /// 174 | /// for p in page_content { 175 | /// assert!(!p.1.await.unwrap().unwrap().is_empty()) 176 | /// } 177 | /// # Ok(()) 178 | /// # } 179 | /// ``` 180 | /// 181 | /// # Errors 182 | /// 183 | /// returns ddg_search::DdgSearchError; 184 | /// 185 | /// * `DdgError` - error with getting results from DuckDuckGO. (ddg::DdgError) 186 | /// 187 | /// First error is for duckduckgo, second is for the future hanle, third is for the actual 188 | /// page content 189 | #[tracing::instrument(skip_all)] 190 | pub async fn get_multiple_pages_content( 191 | &self, 192 | query: &str, 193 | limit: Option, 194 | ) -> Result)>, DdgSearchError> { 195 | tracing::info!("Get multiple pages and their content for search query: {} with a results limit of: {:#?}", &query, &limit); 196 | // get the links from duckduckgo 197 | let links = match self 198 | .ddg 199 | .get_links(query, None, Some(true), None, limit) 200 | .await 201 | { 202 | Ok(res) => res, 203 | Err(err) => return Err(DdgSearchError::DdgError(err)), 204 | }; 205 | 206 | // create a new Vec 207 | let mut pages_content = Vec::with_capacity(links.len()); 208 | 209 | // start looping through the links associating the page title and the joinhandle for 210 | // the future the scrapes the content of the page by inserting them togheter in the 211 | // Vec inside a tuple 212 | for link in links { 213 | // unwrap is safe here since ddg does all the checks 214 | let mut name = String::from(""); 215 | let domain = link.split_once("https://").unwrap().1; 216 | let domain = match domain.split_once('/') { 217 | Some(split) => { 218 | name = link.split('/').last().unwrap().replace('-', " "); 219 | split.1 220 | } 221 | None => domain, 222 | }; 223 | // let name = link.split('/').last().unwrap().replace('-', " "); 224 | let mut full_name = String::with_capacity(domain.len() + name.len() + 3); 225 | full_name.push_str(domain); 226 | full_name.push_str(" | "); 227 | full_name.push_str(&name); 228 | // insert page content 229 | let client = self.client.clone(); 230 | pages_content.push(( 231 | full_name, 232 | tokio::task::spawn(async move { 233 | Self::with_client(client).get_page_content(&link).await 234 | }), 235 | )); 236 | } 237 | 238 | // return the Vec 239 | Ok(pages_content) 240 | } 241 | } 242 | 243 | impl Default for DdgSearch { 244 | fn default() -> Self { 245 | DdgSearch::new() 246 | } 247 | } 248 | 249 | #[cfg(test)] 250 | mod tests { 251 | use super::*; 252 | use crate::search; 253 | use rand::Rng; 254 | use std::thread; 255 | use std::time::Duration; 256 | 257 | #[tokio::test] 258 | async fn test_get_ddg_page() { 259 | let client = search::util::client_with_special_settings(); 260 | let ddg_search = DdgSearch::with_client(client); 261 | 262 | let link = "https://www.rust-lang.org/learn"; 263 | 264 | let page_content = ddg_search.get_page_content(link).await.unwrap(); 265 | 266 | assert!(!page_content.is_empty()) 267 | } 268 | 269 | #[ignore] // ignore to pass github code actions, it work on local machine 270 | #[test] 271 | fn test_get_multiple_ddg_pages_content() { 272 | let test = async { 273 | // random sleep time to prevent rate limiting when testing 274 | thread::sleep(Duration::from_secs(rand::thread_rng().gen_range(0..5))); 275 | 276 | // actual function 277 | let client = util::client_with_special_settings(); 278 | let ddg_search = DdgSearch::with_client(client); 279 | 280 | let page_content = ddg_search 281 | .get_multiple_pages_content("Rust basics", Some(2)) 282 | .await 283 | .unwrap(); 284 | 285 | for p in page_content { 286 | assert!(!p.1.await.unwrap().unwrap().is_empty()) 287 | } 288 | }; 289 | 290 | tokio::runtime::Builder::new_current_thread() 291 | .enable_all() 292 | .build() 293 | .unwrap() 294 | .block_on(test) 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/search/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ddg; 2 | pub mod ddg_search; 3 | pub mod geeksforgeeks; 4 | pub mod github_gist; 5 | pub mod stackexchange; 6 | pub mod stackoverflow; 7 | pub mod util; 8 | -------------------------------------------------------------------------------- /src/search/util.rs: -------------------------------------------------------------------------------- 1 | use rand::distributions::DistString; 2 | use reqwest::header; 3 | 4 | /// Create a new reqwest client using a randomly generated user-agent. 5 | /// This is useful so you don't get limited by some websites like duckduckgo. 6 | /// 7 | /// # Examples 8 | /// 9 | /// ``` 10 | /// use falion::search::util; 11 | /// 12 | /// let client = util::client_with_special_settings(); 13 | /// ``` 14 | pub fn client_with_special_settings() -> reqwest::Client { 15 | let mut rng = rand::thread_rng(); 16 | 17 | // specific headers to avoid rate limiting 18 | let mut headers = header::HeaderMap::new(); 19 | headers.insert( 20 | "X-Forwarded-Host", 21 | header::HeaderValue::from_static("duckduckgo.com"), 22 | ); 23 | headers.insert( 24 | "Origin", 25 | header::HeaderValue::from_static("https://duckduckgo.com"), 26 | ); 27 | headers.insert( 28 | "Accept", 29 | header::HeaderValue::from_static( 30 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", 31 | ), 32 | ); 33 | headers.insert( 34 | "Accept-Language", 35 | header::HeaderValue::from_static("en-US,en;q=0.5"), 36 | ); 37 | headers.insert( 38 | "Accept-Encoding", 39 | header::HeaderValue::from_static("gzip, deflate, br"), 40 | ); 41 | headers.insert("DNT", header::HeaderValue::from_static("1")); 42 | headers.insert( 43 | "Upgrade-Insecure-Requests", 44 | header::HeaderValue::from_static("1"), 45 | ); 46 | 47 | let mut ua = String::with_capacity(27); 48 | ua.push_str("Mozilla/5.0"); 49 | ua.push_str(&rand::distributions::Alphanumeric.sample_string(&mut rng, 16)); 50 | 51 | reqwest::ClientBuilder::new() 52 | .user_agent(ua) 53 | .default_headers(headers) 54 | .brotli(true) 55 | .gzip(true) 56 | .deflate(true) 57 | .https_only(true) 58 | .build() 59 | .unwrap() 60 | } 61 | 62 | /// Converts html got from the web into readeable text inside a terminal. 63 | /// 64 | /// # Arguments 65 | /// 66 | /// * `html` - The html to convert. 67 | /// * `term_width` - The width of your terminal in order to properly display. 68 | /// 69 | /// # Examples 70 | /// 71 | /// ``` 72 | /// use falion::search::util; 73 | /// 74 | /// let text = "

Hello World!

"; 75 | /// assert_eq!(util::html_to_text(text, 50), "Hello World!\n"); 76 | /// ``` 77 | pub fn html_to_text(html: &str, term_width: usize) -> String { 78 | let mut text = html2text::from_read(html.as_bytes(), term_width); 79 | 80 | // remove any chunks of more than 2 new lines 81 | while text.contains("\n\n\n") { 82 | text = text.replace("\n\n\n", "\n\n"); 83 | } 84 | 85 | // return text 86 | text 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::*; 92 | 93 | #[test] 94 | fn test_html_to_text() { 95 | let text = "

Hello World!

"; 96 | assert_eq!(html_to_text(text, 50), "Hello World!\n"); 97 | } 98 | 99 | #[tokio::test] 100 | async fn test_create_client() { 101 | let client = client_with_special_settings(); 102 | 103 | client.get("https://google.com").send().await.unwrap(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ui/dyn_content/mod.rs: -------------------------------------------------------------------------------- 1 | mod button; 2 | use super::results; 3 | use super::results::ResultType; 4 | use super::util; 5 | use super::MainWindow; 6 | use super::ResultsDynType; 7 | use super::DYN_CONTENT_VIEW; 8 | use dashmap::DashMap; 9 | use slint::Weak; 10 | use std::sync::Arc; 11 | use tokio::sync::RwLock; 12 | 13 | /// Setup the button on which the result is printed which when is pressed brings you the view 14 | /// content screen. 15 | /// 16 | /// # Arguments 17 | /// 18 | /// * `ui` - weak pointer to the slint ui 19 | /// * `results` - ARC to the RwLock encapsulation of the Option for the results variable, from the main 20 | /// ui function. 21 | /// * `results_awaited` - ARC to the RwLock of the awaited results variable, from the main ui 22 | /// function. 23 | /// * `index` - ARC to the RwLock of the current results index for this particular resource 24 | /// * `results_type` - the kind of result this is. Ex: StackOverflow. 25 | /// 26 | /// # Panics 27 | /// 28 | /// It the results type is not made for this function 29 | #[tracing::instrument(skip_all)] 30 | pub fn setup_content_display( 31 | ui: Weak, 32 | results: Arc>>>, 33 | results_awaited: Arc>>, 34 | index: Arc>, 35 | content_index: Arc>, 36 | results_type: ResultType, 37 | ) where 38 | E: std::fmt::Display + std::marker::Send + 'static, 39 | F: std::fmt::Display + std::marker::Send + std::marker::Sync + 'static, 40 | { 41 | let ui_strong = util::get_ui(ui.clone()); 42 | 43 | // disable buttons 44 | ui_strong.set_enable_content_btns(false); 45 | 46 | // setup enter content 47 | match results_type { 48 | ResultType::StackOverflow => ui_strong.on_sof_enter(get_resource_enter_fn( 49 | ui.clone(), 50 | Arc::clone(&results), 51 | Arc::clone(&results_awaited), 52 | Arc::clone(&index), 53 | Arc::clone(&content_index), 54 | results_type, 55 | )), 56 | ResultType::StackExchange => ui_strong.on_se_enter(get_resource_enter_fn( 57 | ui.clone(), 58 | Arc::clone(&results), 59 | Arc::clone(&results_awaited), 60 | Arc::clone(&index), 61 | Arc::clone(&content_index), 62 | results_type, 63 | )), 64 | ResultType::GithubGist => ui_strong.on_gg_enter(get_resource_enter_fn( 65 | ui.clone(), 66 | Arc::clone(&results), 67 | Arc::clone(&results_awaited), 68 | Arc::clone(&index), 69 | Arc::clone(&content_index), 70 | results_type, 71 | )), 72 | _ => { 73 | tracing::error!("Results type used on a function that doesn't support it."); 74 | panic!("Results type used on function that doesn't support it. This is a programming error."); 75 | } 76 | } 77 | } 78 | 79 | /// The callback function for viewing the resource. 80 | /// 81 | /// # Arguments 82 | /// 83 | /// * `ui` - weak pointer to the slint ui 84 | /// * `results` - ARC to the RwLock encapsulation of the Option for the results variable, from the main 85 | /// ui function. 86 | /// * `results_awaited` - ARC to the RwLock of the awaited results variable, from the main ui 87 | /// function. 88 | /// * `index` - ARC to the RwLock of the current results index for this particular resource 89 | /// * `content_index` - the index of the item that should be displayed from the result 90 | /// * `results_type` - the kind of result this is. Ex: StackOverflow. 91 | /// 92 | /// # Panics 93 | /// 94 | /// If slint couldn't be invoked from the event loop. 95 | #[tracing::instrument(skip_all)] 96 | fn get_resource_enter_fn( 97 | ui: Weak, 98 | results: Arc>>>, 99 | results_awaited: Arc>>, 100 | index: Arc>, 101 | content_index: Arc>, 102 | results_type: ResultType, 103 | ) -> impl Fn() 104 | where 105 | E: std::fmt::Display + std::marker::Send + 'static, 106 | F: std::fmt::Display + std::marker::Send + std::marker::Sync + 'static, 107 | { 108 | move || { 109 | // clone necessary ARCs 110 | let results_clone = Arc::clone(&results); 111 | let index_clone = Arc::clone(&index); 112 | let content_index_clone = Arc::clone(&content_index); 113 | let results_awaited_clone = Arc::clone(&results_awaited); 114 | // clone ui weak pointer 115 | let ui = ui.clone(); 116 | 117 | // actual logic 118 | tokio::spawn(async move { 119 | // reset content index 120 | results::index::reset_result_index(Arc::clone(&content_index_clone)).await; 121 | // get locks 122 | let locked = futures::join!(results_clone.write(), index_clone.read(),); 123 | let mut results_lock = locked.0; 124 | let index_lock = locked.1; 125 | 126 | let content = match results_lock.as_mut() { 127 | Some(results) => match results { 128 | Ok(results) => match results.get_mut(*index_lock) { 129 | Some(result) => { 130 | // show the view dynamic content window 131 | let ui_clone = ui.clone(); 132 | if let Err(err) = slint::invoke_from_event_loop(move || { 133 | let ui = util::get_ui(ui_clone); 134 | 135 | ui.set_dyn_content("".into()); 136 | ui.set_view(DYN_CONTENT_VIEW); 137 | }) { 138 | util::slint_event_loop_panic(err); 139 | }; 140 | // get content 141 | match results_awaited_clone.get(&result.0) { 142 | Some(result) => result, 143 | None => { 144 | let (title, handle) = result; 145 | let awaited = match handle.await { 146 | Ok(handled) => match handled { 147 | Ok(content) => content, 148 | Err(error) => { 149 | tracing::error!("There was an error getting the contetn for this a result. Error: {}", error); 150 | vec![format!("There has been an error getting the content for this result. Error: {}", error)] 151 | } 152 | }, 153 | Err(error) => { 154 | tracing::error!( 155 | "There was an error handeling the future for a result. Error: {}", 156 | error 157 | ); 158 | vec![format!("There has been an error handeling the future for this result. Error: {}", error)] 159 | } 160 | }; 161 | 162 | // save already awaited 163 | results_awaited_clone.insert(title.to_owned(), awaited); 164 | 165 | // unwrap is safe since we just inserted this element 166 | results_awaited_clone.get(title).unwrap() 167 | } 168 | } 169 | } 170 | None => { 171 | tracing::warn!("User tried accessing a result at a non existen index which shouldn't have happened and it's a programming error if it does"); 172 | return; 173 | } 174 | }, 175 | Err(err) => { 176 | tracing::warn!("The results are an error and the user should have not been able to interact with them. Err: {}", err.to_string()); 177 | return; 178 | } 179 | }, 180 | None => { 181 | tracing::warn!("The results are non existen, yet the user still managed to try and access them."); 182 | return; 183 | } 184 | }; 185 | 186 | // set the first element 187 | let ui_clone = ui.clone(); 188 | // clone necessary ARCs 189 | let results_clone = Arc::clone(&results_clone); 190 | let index_clone = Arc::clone(&index_clone); 191 | let content_index_clone = Arc::clone(&content_index_clone); 192 | let results_awaited_clone = Arc::clone(&results_awaited_clone); 193 | // get first element 194 | let first = content.first().unwrap().to_owned(); 195 | // drop the Mutex locks 196 | drop(results_lock); 197 | drop(index_lock); 198 | if let Err(err) = slint::invoke_from_event_loop(move || { 199 | let ui_strong = util::get_ui(ui_clone); 200 | 201 | // set dynamic content first tag 202 | if results_type == ResultType::GithubGist { 203 | ui_strong.set_dyn_content_tag("File 1".into()); 204 | } else { 205 | ui_strong.set_dyn_content_tag("Question".into()); 206 | } 207 | 208 | // set dyn content 209 | ui_strong.set_dyn_content(first.into()); 210 | 211 | // setup back and next buttons 212 | // setup back content button 213 | ui_strong.on_dyn_back_enter(button::get_back_content_fn( 214 | ui.clone(), 215 | Arc::clone(&results_clone), 216 | Arc::clone(&results_awaited_clone), 217 | Arc::clone(&index_clone), 218 | Arc::clone(&content_index_clone), 219 | results_type, 220 | )); 221 | 222 | // setup next content button 223 | ui_strong.on_dyn_next_enter(button::get_next_content_fn( 224 | ui.clone(), 225 | Arc::clone(&results_clone), 226 | Arc::clone(&results_awaited_clone), 227 | Arc::clone(&index_clone), 228 | Arc::clone(&content_index_clone), 229 | results_type, 230 | )); 231 | 232 | // enable btns 233 | ui_strong.set_enable_content_btns(true); 234 | }) { 235 | util::slint_event_loop_panic(err); 236 | }; 237 | }); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/ui/results/display.rs: -------------------------------------------------------------------------------- 1 | use super::util; 2 | use super::MainWindow; 3 | use super::ResultType; 4 | use super::Results; 5 | use slint::Weak; 6 | use std::sync::Arc; 7 | use tokio::sync::RwLock; 8 | 9 | /// Display the first resould for the provided resource 10 | /// 11 | /// # Arguments 12 | /// 13 | /// * `ui` - weak pointer to the slint ui 14 | /// * `results` - ARC to the RwLock encapsulation of the Option for the results variable, from the main 15 | /// ui function. 16 | /// * `results_type` - the kind of result this is. Ex: StackOverflow. 17 | /// 18 | /// # Panics 19 | /// 20 | /// If it can't invoke the slint event loop. 21 | #[tracing::instrument(skip_all)] 22 | pub fn display_first_result( 23 | ui: Weak, 24 | results: &Results, 25 | results_type: ResultType, 26 | ) where 27 | E: std::fmt::Display, 28 | { 29 | match results { 30 | // unwrap is fine here since it would have been an error if there were no 31 | // results, so there is at least one 32 | Ok(results) => { 33 | let (title, _) = results.first().unwrap(); 34 | let res = slint::SharedString::from(title); 35 | if let Err(err) = slint::invoke_from_event_loop(move || { 36 | let ui = util::get_ui(ui); 37 | 38 | // display the result based on the results type 39 | // set the text, enable button, and the cycle buttons 40 | match results_type { 41 | ResultType::StackOverflow => { 42 | ui.set_sof_result(res); 43 | ui.set_is_sof(true); 44 | ui.set_is_sof_back(true); 45 | ui.set_is_sof_next(true); 46 | } 47 | ResultType::StackExchange => { 48 | ui.set_se_result(res); 49 | ui.set_is_se(true); 50 | ui.set_is_se_back(true); 51 | ui.set_is_se_next(true); 52 | } 53 | ResultType::GithubGist => { 54 | ui.set_gg_result(res); 55 | ui.set_is_gg(true); 56 | ui.set_is_gg_back(true); 57 | ui.set_is_gg_next(true); 58 | } 59 | ResultType::GeeksForGeeks => { 60 | ui.set_gfg_result(res); 61 | ui.set_is_gfg(true); 62 | ui.set_is_gfg_back(true); 63 | ui.set_is_gfg_next(true); 64 | } 65 | ResultType::DdgSearch => { 66 | ui.set_ddg_result(res); 67 | ui.set_is_ddg(true); 68 | ui.set_is_ddg_back(true); 69 | ui.set_is_ddg_next(true); 70 | } 71 | } 72 | }) { 73 | util::slint_event_loop_panic(err); 74 | }; 75 | } 76 | Err(err) => { 77 | let err = slint::SharedString::from(err.to_string()); 78 | // error depending on the results type 79 | match results_type { 80 | ResultType::StackOverflow => { 81 | tracing::warn!("There were no results for StackOverflow. Error {}", err); 82 | } 83 | ResultType::StackExchange => { 84 | tracing::warn!("There were no results for StackExchange. Error {}", err); 85 | } 86 | ResultType::GithubGist => { 87 | tracing::warn!("There were no results for GithubGist. Error {}", err); 88 | } 89 | ResultType::GeeksForGeeks => { 90 | tracing::warn!("There were no results for GeeksForGeeks. Error {}", err); 91 | } 92 | ResultType::DdgSearch => { 93 | tracing::warn!("There were no results for DdgSearch. Error {}", err); 94 | } 95 | } 96 | if let Err(err) = slint::invoke_from_event_loop(move || { 97 | let ui = util::get_ui(ui); 98 | 99 | // error depending on the results type 100 | match results_type { 101 | ResultType::StackOverflow => { 102 | ui.set_sof_result(err); 103 | } 104 | ResultType::StackExchange => { 105 | ui.set_sof_result(err); 106 | } 107 | ResultType::GithubGist => { 108 | ui.set_sof_result(err); 109 | } 110 | ResultType::GeeksForGeeks => { 111 | ui.set_sof_result(err); 112 | } 113 | ResultType::DdgSearch => { 114 | ui.set_sof_result(err); 115 | } 116 | } 117 | }) { 118 | util::slint_event_loop_panic(err); 119 | }; 120 | } 121 | } 122 | } 123 | 124 | /// Redisplay the result for the provide resource 125 | /// 126 | /// # Arguments 127 | /// 128 | /// * `ui` - weak pointer to the slint ui 129 | /// * `results` - ARC to the RwLock encapsulation of the Option for the results variable, from the main 130 | /// ui function. 131 | /// function. 132 | /// * `index` - ARC to the RwLock of the current results index for this particular resource 133 | /// * `results_type` - the kind of result this is. Ex: StackOverflow. 134 | /// 135 | /// # Panics 136 | /// 137 | /// If it can't invoke the slint event loop. 138 | #[tracing::instrument(skip_all)] 139 | pub fn redisplay_result( 140 | ui: Weak, 141 | results: Arc>>>, 142 | index: Arc>, 143 | results_type: ResultType, 144 | ) where 145 | E: std::fmt::Display, 146 | { 147 | if let Some(Ok(results)) = results.blocking_read().as_ref() { 148 | if let Some(res) = results.get(*index.blocking_read()) { 149 | let (title, _) = res; 150 | let res = slint::SharedString::from(title); 151 | if let Err(err) = slint::invoke_from_event_loop(move || { 152 | let ui = util::get_ui(ui); 153 | 154 | // redisplay results based on their type 155 | match results_type { 156 | ResultType::StackOverflow => { 157 | ui.set_sof_result(res); 158 | } 159 | ResultType::StackExchange => { 160 | ui.set_se_result(res); 161 | } 162 | ResultType::GithubGist => { 163 | ui.set_gg_result(res); 164 | } 165 | ResultType::GeeksForGeeks => { 166 | ui.set_gfg_result(res); 167 | } 168 | ResultType::DdgSearch => { 169 | ui.set_ddg_result(res); 170 | } 171 | } 172 | }) { 173 | util::slint_event_loop_panic(err); 174 | }; 175 | }; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/ui/results/helper.rs: -------------------------------------------------------------------------------- 1 | use super::util; 2 | use super::MainWindow; 3 | use slint::Weak; 4 | 5 | /// Disable the search text line edit in the ui 6 | /// 7 | /// # Arguments 8 | /// 9 | /// * `ui` - weak pointer to the slint ui 10 | /// 11 | /// # Panics 12 | /// 13 | /// If it can't invoke the slint event loop. 14 | #[tracing::instrument(skip_all)] 15 | pub fn disable_search(ui: Weak) { 16 | if let Err(err) = slint::invoke_from_event_loop(move || { 17 | let ui = util::get_ui(ui); 18 | 19 | ui.set_enable_search(false); 20 | }) { 21 | util::slint_event_loop_panic(err); 22 | }; 23 | } 24 | 25 | /// Enable the search text line edit in the ui 26 | /// 27 | /// # Arguments 28 | /// 29 | /// * `ui` - weak pointer to the slint ui 30 | /// 31 | /// # Panics 32 | /// 33 | /// If it can't invoke the slint event loop. 34 | #[tracing::instrument(skip_all)] 35 | pub fn enable_search(ui: Weak) { 36 | if let Err(err) = slint::invoke_from_event_loop(move || { 37 | let ui = util::get_ui(ui); 38 | 39 | ui.set_enable_search(true); 40 | }) { 41 | util::slint_event_loop_panic(err); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/results/index.rs: -------------------------------------------------------------------------------- 1 | use super::Results; 2 | use std::sync::Arc; 3 | use tokio::sync::RwLock; 4 | 5 | /// Reset usize variable to 0, in this case an index 6 | /// 7 | /// # Arguments 8 | /// 9 | /// * `index` - ARC to RwLock of a usize variable 10 | #[tracing::instrument(skip_all)] 11 | pub async fn reset_result_index(index: Arc>) { 12 | *index.write().await = 0 13 | } 14 | 15 | /// Try up the index based on if its smaller than the results max index 16 | /// 17 | /// # Arguments 18 | /// 19 | /// * `results` - ARC to the RwLock encapsulation of the Option for the results variable, from the main 20 | /// ui function. 21 | /// function. 22 | /// * `index` - ARC to the RwLock of the current results index for this particular resource 23 | /// 24 | /// # Panics 25 | /// 26 | /// If blocking lock can't be called on the RwLock which would happen in a context where blocking is 27 | /// not acceptable, like an async function or the callback for a button. 28 | #[tracing::instrument(skip_all)] 29 | pub fn try_up_index(results: Arc>>>, index: Arc>) { 30 | if let Some(Ok(results)) = results.blocking_read().as_ref() { 31 | let mut index = index.blocking_write(); 32 | if (*index) < results.len() - 1 { 33 | *index += 1; 34 | } 35 | } 36 | } 37 | 38 | /// Try substract one from a usize, in this case an index, untill it reaches 0 39 | /// 40 | /// # Arguments 41 | /// 42 | /// * `index` - ARC to RwLock of a usize variable 43 | /// 44 | /// # Panics 45 | /// 46 | /// If blocking lock can't be called on the RwLock which would happen in a context where blocking is 47 | /// not acceptable, like an async function or the callback for a button. 48 | #[tracing::instrument(skip_all)] 49 | pub fn try_down_index(index: Arc>) { 50 | let mut index = index.blocking_write(); 51 | // substract one untill we reach the minimum supported by the data type, in our case usize, 52 | // which is 0 53 | *index = index.saturating_sub(1); 54 | } 55 | -------------------------------------------------------------------------------- /src/ui/results/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod display; 2 | pub mod helper; 3 | pub mod index; 4 | use super::util; 5 | use super::MainWindow; 6 | use super::Results; 7 | use slint::Weak; 8 | use std::sync::Arc; 9 | use tokio::sync::RwLock; 10 | 11 | /// Possible results type, pretty self explanatory 12 | #[derive(Clone, Copy, PartialEq)] 13 | pub enum ResultType { 14 | StackOverflow, 15 | StackExchange, 16 | GithubGist, 17 | GeeksForGeeks, 18 | DdgSearch, 19 | } 20 | 21 | /// Reset the results ui elements. Disabling the buttons and removing any button text. 22 | /// 23 | /// # Arguments 24 | /// 25 | /// * `ui` - weak pointer to the slint ui (.as_weak()) 26 | /// 27 | /// # Panics 28 | /// 29 | /// If the slint can't be invoked from the event loop. 30 | #[tracing::instrument(skip_all)] 31 | pub fn reset_results(ui: Weak) { 32 | if let Err(err) = slint::invoke_from_event_loop(move || { 33 | let ui = util::get_ui(ui); 34 | 35 | let space_string = slint::SharedString::from(" "); 36 | 37 | ui.set_sof_result(space_string.clone()); 38 | ui.set_is_sof(false); 39 | ui.set_is_sof_back(false); 40 | ui.set_is_sof_next(false); 41 | 42 | ui.set_se_result(space_string.clone()); 43 | ui.set_is_se(false); 44 | ui.set_is_se_back(false); 45 | ui.set_is_se_next(false); 46 | 47 | ui.set_gg_result(space_string.clone()); 48 | ui.set_is_gg(false); 49 | ui.set_is_gg_back(false); 50 | ui.set_is_gg_next(false); 51 | 52 | ui.set_gfg_result(space_string.clone()); 53 | ui.set_is_gfg(false); 54 | ui.set_is_gfg_back(false); 55 | ui.set_is_gfg_next(false); 56 | 57 | ui.set_ddg_result(space_string.clone()); 58 | ui.set_is_ddg(false); 59 | ui.set_is_ddg_back(false); 60 | ui.set_is_ddg_next(false); 61 | 62 | ui.set_is_back(false); 63 | ui.set_is_next(false); 64 | ui.set_error(space_string); 65 | }) { 66 | util::slint_event_loop_panic(err); 67 | }; 68 | } 69 | 70 | /// Setup the buttons for the results: cycle through them and the results itself 71 | /// 72 | /// # Arguments 73 | /// 74 | /// * `ui` - weak pointer to the slint ui 75 | /// * `results` - ARC to the RwLock encapsulation of the Option for the results variable, from the main 76 | /// ui function. 77 | /// function. 78 | /// * `index` - ARC to the RwLock of the current results index for this particular resource 79 | /// * `results_type` - the kind of result this is. Ex: StackOverflow. 80 | /// 81 | /// # Panics 82 | /// 83 | /// If it can't invoke the slint event loop. 84 | #[tracing::instrument(skip_all)] 85 | pub fn setup_results_btns( 86 | ui: Weak, 87 | results: Arc>>>, 88 | index: Arc>, 89 | results_type: ResultType, 90 | ) where 91 | E: std::fmt::Display + std::marker::Send + std::marker::Sync + 'static, 92 | T: std::marker::Send + std::marker::Sync + 'static, 93 | { 94 | let ui_deref = util::get_ui(ui.clone()); 95 | let ui_clone = ui.clone(); 96 | 97 | // events 98 | let back_event = { 99 | tracing::info!("On sof back enter event hit."); 100 | // clone the necessary ARCs 101 | let results_clone = Arc::clone(&results); 102 | let index_clone = Arc::clone(&index); 103 | 104 | // actual closure 105 | move || { 106 | let results_clone = Arc::clone(&results_clone); 107 | let index_clone = Arc::clone(&index_clone); 108 | let ui_clone = ui_clone.clone(); 109 | tokio::task::spawn_blocking(move || { 110 | // try down the index by one 111 | index::try_down_index(Arc::clone(&index_clone)); 112 | 113 | // redisplay the result 114 | display::redisplay_result( 115 | ui_clone, 116 | Arc::clone(&results_clone), 117 | Arc::clone(&index_clone), 118 | results_type, 119 | ); 120 | 121 | // log the end of the function 122 | tracing::info!("Successfully backed the StaceOverflow results by one."); 123 | }); 124 | } 125 | }; 126 | 127 | let ui_clone = ui.clone(); 128 | let next_event = { 129 | tracing::info!("On sof next enter event hit."); 130 | // clone the necessary ARCs 131 | let results_clone = Arc::clone(&results); 132 | let index_clone = Arc::clone(&index); 133 | 134 | // actual closure 135 | move || { 136 | let results_clone = Arc::clone(&results_clone); 137 | let index_clone = Arc::clone(&index_clone); 138 | let ui_clone = ui_clone.clone(); 139 | 140 | tokio::task::spawn_blocking(move || { 141 | // try down the index by one 142 | index::try_up_index(Arc::clone(&results_clone), Arc::clone(&index_clone)); 143 | 144 | // redisplay the result 145 | display::redisplay_result( 146 | ui_clone, 147 | Arc::clone(&results_clone), 148 | Arc::clone(&index_clone), 149 | results_type, 150 | ); 151 | 152 | // log the end of the function 153 | tracing::info!("Successfully upped the StaceOverflow results by one."); 154 | }); 155 | } 156 | }; 157 | 158 | // seput events based on the results type and their respective callbacks 159 | match results_type { 160 | ResultType::StackOverflow => { 161 | ui_deref.on_sof_back_enter(back_event); 162 | ui_deref.on_sof_next_enter(next_event); 163 | } 164 | ResultType::StackExchange => { 165 | ui_deref.on_se_back_enter(back_event); 166 | ui_deref.on_se_next_enter(next_event); 167 | } 168 | ResultType::GithubGist => { 169 | ui_deref.on_gg_back_enter(back_event); 170 | ui_deref.on_gg_next_enter(next_event); 171 | } 172 | ResultType::GeeksForGeeks => { 173 | ui_deref.on_gfg_back_enter(back_event); 174 | ui_deref.on_gfg_next_enter(next_event); 175 | } 176 | ResultType::DdgSearch => { 177 | ui_deref.on_ddg_back_enter(back_event); 178 | ui_deref.on_ddg_next_enter(next_event); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/ui/static_content.rs: -------------------------------------------------------------------------------- 1 | use super::results::ResultType; 2 | use super::util; 3 | use super::MainWindow; 4 | use super::ResultsStaticType; 5 | use super::STATIC_CONTENT_VIEW; 6 | use dashmap::DashMap; 7 | use slint::Weak; 8 | use std::sync::Arc; 9 | use tokio::sync::RwLock; 10 | 11 | /// Sets up the callbacks in the ui for when a user clicks to see the content of a result. 12 | /// 13 | /// # Arguments 14 | /// 15 | /// * `ui` - weak pointer to the slint ui 16 | /// * `results` - ARC to the RwLock encapsulation of the Option for the results variable, from the main 17 | /// ui function. 18 | /// * `results_awaited` - ARC to the RwLock of the awaited results variable, from the main ui 19 | /// function. 20 | /// * `index` - ARC to the RwLock of the current results index for this particular resource 21 | /// * `results_type` - the kind of result this is. Ex: StackOverflow. 22 | /// 23 | /// # Panics 24 | /// 25 | /// It the results type is not made for this function 26 | #[tracing::instrument(skip_all)] 27 | pub fn setup_content_display( 28 | ui: Weak, 29 | results: Arc>>>, 30 | results_awaited: Arc>, 31 | index: Arc>, 32 | results_type: ResultType, 33 | ) where 34 | E: std::fmt::Display + std::marker::Send + 'static, 35 | F: std::fmt::Display + std::marker::Send + std::marker::Sync + 'static, 36 | { 37 | let ui_strong = util::get_ui(ui.clone()); 38 | 39 | // disaple buttons 40 | ui_strong.set_enable_content_btns(false); 41 | 42 | // setup enter content 43 | match results_type { 44 | ResultType::GeeksForGeeks => ui_strong.on_gfg_enter(get_resource_enter_fn( 45 | ui.clone(), 46 | Arc::clone(&results), 47 | Arc::clone(&results_awaited), 48 | Arc::clone(&index), 49 | )), 50 | ResultType::DdgSearch => ui_strong.on_ddg_enter(get_resource_enter_fn( 51 | ui.clone(), 52 | Arc::clone(&results), 53 | Arc::clone(&results_awaited), 54 | Arc::clone(&index), 55 | )), 56 | _ => { 57 | tracing::error!("Results type used on a function that doesn't support it."); 58 | panic!("Results type used on function that doesn't support it. This is a programming error."); 59 | } 60 | } 61 | } 62 | 63 | /// The callback function for viewing the resource. 64 | /// 65 | /// # Arguments 66 | /// 67 | /// * `ui` - weak pointer to the slint ui 68 | /// * `results` - ARC to the RwLock encapsulation of the Option for the results variable, from the main 69 | /// ui function. 70 | /// * `results_awaited` - ARC to the RwLock of the awaited results variable, from the main ui 71 | /// function. 72 | /// * `index` - ARC to the RwLock of the current results index for this particular resource 73 | /// 74 | /// # Panics 75 | /// 76 | /// If slint couldn't be invoked from the event loop. 77 | #[tracing::instrument(skip_all)] 78 | fn get_resource_enter_fn( 79 | ui: Weak, 80 | results: Arc>>>, 81 | results_awaited: Arc>, 82 | index: Arc>, 83 | ) -> impl Fn() 84 | where 85 | E: std::fmt::Display + std::marker::Send + 'static, 86 | F: std::fmt::Display + std::marker::Send + std::marker::Sync + 'static, 87 | { 88 | move || { 89 | // clone necessary ARCs 90 | let results_clone = Arc::clone(&results); 91 | let index_clone = Arc::clone(&index); 92 | let results_awaited_clone = Arc::clone(&results_awaited); 93 | // clone ui weak pointer 94 | let ui = ui.clone(); 95 | 96 | // actual logic 97 | tokio::spawn(async move { 98 | // get locks 99 | let locked = futures::join!(results_clone.write(), index_clone.read(),); 100 | let mut results_lock = locked.0; 101 | let index_lock = locked.1; 102 | 103 | // try get the content 104 | let content = match results_lock.as_mut() { 105 | Some(results) => match results { 106 | Ok(results) => match results.get_mut(*index_lock) { 107 | Some(result) => { 108 | // show the view static content window 109 | let ui_clone = ui.clone(); 110 | if let Err(err) = slint::invoke_from_event_loop(move || { 111 | let ui = util::get_ui(ui_clone); 112 | 113 | ui.set_static_content("".into()); 114 | ui.set_view(STATIC_CONTENT_VIEW); 115 | }) { 116 | util::slint_event_loop_panic(err); 117 | }; 118 | // get content 119 | match results_awaited_clone.get(&result.0) { 120 | Some(result) => result, 121 | None => { 122 | let (title, handle) = result; 123 | let awaited = match handle.await { 124 | Ok(handled) => match handled { 125 | Ok(content) => content, 126 | Err(error) => { 127 | tracing::error!("There was an error getting the contetn for this a result. Error: {}", error); 128 | format!("There has been an error getting the content for this result. Error: {}", error) 129 | } 130 | }, 131 | Err(error) => { 132 | tracing::error!( 133 | "There was an error handeling the future for a result. Error: {}", 134 | error 135 | ); 136 | format!("There has been an error handeling the future for this result. Error: {}", error) 137 | } 138 | }; 139 | 140 | // save already awaited 141 | results_awaited_clone.insert(title.to_owned(), awaited); 142 | 143 | // unwrap is safe since we just inserted this element 144 | results_awaited_clone.get(title).unwrap() 145 | } 146 | } 147 | } 148 | None => { 149 | tracing::warn!("User tried accessing a result at a non existen index which shouldn't have happened and it's a programming error if it does"); 150 | return; 151 | } 152 | }, 153 | Err(err) => { 154 | tracing::warn!("The results are an error and the user should have not been able to interact with them. Err: {}", err.to_string()); 155 | return; 156 | } 157 | }, 158 | None => { 159 | tracing::warn!("The results are non existen, yet the user still managed to try and access them."); 160 | return; 161 | } 162 | }; 163 | 164 | // set the first element 165 | let ui_clone = ui.clone(); 166 | // get owned data for content 167 | let content = content.to_owned(); 168 | // drop the RwLock locks 169 | drop(results_lock); 170 | drop(index_lock); 171 | if let Err(err) = slint::invoke_from_event_loop(move || { 172 | let ui_strong = util::get_ui(ui_clone); 173 | 174 | // set content tag 175 | ui_strong.set_static_content_tag("Page".into()); 176 | 177 | // set content 178 | ui_strong.set_static_content(content.into()); 179 | 180 | // enable btns 181 | ui_strong.set_enable_content_btns(true); 182 | 183 | // log done displaying 184 | tracing::info!("Displayed static resource."); 185 | }) { 186 | util::slint_event_loop_panic(err); 187 | }; 188 | }); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/ui/util.rs: -------------------------------------------------------------------------------- 1 | use slint::Weak; 2 | 3 | /// Get the strong pointer for the slint UI. 4 | /// 5 | /// # Arguments 6 | /// 7 | /// * `ui` - weak pointes to the ui (.as_weak()) 8 | /// 9 | /// # Panics 10 | /// 11 | /// If it can't upgrade the pointes to a strong one because it ussually means that the ui is not 12 | /// usable. 13 | #[tracing::instrument(skip_all)] 14 | pub fn get_ui(ui: Weak) -> T 15 | where 16 | T: slint::ComponentHandle, 17 | { 18 | match ui.upgrade() { 19 | Some(ui) => ui, 20 | None => { 21 | tracing::error!("Failed to get ui thread behind weak pointer."); 22 | // if the pointer to the UI is invalid probably the program is not working anymore. 23 | panic!("Failed to get pointer to ui."); 24 | } 25 | } 26 | } 27 | 28 | /// Code to run when needing to panic because slint couldn't be invoked from the event loop which 29 | /// usualy means the ui is not usable. 30 | /// 31 | /// # Panics 32 | /// 33 | /// When run 34 | #[tracing::instrument(skip_all)] 35 | pub fn slint_event_loop_panic(err: slint::EventLoopError) { 36 | tracing::error!("Failed to invoke slint from event loop. Error {}", err); 37 | // if we can't invoke slint from the event loop it's probably right to panic as 38 | // the program is not responding. 39 | panic!("Failed to invoke slint from event loop. Error {}", err); 40 | } 41 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::{fs::File, sync::Arc}; 3 | use tracing_subscriber::{filter, prelude::*}; 4 | 5 | /// Setup logging using tracing crate. 6 | /// 7 | /// # Arguments 8 | /// 9 | /// * `verbose` if set to true the stdout log output will also show debug information. Log written 10 | /// to file always have debug output option set. 11 | pub fn setup_logs(verbose: bool) { 12 | // get/create cache dir 13 | let cache_dir = match dirs::cache_dir() { 14 | Some(mut path) => { 15 | // put the logs in Temp folder from local appdata on windows 16 | #[cfg(windows)] 17 | { 18 | path.push("Temp"); 19 | } 20 | 21 | path.push("falion"); 22 | if let Err(error) = fs::create_dir_all(&path) { 23 | eprintln!("Failed to create cache dir. Error: {}", error); 24 | return; 25 | } 26 | 27 | path 28 | } 29 | None => { 30 | eprintln!("Failed to get cache dir!"); 31 | return; 32 | } 33 | }; 34 | // move the contents from latest.log (if any) to another file 35 | let latest_log = cache_dir.join("latest.log"); 36 | let older_log = 37 | cache_dir.join(chrono::Utc::now().format("%d-%m-%YT%H.%M.%S").to_string() + ".log"); 38 | if let Err(error) = fs::copy(latest_log, older_log) { 39 | if error.kind() != std::io::ErrorKind::NotFound { 40 | eprintln!( 41 | "Failed to copy latest log to another file. Error: {}", 42 | error 43 | ); 44 | return; 45 | } 46 | } 47 | 48 | // setup tracing subscriber 49 | let stdout_log = tracing_subscriber::fmt::layer().pretty(); 50 | 51 | // A layer that logs events to a file. 52 | // let latest_log = cache_dir. 53 | let file = match File::create(cache_dir.join("latest.log")) { 54 | Ok(file) => file, 55 | Err(error) => { 56 | eprintln!("Failed to create a latest.log file. Error: {:#?}", error); 57 | return; 58 | } 59 | }; 60 | let mut debug_log = tracing_subscriber::fmt::layer() 61 | .with_writer(Arc::new(file)) 62 | .with_line_number(true) 63 | .with_file(true) 64 | .with_thread_ids(true) 65 | .with_target(false); 66 | debug_log.set_ansi(false); 67 | 68 | // register the layers 69 | if !verbose { 70 | let filter_debug_log = filter::Targets::new() 71 | .with_target("falion", filter::LevelFilter::DEBUG) 72 | .with_target("hyper", filter::LevelFilter::WARN); 73 | let filter_stdout_log = filter::Targets::new() 74 | .with_target("falion", filter::LevelFilter::WARN) 75 | .with_target("falion::search", filter::LevelFilter::OFF) 76 | .with_target("hyper", filter::LevelFilter::WARN); 77 | tracing_subscriber::registry() 78 | .with( 79 | stdout_log 80 | // Add an `INFO` filter to the stdout logging layer 81 | .with_filter(filter_stdout_log) 82 | // Combine the filtered `stdout_log` layer with the 83 | // `debug_log` layer, producing a new `Layered` layer. 84 | .and_then(debug_log.with_filter(filter_debug_log)), // Add a filter to *both* layers that rejects spans and 85 | // events whose targets start with `metrics`. 86 | ) 87 | .init(); 88 | } else { 89 | let filter_debug_log = filter::Targets::new() 90 | .with_target("falion", filter::LevelFilter::DEBUG) 91 | .with_target("hyper", filter::LevelFilter::DEBUG); 92 | let filter_stdout_log = filter::Targets::new() 93 | .with_target("falion", filter::LevelFilter::DEBUG) 94 | .with_target("hyper", filter::LevelFilter::WARN); 95 | tracing_subscriber::registry() 96 | .with( 97 | stdout_log 98 | // Add an `INFO` filter to the stdout logging layer 99 | .with_filter(filter_stdout_log) 100 | // Combine the filtered `stdout_log` layer with the 101 | // `debug_log` layer, producing a new `Layered` layer. 102 | .and_then(debug_log.with_filter(filter_debug_log)), // Add a filter to *both* layers that rejects spans and 103 | // events whose targets start with `metrics`. 104 | ) 105 | .init(); 106 | } 107 | } 108 | 109 | /// Check if the parent process in explorer.exe on windows. 110 | #[cfg(windows)] 111 | pub fn is_parent_explorer() -> Option { 112 | use std::process::Command; 113 | // Use the "wmic" command to retrieve the parent process IDs 114 | let output = Command::new("wmic") 115 | .args(["process", "get", "ParentProcessId"]) 116 | .output() 117 | .ok()?; 118 | 119 | // Parse the output to extract the parent process ID 120 | let output_str = String::from_utf8_lossy(&output.stdout); 121 | let mut parent_id = output_str.trim().lines().rev(); 122 | // the third process from the back in the list is the actual parent process 123 | parent_id.next(); 124 | parent_id.next(); 125 | let parent_id = parent_id.next()?.trim(); 126 | 127 | let output = Command::new("wmic") 128 | .args([ 129 | "process", 130 | "where", 131 | format!("processId={}", parent_id).as_str(), 132 | "get", 133 | "name", 134 | ]) 135 | .output() 136 | .ok()?; 137 | 138 | if String::from_utf8_lossy(&output.stdout).contains("explorer.exe") { 139 | Some(true) 140 | } else { 141 | Some(false) 142 | } 143 | } 144 | 145 | /// Returns None since we don't need to get the parent process on linux 146 | #[cfg(not(windows))] 147 | pub fn is_parent_explorer() -> Option { 148 | None 149 | } 150 | 151 | #[cfg(windows)] 152 | pub fn hide_console_window() { 153 | use std::ptr; 154 | let window = unsafe { kernel32::GetConsoleWindow() }; 155 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633548%28v=vs.85%29.aspx 156 | if window != ptr::null_mut() { 157 | unsafe { user32::ShowWindow(window, winapi::um::winuser::SW_HIDE) }; 158 | } 159 | } 160 | 161 | #[cfg(not(windows))] 162 | pub fn hide_console_window() {} 163 | -------------------------------------------------------------------------------- /ui/dyn_content.slint: -------------------------------------------------------------------------------- 1 | // Copyright © SixtyFPS GmbH 2 | // SPDX-License-Identifier: MIT 3 | 4 | import { Button, HorizontalBox, VerticalBox, ScrollView } from "std-widgets.slint"; 5 | 6 | export component DynContent inherits Window { 7 | callback return-enter(); 8 | callback next-enter(); 9 | callback back-enter(); 10 | default-font-size: 24px; 11 | 12 | in property content-tag: "Content"; 13 | in property content; 14 | in property enable_btns: false; 15 | 16 | VerticalBox { 17 | HorizontalBox { 18 | back-btn := Button { 19 | text: " Back"; 20 | max-width: 50px; 21 | enabled: enable-btns; 22 | clicked => { 23 | root.return-enter(); 24 | } 25 | } 26 | text-content-tag := Text { 27 | text: root.content-tag; 28 | font-size: 28px; 29 | min-width: 260px; 30 | color: yellow; 31 | } 32 | } 33 | ScrollView { 34 | VerticalBox { 35 | content := Text { 36 | wrap: TextWrap.word-wrap; 37 | text: root.content; 38 | } 39 | } 40 | } 41 | HorizontalBox { 42 | btn-content-back := Button { 43 | text: ""; 44 | max-height: 30px; 45 | enabled: enable-btns; 46 | clicked => { 47 | root.back-enter(); 48 | } 49 | } 50 | spacing: 500px; 51 | btn-content-next := Button { 52 | text: ""; 53 | max-height: 30px; 54 | enabled: enable-btns; 55 | clicked => { 56 | root.next-enter(); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ui/main.slint: -------------------------------------------------------------------------------- 1 | // Copyright © SixtyFPS GmbH 2 | // SPDX-License-Identifier: MIT 3 | 4 | import { Results } from "./results.slint"; 5 | import { DynContent } from "./dyn_content.slint"; 6 | import { StaticContent } from "./static_content.slint"; 7 | 8 | import "../assets/fonts/RobotoMonoNerdFont-Regular.ttf"; 9 | 10 | export component MainWindow inherits Window { 11 | preferred-width: 1250px; 12 | preferred-height: 750px; 13 | min-width: 750px; 14 | min-height: 500px; 15 | default-font-size: 24px; 16 | default-font-family: "RobotoMono Nerd Font"; 17 | title: "Falion"; 18 | icon: @image-url("../assets/images/logo.png"); 19 | 20 | in property view: 0; 21 | 22 | callback query-enter(string); 23 | callback sof-enter(); 24 | callback sof-back-enter(); 25 | callback sof-next-enter(); 26 | callback se-enter(); 27 | callback se-back-enter(); 28 | callback se-next-enter(); 29 | callback gg-enter(); 30 | callback gg-back-enter(); 31 | callback gg-next-enter(); 32 | callback gfg-enter(); 33 | callback gfg-back-enter(); 34 | callback gfg-next-enter(); 35 | callback ddg-enter(); 36 | callback ddg-back-enter(); 37 | callback ddg-next-enter(); 38 | callback back-enter(); 39 | callback next-enter(); 40 | 41 | callback dyn-back-enter(); 42 | callback dyn-next-enter(); 43 | callback content-return-enter(); 44 | 45 | in property enable_search: true; 46 | 47 | in property sof-result: " "; 48 | in property se-result: " "; 49 | in property gg-result: " "; 50 | in property gfg-result: " "; 51 | in property ddg-result: " "; 52 | 53 | in property is-sof: false; 54 | in property is-sof-back: false; 55 | in property is-sof-next: false; 56 | 57 | in property is-se: false; 58 | in property is-se-back: false; 59 | in property is-se-next: false; 60 | 61 | in property is-gg: false; 62 | in property is-gg-back: false; 63 | in property is-gg-next: false; 64 | 65 | in property is-gfg: false; 66 | in property is-gfg-back: false; 67 | in property is-gfg-next: false; 68 | 69 | in property is-ddg: false; 70 | in property is-ddg-back: false; 71 | in property is-ddg-next: false; 72 | 73 | in property is-back: false; 74 | in property is-next: false; 75 | 76 | in property error: ""; 77 | 78 | in property dyn-content-tag: ""; 79 | in property dyn-content: ""; 80 | in property static-content-tag: ""; 81 | in property static-content: ""; 82 | in property enable_content_btns: false; 83 | 84 | VerticalLayout { 85 | if (view == 0) : Results { 86 | query-enter(text) => {query-enter(text);} 87 | sof-enter() => {sof-enter()} 88 | sof-back-enter() => {sof-back-enter();} 89 | sof-next-enter() => {sof-next-enter();} 90 | se-enter() => {se-enter()} 91 | se-back-enter() => {se-back-enter();} 92 | se-next-enter() => {se-next-enter();} 93 | gg-enter() => {gg-enter()} 94 | gg-back-enter() => {gg-back-enter();} 95 | gg-next-enter() => {gg-next-enter();} 96 | gfg-enter() => {gfg-enter()} 97 | gfg-back-enter() => {gfg-back-enter();} 98 | gfg-next-enter() => {gfg-next-enter();} 99 | ddg-enter() => {ddg-enter()} 100 | ddg-back-enter() => {ddg-back-enter();} 101 | ddg-next-enter() => {ddg-next-enter();} 102 | back-enter() => {back-enter();} 103 | next-enter() => {next-enter();} 104 | 105 | enable-search: enable-search; 106 | 107 | sof-result: sof-result; 108 | se-result: se-result; 109 | gg-result: gg-result; 110 | gfg-result: gfg-result; 111 | ddg-result: ddg-result; 112 | 113 | is-sof: is-sof; 114 | is-sof-back: is-sof-back; 115 | is-sof-next: is-sof-next; 116 | 117 | is-se: is-se; 118 | is-se-back: is-se-back; 119 | is-se-next: is-se-next; 120 | 121 | is-gg: is-gg; 122 | is-gg-back: is-gg-back; 123 | is-gg-next: is-gg-next; 124 | 125 | is-gfg: is-gfg; 126 | is-gfg-back: is-gfg-back; 127 | is-gfg-next: is-gfg-next; 128 | 129 | is-ddg: is-ddg; 130 | is-ddg-back: is-ddg-back; 131 | is-ddg-next: is-ddg-next; 132 | 133 | is-back: is-back; 134 | is-next: is-next; 135 | 136 | error: error; 137 | } 138 | if (view == 1) : DynContent { 139 | content-tag: dyn-content-tag; 140 | content: dyn-content; 141 | enable-btns: enable-content-btns; 142 | next-enter() => {dyn-next-enter();} 143 | back-enter() => {dyn-back-enter();} 144 | return-enter() => {content-return-enter();} 145 | } 146 | if (view == 2) : StaticContent { 147 | content-tag: static-content-tag; 148 | content: static-content; 149 | enable-btns: enable-content-btns; 150 | return-enter() => {content-return-enter();} 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /ui/results.slint: -------------------------------------------------------------------------------- 1 | // Copyright © SixtyFPS GmbH 2 | // SPDX-License-Identifier: MIT 3 | 4 | import { Button, LineEdit, HorizontalBox, VerticalBox } from "std-widgets.slint"; 5 | 6 | import "../assets/fonts/RobotoMonoNerdFont-Regular.ttf"; 7 | 8 | export component Results inherits Window { 9 | callback query-enter(string); 10 | callback sof-enter(); 11 | callback sof-back-enter(); 12 | callback sof-next-enter(); 13 | callback se-enter(); 14 | callback se-back-enter(); 15 | callback se-next-enter(); 16 | callback gg-enter(); 17 | callback gg-back-enter(); 18 | callback gg-next-enter(); 19 | callback gfg-enter(); 20 | callback gfg-back-enter(); 21 | callback gfg-next-enter(); 22 | callback ddg-enter(); 23 | callback ddg-back-enter(); 24 | callback ddg-next-enter(); 25 | callback back-enter(); 26 | callback next-enter(); 27 | 28 | in property enable-search: true; 29 | 30 | in property sof-result: " "; 31 | in property se-result: " "; 32 | in property gg-result: " "; 33 | in property gfg-result: " "; 34 | in property ddg-result: " "; 35 | 36 | in property is-sof: false; 37 | in property is-sof-back: false; 38 | in property is-sof-next: false; 39 | 40 | in property is-se: false; 41 | in property is-se-back: false; 42 | in property is-se-next: false; 43 | 44 | in property is-gg: false; 45 | in property is-gg-back: false; 46 | in property is-gg-next: false; 47 | 48 | in property is-gfg: false; 49 | in property is-gfg-back: false; 50 | in property is-gfg-next: false; 51 | 52 | in property is-ddg: false; 53 | in property is-ddg-back: false; 54 | in property is-ddg-next: false; 55 | 56 | in property is-back: false; 57 | in property is-next: false; 58 | 59 | in property error: ""; 60 | 61 | // window 62 | VerticalBox { 63 | min-width: 750px; 64 | min-height: 500px; 65 | HorizontalBox { 66 | text-search := LineEdit { 67 | max-height: 80px; 68 | min-height: 40px; 69 | font-size: 28px; 70 | placeholder-text: "What are you wondering about?"; 71 | enabled: true; 72 | accepted(text) => { 73 | if (enable-search) { 74 | root.query-enter(self.text); 75 | } 76 | } 77 | } 78 | btn-search := Button { 79 | min-width: 50px; 80 | max-height: 80px; 81 | min-height: 40px; 82 | icon: @image-url("../assets/images/logo.png"); 83 | enabled: text-search.text != "" && enable-search; 84 | clicked => { 85 | root.query-enter(text-search.text); 86 | } 87 | } 88 | } 89 | HorizontalBox { 90 | text-sof := Text { 91 | text: "(1) StackOverflow"; 92 | font-size: 24px; 93 | min-width: 260px; 94 | max-width: 260px; 95 | color: yellow; 96 | } 97 | btn-sof-back := Button { 98 | text: ""; 99 | max-width: 20px; 100 | max-height: 30px; 101 | enabled: root.is-sof-back; 102 | clicked => { 103 | root.sof-back-enter(); 104 | } 105 | } 106 | btn-sof-next := Button { 107 | text: ""; 108 | max-width: 20px; 109 | max-height: 30px; 110 | enabled: root.is-sof-next; 111 | clicked => { 112 | root.sof-next-enter(); 113 | } 114 | } 115 | btn-sof := Button { 116 | text: root.sof-result; 117 | max-height: 30px; 118 | enabled: root.is-sof; 119 | clicked => { 120 | root.sof-enter(); 121 | } 122 | } 123 | } 124 | HorizontalBox { 125 | text-se := Text { 126 | text: "(2) StackExchange"; 127 | font-size: 24px; 128 | min-width: 260px; 129 | max-width: 260px; 130 | color: yellow; 131 | } 132 | btn-se-back := Button { 133 | text: ""; 134 | max-width: 20px; 135 | max-height: 30px; 136 | enabled: root.is-se-back; 137 | clicked => { 138 | root.se-back-enter(); 139 | } 140 | } 141 | btn-se-next := Button { 142 | text: ""; 143 | max-width: 20px; 144 | max-height: 30px; 145 | enabled: root.is-se-next; 146 | clicked => { 147 | root.se-next-enter(); 148 | } 149 | } 150 | btn-se := Button { 151 | text: root.se-result; 152 | max-height: 30px; 153 | enabled: root.is-se; 154 | clicked => { 155 | root.se-enter(); 156 | } 157 | } 158 | } 159 | HorizontalBox { 160 | text-gg := Text { 161 | text: "(3) GitHub Gist"; 162 | font-size: 24px; 163 | min-width: 260px; 164 | max-width: 260px; 165 | color: yellow; 166 | } 167 | btn-gg-back := Button { 168 | text: ""; 169 | max-width: 20px; 170 | max-height: 30px; 171 | enabled: root.is-gg-back; 172 | clicked => { 173 | root.gg-back-enter(); 174 | } 175 | } 176 | btn-gg-next := Button { 177 | text: ""; 178 | max-width: 20px; 179 | max-height: 30px; 180 | enabled: root.is-gg-next; 181 | clicked => { 182 | root.gg-next-enter(); 183 | } 184 | } 185 | btn-gg := Button { 186 | text: root.gg-result; 187 | max-height: 30px; 188 | enabled: root.is-gg; 189 | clicked => { 190 | root.gg-enter(); 191 | } 192 | } 193 | } 194 | HorizontalBox { 195 | text-gfg := Text { 196 | text: "(4) GeeksForGeeks"; 197 | font-size: 24px; 198 | min-width: 260px; 199 | max-width: 260px; 200 | color: yellow; 201 | } 202 | btn-gfg-back := Button { 203 | text: ""; 204 | max-width: 20px; 205 | max-height: 30px; 206 | enabled: root.is-gfg-back; 207 | clicked => { 208 | root.gfg-back-enter(); 209 | } 210 | } 211 | btn-gfg-next := Button { 212 | text: ""; 213 | max-width: 20px; 214 | max-height: 30px; 215 | enabled: root.is-gfg-next; 216 | clicked => { 217 | root.gfg-next-enter(); 218 | } 219 | } 220 | btn-gfg := Button { 221 | text: root.gfg-result; 222 | max-height: 30px; 223 | enabled: root.is-gfg; 224 | clicked => { 225 | root.gfg-enter(); 226 | } 227 | } 228 | } 229 | HorizontalBox { 230 | text-dgg := Text { 231 | text: "(5) DuckDuckGO"; 232 | font-size: 24px; 233 | min-width: 260px; 234 | max-width: 260px; 235 | color: yellow; 236 | } 237 | btn-ddg-back := Button { 238 | text: ""; 239 | max-width: 20px; 240 | max-height: 30px; 241 | enabled: root.is-ddg-back; 242 | clicked => { 243 | root.ddg-back-enter(); 244 | } 245 | } 246 | btn-ddg-next := Button { 247 | text: ""; 248 | max-width: 20px; 249 | max-height: 30px; 250 | enabled: root.is-ddg-next; 251 | clicked => { 252 | root.ddg-next-enter(); 253 | } 254 | } 255 | btn-dgg := Button { 256 | text: root.ddg-result; 257 | max-height: 30px; 258 | enabled: root.is-ddg; 259 | clicked => { 260 | root.ddg-enter(); 261 | } 262 | } 263 | } 264 | HorizontalBox { 265 | text-error := Text { 266 | text: root.error; 267 | font-size: 24px; 268 | color: red; 269 | } 270 | } 271 | HorizontalBox { 272 | spacing: 300px; 273 | Text {} 274 | } 275 | HorizontalBox { 276 | btn-back := Button { 277 | max-height: 30px; 278 | enabled: root.is-back; 279 | text: " Back"; 280 | clicked => { 281 | root.back-enter(); 282 | } 283 | } 284 | spacing: 500px; 285 | btn-next := Button { 286 | max-height: 30px; 287 | enabled: root.is-next; 288 | text: "Next "; 289 | clicked => { 290 | root.next-enter(); 291 | } 292 | } 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /ui/static_content.slint: -------------------------------------------------------------------------------- 1 | // Copyright © SixtyFPS GmbH 2 | // SPDX-License-Identifier: MIT 3 | 4 | import { ScrollView, VerticalBox , HorizontalBox, Button } from "std-widgets.slint"; 5 | 6 | export component StaticContent inherits Window { 7 | callback return-enter(); 8 | 9 | in property content-tag: "Page"; 10 | in property content; 11 | in property enable-btns: false; 12 | 13 | VerticalBox { 14 | HorizontalBox { 15 | back-btn := Button { 16 | text: " Back"; 17 | max-width: 50px; 18 | enabled: enable-btns; 19 | clicked => { 20 | root.return-enter(); 21 | } 22 | } 23 | text-content-tag := Text { 24 | text: root.content-tag; 25 | font-size: 28px; 26 | min-width: 260px; 27 | color: yellow; 28 | } 29 | } 30 | ScrollView { 31 | VerticalBox { 32 | content := Text { 33 | wrap: TextWrap.word-wrap; 34 | text: root.content; 35 | } 36 | } 37 | } 38 | } 39 | } 40 | --------------------------------------------------------------------------------