├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE │ ├── bug_fix.md │ └── new_feature.md └── workflows │ ├── aur_update.yml │ ├── aur_update_git.yml │ └── github_metrics.yml ├── .gitignore ├── .gitmodules ├── ACKNOWLEDGMENTS.md ├── AUTHORS.md ├── CHANGELOG.md ├── CITATION.cff ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── SUPPORT.md ├── assets ├── css │ └── cavasik.css ├── fork_profile.png ├── icons │ ├── devel │ │ └── io.github.fsobolev.Cavasik-Workspace.svg │ ├── io.github.TheWisker.Cavasik-symbolic.svg │ ├── io.github.TheWisker.Cavasik.png │ ├── io.github.TheWisker.Cavasik.svg │ └── meson.build ├── io.github.TheWisker.Cavasik.desktop.in ├── io.github.TheWisker.Cavasik.gschema.xml ├── io.github.TheWisker.Cavasik.metainfo.xml.in ├── meson.build ├── metrics │ ├── base.svg │ ├── contributors.svg │ ├── languages.svg │ └── licenses.svg ├── profile.png └── screenshots │ ├── bars.png │ ├── bars_circle.png │ ├── direction_top.png │ ├── levels.png │ ├── main.png │ ├── mirror_column.png │ ├── mirror_inverted.png │ ├── mirror_normal.png │ ├── mirror_overlapping.png │ ├── particles.png │ ├── spine.png │ ├── wave_circle.png │ └── waves.png ├── aur ├── cavasik-git │ ├── .SRCINFO │ └── PKGBUILD └── cavasik │ ├── .SRCINFO │ └── PKGBUILD ├── cavasik.doap ├── io.github.TheWisker.Cavasik.json ├── lang ├── CONTRIBUTING.md ├── CREDITS.json ├── LINGUAS ├── POTFILES.in ├── cavasik.pot ├── de.po ├── es.po ├── fr.po ├── it.po ├── jp.po ├── meson.build ├── nl.po ├── pt_BR.po └── ru.po ├── meson.build ├── src ├── __init__.py ├── cava.py ├── cavasik.gresource.xml ├── cavasik.in ├── dbus.py ├── draw_functions.py ├── drawing_area.py ├── main.py ├── meson.build ├── preferences_window.py ├── settings.py ├── settings_import_export.py ├── shortcuts.py ├── shortcuts_dialog.ui ├── translation_credits.py.in └── window.py └── utils └── flatpak-pip-generator.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS 2 | 3 | # Specifies the owner of each file in this branch using a structure similar to .gitignore files 4 | 5 | # This owner will be the default owner for everything in the repo. 6 | # Unless a later match takes precedence, @TheWisker will be requested for review when someone opens a pull request. 7 | * @TheWisker -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 |

Cavasik

2 |
3 | 4 | 5 | 6 |
7 |

Audio visualizer based on CAVA

8 | 9 |

Contributor Covenant Code of Conduct

10 | 11 | ## Our Pledge 12 | 13 | We as members, contributors, and leaders pledge to make participation in our 14 | community a harassment-free experience for everyone, regardless of age, body 15 | size, visible or invisible disability, ethnicity, sex characteristics, gender 16 | identity and expression, level of experience, education, socio-economic status, 17 | nationality, personal appearance, race, religion, or sexual identity 18 | and orientation. 19 | 20 | We pledge to act and interact in ways that contribute to an open, welcoming, 21 | diverse, inclusive, and healthy community. 22 | 23 | ## Our Standards 24 | 25 | Examples of behavior that contributes to a positive environment for our 26 | community include: 27 | 28 | * Demonstrating empathy and kindness toward other people 29 | * Being respectful of differing opinions, viewpoints, and experiences 30 | * Giving and gracefully accepting constructive feedback 31 | * Accepting responsibility and apologizing to those affected by our mistakes, 32 | and learning from the experience 33 | * Focusing on what is best not just for us as individuals, but for the 34 | overall community 35 | 36 | Examples of unacceptable behavior include: 37 | 38 | * The use of sexualized language or imagery, and sexual attention or 39 | advances of any kind 40 | * Trolling, insulting or derogatory comments, and personal or political attacks 41 | * Public or private harassment 42 | * Publishing others' private information, such as a physical or email 43 | address, without their explicit permission 44 | * Other conduct which could reasonably be considered inappropriate in a 45 | professional setting 46 | 47 | ## Enforcement Responsibilities 48 | 49 | Community leaders are responsible for clarifying and enforcing our standards of 50 | acceptable behavior and will take appropriate and fair corrective action in 51 | response to any behavior that they deem inappropriate, threatening, offensive, 52 | or harmful. 53 | 54 | Community leaders have the right and responsibility to remove, edit, or reject 55 | comments, commits, code, wiki edits, issues, and other contributions that are 56 | not aligned to this Code of Conduct, and will communicate reasons for moderation 57 | decisions when appropriate. 58 | 59 | ## Scope 60 | 61 | This Code of Conduct applies within all community spaces, and also applies when 62 | an individual is officially representing the community in public spaces. 63 | Examples of representing our community include using an official e-mail address, 64 | posting via an official social media account, or acting as an appointed 65 | representative at an online or offline event. 66 | 67 | ## Enforcement 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 70 | reported to the community leaders responsible for enforcement at 71 | TheWisker@protonmail.com. 72 | All complaints will be reviewed and investigated promptly and fairly. 73 | 74 | All community leaders are obligated to respect the privacy and security of the 75 | reporter of any incident. 76 | 77 | ## Enforcement Guidelines 78 | 79 | Community leaders will follow these Community Impact Guidelines in determining 80 | the consequences for any action they deem in violation of this Code of Conduct: 81 | 82 | ### 1. Correction 83 | 84 | **Community Impact**: Use of inappropriate language or other behavior deemed 85 | unprofessional or unwelcome in the community. 86 | 87 | **Consequence**: A private, written warning from community leaders, providing 88 | clarity around the nature of the violation and an explanation of why the 89 | behavior was inappropriate. A public apology may be requested. 90 | 91 | ### 2. Warning 92 | 93 | **Community Impact**: A violation through a single incident or series 94 | of actions. 95 | 96 | **Consequence**: A warning with consequences for continued behavior. No 97 | interaction with the people involved, including unsolicited interaction with 98 | those enforcing the Code of Conduct, for a specified period of time. This 99 | includes avoiding interactions in community spaces as well as external channels 100 | like social media. Violating these terms may lead to a temporary or 101 | permanent ban. 102 | 103 | ### 3. Temporary Ban 104 | 105 | **Community Impact**: A serious violation of community standards, including 106 | sustained inappropriate behavior. 107 | 108 | **Consequence**: A temporary ban from any sort of interaction or public 109 | communication with the community for a specified period of time. No public or 110 | private interaction with the people involved, including unsolicited interaction 111 | with those enforcing the Code of Conduct, is allowed during this period. 112 | Violating these terms may lead to a permanent ban. 113 | 114 | ### 4. Permanent Ban 115 | 116 | **Community Impact**: Demonstrating a pattern of violation of community 117 | standards, including sustained inappropriate behavior, harassment of an 118 | individual, or aggression toward or disparagement of classes of individuals. 119 | 120 | **Consequence**: A permanent ban from any sort of public interaction within 121 | the community. 122 | 123 | ## Attribution 124 | 125 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 126 | version 2.0, available at 127 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 128 | 129 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 130 | enforcement ladder](https://github.com/mozilla/diversity). 131 | 132 | [homepage]: https://www.contributor-covenant.org 133 | 134 | For answers to common questions about this code of conduct, see the FAQ at 135 | https://www.contributor-covenant.org/faq. Translations are available at 136 | https://www.contributor-covenant.org/translations. 137 | 138 |

Author

139 |
140 | 141 | 142 | 143 |
144 |

TheWisker

145 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[BUG] Title" 4 | labels: ["bug"] 5 | assignees: 6 | - TheWisker 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | # Cavasik Bug Report 12 | Thanks for taking the time to fill out this bug report! 13 | - type: textarea 14 | id: explanation 15 | attributes: 16 | label: Explanation 17 | description: Clear and concise description of the bug 18 | placeholder: There is a bug in ... when you click ... 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: steps 23 | attributes: 24 | label: Steps to reproduce 25 | description: Steps to reproduce the issue 26 | placeholder: | 27 | 1. Go to '...' 28 | 2. Click on '....' 29 | 3. Scroll down to '....' 30 | 4. See error 31 | - type: dropdown 32 | id: version 33 | attributes: 34 | label: Cavasik Version 35 | description: What version of our software are you running? 36 | options: 37 | - v1.0.0 38 | validations: 39 | required: true 40 | - type: input 41 | id: distro 42 | attributes: 43 | label: Linux Distro 44 | placeholder: ex. Arch 45 | validations: 46 | required: true 47 | - type: textarea 48 | id: context 49 | attributes: 50 | label: Additional Context 51 | description: Add any other context about the problem here 52 | - type: textarea 53 | id: logs 54 | attributes: 55 | label: Relevant log output 56 | description: Please copy and paste any relevant log output. 57 | render: shell 58 | - type: checkboxes 59 | id: terms 60 | attributes: 61 | label: Code of Conduct 62 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/TheWisker/Cavasik/blob/master/.github/CODE_OF_CONDUCT.md) 63 | options: 64 | - label: I agree to follow this project's Code of Conduct 65 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | title: "[Enhancement] Title" 4 | labels: ["enhancement"] 5 | assignees: 6 | - TheWisker 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | # Cavasik Feature Request 12 | Thanks for taking the time to fill out this feature request! 13 | - type: textarea 14 | id: explanation 15 | attributes: 16 | label: Explanation 17 | description: Clear and concise description of the feature request 18 | placeholder: I would like that ... is added and ... is changed because ... 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: behavior 23 | attributes: 24 | label: Expected Behavior 25 | description: Clear and concise description of what you expected to happen 26 | placeholder: I would like that when I ... ... happens 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: alternatives 31 | attributes: 32 | label: Alternative Solutions 33 | description: Clear and concise description of any alternative solutions or features you've considered 34 | placeholder: I have also considered ... and it can also be achieved with ... 35 | - type: textarea 36 | id: context 37 | attributes: 38 | label: Additional Context 39 | description: Add any other context about the feature request here 40 | - type: checkboxes 41 | id: terms 42 | attributes: 43 | label: Code of Conduct 44 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/TheWisker/Cavasik/blob/master/.github/CODE_OF_CONDUCT.md) 45 | options: 46 | - label: I agree to follow this project's Code of Conduct 47 | required: true -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bug_fix.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug fix 3 | about: Create a pull request about a bug fix 4 | title: "[BUG] Title" 5 | labels: bug 6 | assignees: TheWisker 7 | 8 | --- 9 | 10 | # Cavasik Pull Request 11 | 12 | This is a **template** for a **bug fix** pull request for **Cavasik**. 13 | 14 | ## Explanation 15 | 16 | 1. Include a **summary** of the changes and the **related issue**. 17 | 2. Include relevant **motivation** and **context**. 18 | 3. List any **dependency** changes that are required for this pull request. 19 | 4. List exactly what it **fixes**. 20 | 21 | ## Checklist 22 | 23 | Check all **true** statements about this pull request: 24 | 25 | - [ ] It is a **non-breaking** change which fixes an issue. 26 | - [ ] It is a **breaking** change that causes existing functionality to not work as expected. 27 | - [ ] This change requires a **documentation update**. 28 | - [ ] It follows the **style** guidelines of this project. 29 | - [ ] The code is **commented**, particularly in hard-to-understand areas. 30 | - [ ] **Self-reviews** of the code have been performed. 31 | - [ ] The changes generate **no new warnings**. 32 | - [ ] It has been **tested** for stability. 33 | 34 | ### Thank you for contributing! -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/new_feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New feature 3 | about: Create a pull request about a new feature 4 | title: "[Enhancement] Title" 5 | labels: enhancement 6 | assignees: TheWisker 7 | 8 | --- 9 | 10 | # Cavasik Pull Request 11 | 12 | This is a **template** for a **new feature** pull request for **Cavasik**. 13 | 14 | ## Explanation 15 | 16 | 1. Include a **summary** of the changes and the **additions** made. 17 | 2. Include relevant **motivation** and **context**. 18 | 3. List any **dependency** changes that are required for this pull request. 19 | 4. List exactly what it **changes**: **adds** and **removes**. 20 | 21 | ## Checklist 22 | 23 | Check all **true** statements about this pull request: 24 | 25 | - [ ] It is a **non-breaking** change. 26 | - [ ] It is a **breaking** change that causes existing functionality to not work as expected. 27 | - [ ] This change requires a **documentation update**. 28 | - [ ] It follows the **style** guidelines of this project. 29 | - [ ] The code is **commented**, particularly in hard-to-understand areas. 30 | - [ ] **Self-reviews** of the code have been performed. 31 | - [ ] The changes generate **no new warnings**. 32 | - [ ] It has been **tested** for stability. 33 | 34 | ### Thank you for contributing! -------------------------------------------------------------------------------- /.github/workflows/aur_update.yml: -------------------------------------------------------------------------------- 1 | name: AUR Update 2 | run-name: AUR package update by ${{ github.actor }} release 3 | on: 4 | release: 5 | types: [published] 6 | permissions: 7 | contents: write 8 | concurrency: 9 | group: "aur" 10 | cancel-in-progress: true 11 | jobs: 12 | aur-update: 13 | environment: 14 | name: aur-packages 15 | runs-on: ubuntu-latest 16 | container: 17 | image: archlinux 18 | options: --privileged 19 | steps: 20 | - name: AUR Update 21 | uses: TheWisker/aur-update@master 22 | with: 23 | aur_key: ${{ secrets.AUR_KEY }} 24 | username: TheWisker 25 | email: TheWisker@protonmail.com -------------------------------------------------------------------------------- /.github/workflows/aur_update_git.yml: -------------------------------------------------------------------------------- 1 | name: AUR Update Git 2 | run-name: AUR package update by ${{ github.actor }} push 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | permissions: 8 | contents: write 9 | concurrency: 10 | group: "aur" 11 | cancel-in-progress: false 12 | jobs: 13 | aur-update-git: 14 | environment: 15 | name: aur-packages 16 | runs-on: ubuntu-latest 17 | container: 18 | image: archlinux 19 | options: --privileged 20 | steps: 21 | - name: AUR Update Git 22 | uses: TheWisker/aur-update-git@master 23 | with: 24 | aur_key: ${{ secrets.AUR_KEY }} 25 | username: TheWisker 26 | email: TheWisker@protonmail.com -------------------------------------------------------------------------------- /.github/workflows/github_metrics.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Metrics 2 | run-name: Updating ${{ github.event.repository.name }} metrics 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | permissions: 8 | contents: write 9 | concurrency: 10 | group: "metrics" 11 | cancel-in-progress: true 12 | jobs: 13 | github-metrics: 14 | runs-on: ubuntu-latest 15 | environment: 16 | name: repo-metrics 17 | steps: 18 | - name: Configure Git Credentials 19 | shell: bash 20 | env: 21 | username: ${{ github.repository_owner }} 22 | email: TheWisker@protonmail.com 23 | repo_name: ${{ github.event.repository.name }} 24 | run: | 25 | git config --global user.name $username 26 | git config --global user.email $email 27 | git config --global init.defaultBranch master 28 | git config --global --add safe.directory /__w/$repo_name/$repo_name 29 | 30 | echo "Configured Git Credentials" 31 | 32 | - name: Base Metrics 33 | uses: TheWisker/GitHubMetrics@master 34 | with: 35 | token: ${{ secrets.METRICS_TOKEN }} 36 | user: ${{ github.repository_owner }} 37 | repo: ${{ github.event.repository.name }} 38 | #committer_token: 39 | #committer_branch: 40 | #committer_message: 41 | #committer_gist: 42 | filename: base.* 43 | #markdown: 44 | #markdown_cache: 45 | output_action: none 46 | output_condition: data-changed 47 | optimize: css, xml, svg 48 | #setup_community_templates: 49 | template: repository 50 | #query: 51 | #extras_css: 52 | #extras_js: 53 | #github_api_rest: 54 | #github_api_graphql: 55 | config_timezone: Europe/Madrid 56 | #config_order: 57 | #config_twemoji: 58 | config_gemoji: yes 59 | #config_octicon: 60 | config_display: regular 61 | config_animations: yes 62 | config_base64: yes 63 | #config_padding: 64 | config_output: auto 65 | #config_presets: 66 | retries: 5 67 | retries_delay: 600 68 | retries_output_action: 5 69 | retries_delay_output_action: 300 70 | clean_workflows: failure 71 | #delay: 72 | #quota_required_rest: 73 | #quota_required_graphql: 74 | #quota_required_search: 75 | notice_releases: yes 76 | use_prebuilt_image: yes 77 | #plugins_errors_fatal: 78 | debug: no 79 | verify: no 80 | #debug_flags: 81 | #debug_print: 82 | #dryrun: 83 | experimental_features: --optimize-svg 84 | #use_mocked_data 85 | base: header, activity, community, repositories 86 | base_indepth: yes 87 | #base_hireable: 88 | #base_skip: 89 | repositories: 200 90 | repositories_batch: 100 91 | repositories_forks: yes 92 | repositories_affiliations: owner, collaborator, organization_member 93 | #repositories_skipped: 94 | #users_ignored: 95 | #commits_authoring: 96 | plugin_traffic: yes 97 | #plugin_traffic_skipped: 98 | plugin_lines: yes 99 | #plugin_lines_sections: 100 | #plugin_lines_repositories_limit: 101 | plugin_lines_history_limit: 1 102 | 103 | - name: Languages Metrics 104 | uses: TheWisker/GitHubMetrics@master 105 | with: 106 | token: ${{ secrets.METRICS_TOKEN }} 107 | user: ${{ github.repository_owner }} 108 | repo: ${{ github.event.repository.name }} 109 | #committer_token: 110 | #committer_branch: 111 | #committer_message: 112 | #committer_gist: 113 | filename: languages.* 114 | #markdown: 115 | #markdown_cache: 116 | output_action: none 117 | output_condition: data-changed 118 | optimize: css, xml, svg 119 | #setup_community_templates: 120 | template: repository 121 | #query: 122 | #extras_css: 123 | #extras_js: 124 | #github_api_rest: 125 | #github_api_graphql: 126 | config_timezone: Europe/Madrid 127 | #config_order: 128 | #config_twemoji: 129 | config_gemoji: yes 130 | #config_octicon: 131 | config_display: regular 132 | config_animations: yes 133 | config_base64: yes 134 | #config_padding: 135 | config_output: auto 136 | #config_presets: 137 | retries: 5 138 | retries_delay: 600 139 | retries_output_action: 5 140 | retries_delay_output_action: 300 141 | clean_workflows: failure 142 | #delay: 143 | #quota_required_rest: 144 | #quota_required_graphql: 145 | #quota_required_search: 146 | notice_releases: yes 147 | use_prebuilt_image: yes 148 | #plugins_errors_fatal: 149 | debug: no 150 | verify: no 151 | #debug_flags: 152 | #debug_print: 153 | #dryrun: 154 | experimental_features: --optimize-svg 155 | #use_mocked_data 156 | base: "" 157 | plugin_languages: yes 158 | #plugin_languages_ignored: 159 | #plugin_languages_skipped: 160 | plugin_languages_limit: 0 161 | #plugin_languages_threshold: 162 | #plugin_languages_other: 163 | #plugin_languages_colors: 164 | #plugin_languages_aliases: 165 | plugin_languages_sections: most-used 166 | plugin_languages_details: bytes-size, percentage, lines 167 | plugin_languages_indepth: yes 168 | #plugin_languages_indepth_custom: 169 | plugin_languages_analysis_timeout: 60 170 | plugin_languages_analysis_timeout_repositories: 15 171 | #plugin_languages_categories: 172 | #plugin_languages_recent_categories: 173 | plugin_languages_recent_load: 1000 174 | plugin_languages_recent_days: 365 175 | 176 | - name: Contributors Metrics 177 | uses: TheWisker/GitHubMetrics@master 178 | with: 179 | token: ${{ secrets.METRICS_TOKEN }} 180 | user: ${{ github.repository_owner }} 181 | repo: ${{ github.event.repository.name }} 182 | #committer_token: 183 | #committer_branch: 184 | #committer_message: 185 | #committer_gist: 186 | filename: contributors.* 187 | #markdown: 188 | #markdown_cache: 189 | output_action: none 190 | output_condition: data-changed 191 | optimize: css, xml, svg 192 | #setup_community_templates: 193 | template: repository 194 | #query: 195 | #extras_css: 196 | #extras_js: 197 | #github_api_rest: 198 | #github_api_graphql: 199 | config_timezone: Europe/Madrid 200 | #config_order: 201 | #config_twemoji: 202 | config_gemoji: yes 203 | #config_octicon: 204 | config_display: regular 205 | config_animations: yes 206 | config_base64: yes 207 | #config_padding: 208 | config_output: auto 209 | #config_presets: 210 | retries: 5 211 | retries_delay: 600 212 | retries_output_action: 5 213 | retries_delay_output_action: 300 214 | clean_workflows: failure 215 | #delay: 216 | #quota_required_rest: 217 | #quota_required_graphql: 218 | #quota_required_search: 219 | notice_releases: yes 220 | use_prebuilt_image: yes 221 | #plugins_errors_fatal: 222 | debug: no 223 | verify: no 224 | #debug_flags: 225 | #debug_print: 226 | #dryrun: 227 | experimental_features: --optimize-svg 228 | #use_mocked_data 229 | base: "" 230 | plugin_contributors: yes 231 | #plugin_contributors_base: 232 | #plugin_contributors_head: 233 | #plugin_contributors_ignored: 234 | plugin_contributors_contributions: yes 235 | #plugin_contributors_sections: 236 | #plugin_contributors_categories: 237 | 238 | - name: Licenses Metrics 239 | uses: TheWisker/GitHubMetrics@master 240 | with: 241 | token: ${{ secrets.METRICS_TOKEN }} 242 | user: ${{ github.repository_owner }} 243 | repo: ${{ github.event.repository.name }} 244 | #committer_token: 245 | #committer_branch: 246 | #committer_message: 247 | #committer_gist: 248 | filename: licenses.* 249 | #markdown: 250 | #markdown_cache: 251 | output_action: none 252 | output_condition: data-changed 253 | optimize: css, xml, svg 254 | #setup_community_templates: 255 | template: repository 256 | #query: 257 | #extras_css: 258 | #extras_js: 259 | #github_api_rest: 260 | #github_api_graphql: 261 | config_timezone: Europe/Madrid 262 | #config_order: 263 | #config_twemoji: 264 | config_gemoji: yes 265 | #config_octicon: 266 | config_display: regular 267 | config_animations: yes 268 | config_base64: yes 269 | #config_padding: 270 | config_output: auto 271 | #config_presets: 272 | retries: 5 273 | retries_delay: 600 274 | retries_output_action: 5 275 | retries_delay_output_action: 300 276 | clean_workflows: failure 277 | #delay: 278 | #quota_required_rest: 279 | #quota_required_graphql: 280 | #quota_required_search: 281 | notice_releases: yes 282 | use_prebuilt_image: yes 283 | #plugins_errors_fatal: 284 | debug: no 285 | verify: no 286 | #debug_flags: 287 | #debug_print: 288 | #dryrun: 289 | experimental_features: --optimize-svg 290 | #use_mocked_data: 291 | base: "" 292 | plugin_licenses: yes 293 | plugin_licenses_setup: npm install 294 | #plugin_licenses_ratio: 295 | plugin_licenses_legal: yes 296 | 297 | - name: Checkout 298 | uses: actions/checkout@v3 299 | with: 300 | ref: master 301 | 302 | - name: Flush Metrics 303 | shell: bash 304 | run: | 305 | rm -fr ./assets/metrics 306 | mkdir -p ./assets/metrics 307 | cp -f /metrics_renders/* ./assets/metrics 308 | 309 | echo "Flushed Metrics" 310 | 311 | - name: Update Repository 312 | shell: bash 313 | run: | 314 | git add . 315 | git commit -m "Metrics Update" 316 | git push 317 | 318 | echo "Updated Repository" 319 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | .vscode/ 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | #wwwroot/ 33 | 34 | # Visual Studio 2017 auto generated files 35 | Generated\ Files/ 36 | 37 | # MSTest test Results 38 | [Tt]est[Rr]esult*/ 39 | [Bb]uild[Ll]og.* 40 | 41 | # NUNIT 42 | *.VisualState.xml 43 | TestResult.xml 44 | 45 | # Build Results of an ATL Project 46 | [Dd]ebugPS/ 47 | [Rr]eleasePS/ 48 | dlldata.c 49 | 50 | # Benchmark Results 51 | BenchmarkDotNet.Artifacts/ 52 | 53 | # .NET Core 54 | project.lock.json 55 | project.fragment.lock.json 56 | artifacts/ 57 | 58 | # StyleCop 59 | StyleCopReport.xml 60 | 61 | # Files built by Visual Studio 62 | *_i.c 63 | *_p.c 64 | *_h.h 65 | *.ilk 66 | *.meta 67 | *.obj 68 | *.iobj 69 | *.pch 70 | *.pdb 71 | *.ipdb 72 | *.pgc 73 | *.pgd 74 | *.rsp 75 | *.sbr 76 | *.tlb 77 | *.tli 78 | *.tlh 79 | *.tmp 80 | *.tmp_proj 81 | *_wpftmp.csproj 82 | *.log 83 | *.vspscc 84 | *.vssscc 85 | .builds 86 | *.pidb 87 | *.svclog 88 | *.scc 89 | 90 | # Chutzpah Test files 91 | _Chutzpah* 92 | 93 | # Visual C++ cache files 94 | ipch/ 95 | *.aps 96 | *.ncb 97 | *.opendb 98 | *.opensdf 99 | *.sdf 100 | *.cachefile 101 | *.VC.db 102 | *.VC.VC.opendb 103 | 104 | # Visual Studio profiler 105 | *.psess 106 | *.vsp 107 | *.vspx 108 | *.sap 109 | 110 | # Visual Studio Trace Files 111 | *.e2e 112 | 113 | # TFS 2012 Local Workspace 114 | $tf/ 115 | 116 | # Guidance Automation Toolkit 117 | *.gpState 118 | 119 | # ReSharper is a .NET coding add-in 120 | _ReSharper*/ 121 | *.[Rr]e[Ss]harper 122 | *.DotSettings.user 123 | 124 | # JustCode is a .NET coding add-in 125 | .JustCode 126 | 127 | # TeamCity is a build add-in 128 | _TeamCity* 129 | 130 | # DotCover is a Code Coverage Tool 131 | *.dotCover 132 | 133 | # AxoCover is a Code Coverage Tool 134 | .axoCover/* 135 | !.axoCover/settings.json 136 | 137 | # Visual Studio code coverage results 138 | *.coverage 139 | *.coveragexml 140 | 141 | # NCrunch 142 | _NCrunch_* 143 | .*crunch*.local.xml 144 | nCrunchTemp_* 145 | 146 | # MightyMoose 147 | *.mm.* 148 | AutoTest.Net/ 149 | 150 | # Web workbench (sass) 151 | .sass-cache/ 152 | 153 | # Installshield output folder 154 | [Ee]xpress/ 155 | 156 | # DocProject is a documentation generator add-in 157 | DocProject/buildhelp/ 158 | DocProject/Help/*.HxT 159 | DocProject/Help/*.HxC 160 | DocProject/Help/*.hhc 161 | DocProject/Help/*.hhk 162 | DocProject/Help/*.hhp 163 | DocProject/Help/Html2 164 | DocProject/Help/html 165 | 166 | # Click-Once directory 167 | publish/ 168 | 169 | # Publish Web Output 170 | *.[Pp]ublish.xml 171 | *.azurePubxml 172 | # Note: Comment the next line if you want to checkin your web deploy settings, 173 | # but database connection strings (with potential passwords) will be unencrypted 174 | *.pubxml 175 | *.publishproj 176 | 177 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 178 | # checkin your Azure Web App publish settings, but sensitive information contained 179 | # in these scripts will be unencrypted 180 | PublishScripts/ 181 | 182 | # NuGet Packages 183 | *.nupkg 184 | # The packages folder can be ignored because of Package Restore 185 | **/[Pp]ackages/* 186 | # except build/, which is used as an MSBuild target. 187 | !**/[Pp]ackages/build/ 188 | # Uncomment if necessary however generally it will be regenerated when needed 189 | #!**/[Pp]ackages/repositories.config 190 | # NuGet v3's project.json files produces more ignorable files 191 | *.nuget.props 192 | *.nuget.targets 193 | 194 | # Microsoft Azure Build Output 195 | csx/ 196 | *.build.csdef 197 | 198 | # Microsoft Azure Emulator 199 | ecf/ 200 | rcf/ 201 | 202 | # Windows Store app package directories and files 203 | AppPackages/ 204 | BundleArtifacts/ 205 | Package.StoreAssociation.xml 206 | _pkginfo.txt 207 | *.appx 208 | 209 | # Visual Studio cache files 210 | # files ending in .cache can be ignored 211 | *.[Cc]ache 212 | # but keep track of directories ending in .cache 213 | !*.[Cc]ache/ 214 | 215 | # Others 216 | ClientBin/ 217 | ~$* 218 | *~ 219 | *.dbmdl 220 | *.dbproj.schemaview 221 | *.jfm 222 | *.pfx 223 | *.publishsettings 224 | orleans.codegen.cs 225 | 226 | # Including strong name files can present a security risk 227 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 228 | #*.snk 229 | 230 | # Since there are multiple workflows, uncomment next line to ignore bower_components 231 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 232 | #bower_components/ 233 | 234 | # RIA/Silverlight projects 235 | Generated_Code/ 236 | 237 | # Backup & report files from converting an old project file 238 | # to a newer Visual Studio version. Backup files are not needed, 239 | # because we have git ;-) 240 | _UpgradeReport_Files/ 241 | Backup*/ 242 | UpgradeLog*.XML 243 | UpgradeLog*.htm 244 | ServiceFabricBackup/ 245 | *.rptproj.bak 246 | 247 | # SQL Server files 248 | *.mdf 249 | *.ldf 250 | *.ndf 251 | 252 | # Business Intelligence projects 253 | *.rdl.data 254 | *.bim.layout 255 | *.bim_*.settings 256 | *.rptproj.rsuser 257 | 258 | # Microsoft Fakes 259 | FakesAssemblies/ 260 | 261 | # GhostDoc plugin setting file 262 | *.GhostDoc.xml 263 | 264 | # Node.js Tools for Visual Studio 265 | .ntvs_analysis.dat 266 | node_modules/ 267 | 268 | # Visual Studio 6 build log 269 | *.plg 270 | 271 | # Visual Studio 6 workspace options file 272 | *.opt 273 | 274 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 275 | *.vbw 276 | 277 | # Visual Studio LightSwitch build output 278 | **/*.HTMLClient/GeneratedArtifacts 279 | **/*.DesktopClient/GeneratedArtifacts 280 | **/*.DesktopClient/ModelManifest.xml 281 | **/*.Server/GeneratedArtifacts 282 | **/*.Server/ModelManifest.xml 283 | _Pvt_Extensions 284 | 285 | # Paket dependency manager 286 | .paket/paket.exe 287 | paket-files/ 288 | 289 | # FAKE - F# Make 290 | .fake/ 291 | 292 | # JetBrains Rider 293 | .idea/ 294 | *.sln.iml 295 | 296 | # CodeRush personal settings 297 | .cr/personal 298 | 299 | # Python Tools for Visual Studio (PTVS) 300 | __pycache__/ 301 | *.pyc 302 | 303 | # Cake - Uncomment if you are using it 304 | # tools/** 305 | # !tools/packages.config 306 | 307 | # Tabs Studio 308 | *.tss 309 | 310 | # Telerik's JustMock configuration file 311 | *.jmconfig 312 | 313 | # BizTalk build output 314 | *.btp.cs 315 | *.btm.cs 316 | *.odx.cs 317 | *.xsd.cs 318 | 319 | # OpenCover UI analysis results 320 | OpenCover/ 321 | 322 | # Azure Stream Analytics local run output 323 | ASALocalRun/ 324 | 325 | # MSBuild Binary and Structured Log 326 | *.binlog 327 | 328 | # NVidia Nsight GPU debugger configuration file 329 | *.nvuser 330 | 331 | # MFractors (Xamarin productivity tool) working folder 332 | .mfractor/ 333 | 334 | # Local History for Visual Studio 335 | .localhistory/ 336 | 337 | #Intellij 338 | .idea/ 339 | 340 | #Resources 341 | *.Designer.cs 342 | 343 | # Flatpak Builder 344 | .flatpak-builder/ 345 | .flatpak/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "shared-modules"] 2 | path = shared-modules 3 | url = https://github.com/flathub/shared-modules.git 4 | -------------------------------------------------------------------------------- /ACKNOWLEDGMENTS.md: -------------------------------------------------------------------------------- 1 |

Cavasik

2 |
3 | 4 | 5 | 6 |
7 |

Audio visualizer based on CAVA

8 | 9 |

Acknowledgments

10 | 11 |

This page gives credit to the underlying projects in which Cavasik is based upon.

12 | 13 | ## Acknowledgment Index 14 | 15 | - [Cavalier][cavalier-hook] 16 | - [Cava][cava-hook] 17 | - [Gtk][gtk-hook] 18 | - [GNOME][gnome-hook] 19 | - [LibAdwaita][libadwaita-hook] 20 | - [Meson][meson-hook] 21 | - [Python][python-hook] 22 | 23 | ## Cavalier [[↑][index]] 24 | 25 | **Cavasik** is a fork of **[Cavalier][cavalier]** so we give our acknowledgments. 26 | 27 | ## Cava [[↑][index]] 28 | 29 | **Cavasik** is based on **[Cava][cava]** so we give our acknowledgments. 30 | 31 | ## Gtk [[↑][index]] 32 | 33 | **Cavasik** makes use of the **[Gtk][gtk]** so we give our acknowledgments. 34 | 35 | ## GNOME [[↑][index]] 36 | 37 | **Cavasik** uses of **[GNOME][gnome]** so we give our acknowledgments. 38 | 39 | ## LibAdwaita [[↑][index]] 40 | 41 | **Cavasik** makes use of **[LibAdwaita][libadwaita]** so we give our acknowledgments. 42 | 43 | ## Meson [[↑][index]] 44 | 45 | **Cavasik** makes use of **[Meson][meson]** so we give our acknowledgments. 46 | 47 | ## Python [[↑][index]] 48 | 49 | **Cavasik** makes use of **[Python][python]** and some libraries: 50 | 51 | - **[PyGObject][pygobject]** 52 | - **[PyCairo][pycairo]** 53 | - **[PyDBus][pydbus]** 54 | 55 |

Author

56 |
57 | 58 | 59 | 60 |
61 |

TheWisker

62 | 63 | [index]: https://github.com/TheWisker/Cavasik/blob/master/ACKNOWLEDGMENTS.md#acknowledgment-index 64 | [cavalier-hook]: https://github.com/TheWisker/Cavasik/blob/master/ACKNOWLEDGMENTS.md#cavalier- 65 | [cava-hook]: https://github.com/TheWisker/Cavasik/blob/master/ACKNOWLEDGMENTS.md#cava- 66 | [gtk-hook]: https://github.com/TheWisker/Cavasik/blob/master/ACKNOWLEDGMENTS.md#gtk- 67 | [gnome-hook]: https://github.com/TheWisker/Cavasik/blob/master/ACKNOWLEDGMENTS.md#gnome- 68 | [libadwaita-hook]: https://github.com/TheWisker/Cavasik/blob/master/ACKNOWLEDGMENTS.md#libadwaita- 69 | [meson-hook]: https://github.com/TheWisker/Cavasik/blob/master/ACKNOWLEDGMENTS.md#meson- 70 | [python-hook]: https://github.com/TheWisker/Cavasik/blob/master/ACKNOWLEDGMENTS.md#python- 71 | 72 | [cavalier]: https://github.com/fsobolev/cavalier 73 | [cava]: https://github.com/karlstav/cava 74 | [gtk]: https://www.gtk.org/ 75 | [gnome]: https://www.gnome.org/ 76 | [libadwaita]: https://gitlab.gnome.org/GNOME/libadwaita 77 | [meson]: https://mesonbuild.com/ 78 | [python]: https://www.python.org/ 79 | [pygobject]: https://pygobject.readthedocs.io/en/latest/ 80 | [pycairo]: https://pycairo.readthedocs.io/en/latest/ 81 | [pydbus]: https://pydbus.readthedocs.io/en/latest/gettingstarted.html 82 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 |

Cavasik

2 |
3 | 4 | 5 | 6 |
7 |

Audio visualizer based on CAVA

8 | 9 |

Authors

10 | 11 | - **Main**: [TheWisker](https://github.com/TheWisker) => TheWisker@protonmail.com 12 | 13 | - **Owner** of the **Cavasik** repository, has written the changes made to **Cavalier**. 14 | 15 | - **Fork Source**: [Fyodor Sobolev](https://github.com/fsobolev) 16 | 17 | - **Owner** of the **Cavalier** repository from which **Cavasik** was forked. 18 | 19 | **Other contributors** are listed in the [CONTRIBUTORS.md][contributors] file. 20 | 21 |

Author

22 |
23 | 24 | 25 | 26 |
27 |

TheWisker

28 | 29 | [contributors]: ./CONTRIBUTORS.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 |

Cavasik

2 |
3 | 4 | 5 | 6 |
7 |

Audio visualizer based on CAVA

8 | 9 |

Changelog

10 | 11 | All notable changes to this project will be documented in this file. 12 | 13 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 14 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 15 | 16 | ## [2.0.1] - 2023-08-31 17 | 18 | Minor update for **Cavasik**. 19 | 20 | ### Added 21 | - **Mirror Sync** related setting. 22 | 23 | ### Changed 24 | - No changes 25 | 26 | ### Fixed 27 | - Some typos 28 | 29 | ## [2.0.0] - 2023-06-17 30 | 31 | **First** official version of **Cavasik**. 32 | 33 | ### Added 34 | 35 | - **Circle** related settings. 36 | - **Circle** version of **Waves** and **Bars**. 37 | - All **mirror modes** and settings. 38 | - All **direction modes** and settings. 39 | - **Color animation** and it's settings. 40 | - **DBus interface** to allow changing colors externally. 41 | - **FPS** configuration option. 42 | 43 | ### Changed 44 | - Keyboard shortcuts (some removed, some added) 45 | 46 | ### Fixed 47 | - No fixes 48 | 49 | ## [1.0.0] - 2023-01-29 50 | 51 | Latest release of **Cavalier**. 52 | 53 | ### Added 54 | 55 | - New drawing mode — Particles! 56 | - Color profiles! Create as many as you want and change between them instantly. Unfortunately, this new feature required to change how the - application saves colors, and because of this your previous colors settings will be lost after installing this update. 57 | - Added keyboard shortcuts to change most of the settings on the fly. 58 | - Added option to show/hide window controls. 59 | - Added option to autohide headerbar when the main window is not focused. 60 | - Added option to change roundness of items in "levels" and "particles" modes. 61 | - Added option to reverse the order of bars. 62 | - Import/Export Settings 63 | 64 | ### Changed 65 | - No changes 66 | 67 | ### Fixed 68 | - No fixes 69 | 70 |

Author

71 |
72 | 73 | 74 | 75 |
76 |

TheWisker

-------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software in your work, or make a fork of it, please cite it using the following metadata." 3 | authors: 4 | - given-names: "TheWisker" 5 | title: "Cavasik Project" 6 | version: 2.0.1 7 | date-released: 2023-06-18 8 | url: "https://github.com/TheWisker/Cavasik" -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

Cavasik

2 |
3 | 4 | 5 | 6 |
7 |

Audio visualizer based on CAVA

8 | 9 |

Contributing

10 | 11 | It is great to have you here. Here are a few ways you can help make this project better! 12 | 13 | ## Team Members 14 | 15 | - Owner: [TheWisker](https://github.com/TheWisker) => TheWisker@protonmail.com 16 | 17 | ## Contributing Index 18 | 19 | - [Adding new features][new-features-hook] 20 | - [Adding a translation][translation-hook] 21 | - [Other contributions][other-contributions-hook] 22 | 23 | ## Adding new features [[↑][index]] 24 | 25 | First of all, thank you for taking the time to contribute to this project! 26 | Here are the **steps** involved when making a contribution: 27 | 28 | - Make a **fork** of this repository 29 | - **Clone** the fork locally 30 | - Make the **changes and additions** desired to the cloned fork 31 | - **Modify** the [CHANGELOG.md][changelog] file, following its structure. 32 | - **Modify** the [meson.build][meson] and [CITATION.cff][citation] file, updating their **version number** using [Semantic Versioning](https://semver.org/spec/v2.0.0.html) 33 | - Add the following **header** to newly added code files: 34 | 35 | ``` 36 | # filename.ext 37 | # 38 | # Copyright (c) 2023, TheWisker 39 | # All rights reserved. 40 | # 41 | # This source code is licensed under the BSD-style license found in the 42 | # LICENSE file in the root directory of this source tree. 43 | ``` 44 | 45 | - **Add** yourself or your organization to the [CONTRIBUTORS.md][contributors] file, following its structure. 46 | - Git **add**, **commit**, and **push** those changes. 47 | - Open a new pull request which will be usually reviewed in less than three days. 48 | 49 | ## Adding a translation [[↑][index]] 50 | 51 | First of all, thanks for taking the time to contribute to this project! 52 | Usually, the process of making a translation is quite **similar** to any other contribution, so follow the steps explained [here][new-features-hook]. 53 | The main **difference** are the steps in the [/lang/CONTRIBUTING.md][lang-contributing] file which explains how to tamper with the translation files and **add** a new **translation**. 54 | 55 | ## Other contributions [[↑][index]] 56 | 57 | You can even contribute by adding new enhancement and improvement **ideas** to the [ideas discussion][ideas-discussion] or lending someone a hand in the repository! 58 | 59 |

Author

60 |
61 | 62 | 63 | 64 |
65 |

TheWisker

66 | 67 | [index]: https://github.com/TheWisker/Cavasik/blob/master/CONTRIBUTING.md#contributing-index 68 | [changelog]: ./CHANGELOG.md 69 | [meson]: ./meson.build 70 | [citation]: ./CITATION.cff 71 | [contributors]: ./CONTRIBUTORS.md 72 | [new-features-hook]: https://github.com/TheWisker/Cavasik/blob/master/CONTRIBUTING.md#adding-new-features- 73 | [translation-hook]: https://github.com/TheWisker/Cavasik/blob/master/CONTRIBUTING.md#adding-a-translation- 74 | [other-contributions-hook]: https://github.com/TheWisker/Cavasik/blob/master/CONTRIBUTING.md#other-contributions- 75 | [lang-contributing]: ./lang/CONTRIBUTING.md 76 | [ideas-discussion]: https://github.com/TheWisker/Cavasik/discussions/new?category=ideas -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 |

Cavasik

2 |
3 | 4 | 5 | 6 |
7 |

Audio visualizer based on CAVA

8 | 9 |

Contributors

10 | 11 | - **Main**: [TheWisker](https://github.com/TheWisker) => TheWisker@protonmail.com 12 | 13 | - **Fork Source**: [Fyodor Sobolev](https://github.com/fsobolev) 14 | 15 | These are the **Cavasik** project **contributors**, ordered from most recent to least recent: 16 | 17 | - [Myles](https://github.com/MylesGit) => Opened an [issue](https://github.com/TheWisker/Cavasik/issues/2) resulting in an enhancement. 18 | - [Kian-Meng Ang](https://github.com/kianmeng) => Made a [pull request](https://github.com/TheWisker/Cavasik/pull/5) resulting in the correction of some typos. 19 | 20 | In another format: 21 | 22 |
23 | 24 | 25 | 26 |
27 | 28 |

Author

29 |
30 | 31 | 32 | 33 |
34 |

TheWisker

35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Cavasik

2 |
3 | 4 | 5 | 6 |
7 |

Audio visualizer based on CAVA

8 | 9 |

Index

10 | 11 |
12 | 13 | [Description][description] 14 | 15 | [Features][features] 16 | 17 | [Screenshots][screenshots] 18 | 19 | [Installation][installation] 20 | 21 | [Dependencies][dependencies] 22 | 23 | [Contributions][contributions] 24 | 25 | [Translations][translations] 26 | 27 | [Metrics][metrics] 28 | 29 | [License][license] 30 | 31 | [Code of Conduct][coc] 32 | 33 | [Credits][credits] 34 | 35 |
36 | 37 |

Description []

38 | 39 |

This is an audio visualizer based on CAVA with extended capabilities.

40 | 41 |

Features []

42 | 43 | The visualizer features: 44 | 45 | - Five **normal** drawing modes! 46 | - Two **circle** drawing modes! 47 | - Three **mirror** drawing modes! 48 | - Four drawing **directions**! 49 | - Customizable **LibAdwaita** interface! 50 | - Set a single color or up to a 10 color linear gradient for **background** and **foreground**! 51 | - Select different **foreground** colors for the mirrored images in **mirror** mode! 52 | - Set up a **color animation** that changes the colors gradually in a loop! 53 | - Configure *smoothing*, *noise reduction* and a few other **CAVA** settings! 54 | - Change **background** and **foreground** colors through a **DBus interface**! 55 | 56 |

Screenshots []

57 | 58 |
59 | 60 |
61 | 62 |

Waves mode

63 | 64 |
65 | 66 |
67 | 68 |

Levels mode

69 | 70 |
71 | 72 |
73 | 74 |

Particles mode

75 | 76 |
77 | 78 |
79 | 80 |

Spine mode

81 | 82 |
83 | 84 |
85 | 86 |

Bars mode

87 | 88 |
89 | 90 |
91 | 92 |

Waves mode + Circle shape

93 | 94 |
95 | 96 |
97 | 98 |

Bars mode + Circle shape

99 | 100 |
101 | 102 |
103 | 104 |

Normal mirror + Waves mode

105 | 106 |
107 | 108 |
109 | 110 |

Inverted mirror + Waves mode

111 | 112 |
113 | 114 |
115 | 116 |

Overlapping mirror + Waves mode

117 | 118 |
119 | 120 |
121 | 122 |

Direction top-bottom + Waves mode

123 | 124 |
125 | 126 |
127 | 128 |

Normal mirror + Direction left-right + Waves mode

129 | 130 |
131 | 132 |
133 | 134 |

Installation []

135 | 136 |

Flathub

137 | 138 | You can install the **Cavasik** app from [Flathub][flathub] in its [app page][flathub-cavasik]. 139 | 140 | 141 | 142 | 143 | 144 | - For information on how to setup *flatpak* on any distro read [this][flatpak-setup]. 145 | 146 |

Arch Linux

147 | 148 | You can install **Cavasik** from the [AUR][aur] repository: 149 | 150 | 151 | 152 | 153 | 154 | - For information on how to install an [AUR][aur] package read [this][aur-wiki] wiki. 155 | 156 |

Manually

157 | 158 | To manually install **Cavasik** start by **downloading** a [release][releases]. 159 | Then, **uncompress** the downloaded release into a resulting folder. 160 | Make sure you have all the [dependencies][dependencies] needed. 161 | Then, proceed to **run** the following commands: 162 | 163 | ``` 164 | #BUILD 165 | arch-meson Cavasik build 166 | meson compile -C build 167 | 168 | #TEST 169 | meson test -C build --print-errorlog 170 | 171 | #INSTALL 172 | meson install -C build 173 | install -Dm644 Cavasik/LICENSE -t "/usr/share/licenses/cavasik" 174 | ``` 175 | 176 |

Dependencies []

177 | 178 |

Buildtime

179 | 180 | The **Cavasik** application has the following *buildtime* dependencies: 181 | 182 | - [meson][meson] 183 | 184 |

Runtime

185 | 186 | The **Cavasik** application has the following *runtime* dependencies: 187 | 188 | - [cava][cava] 189 | - [libadwaita][libadwaita] 190 | - [python-gobject][python-gobject] 191 | - [python-cairo][python-cairo] 192 | - [python-pydbus][python-pydbus] 193 | 194 |

Contributions []

195 | 196 | First and foremost, all contributions are welcome! 197 | The **steps** involved when making a contribution are **explained** in the [CONTRIBUTING.md][contributing] file. 198 | We look forward to your contributions! 199 | 200 | - The **contributors** list is located [here][contributors]. 201 | 202 |

Translations []

203 | 204 | Secondly, all translations are also welcome! 205 | The **steps** involved when making a translation are **explained** in the [CONTRIBUTING.md][contributing] file. 206 | More **specific steps** can be found in the [CONTRIBUTING.md][lang-contributing] file in the `/lang` folder. 207 | We look forward to your translations! 208 | 209 | - The **credits** of the translators are located [here][translator-credits]. 210 | 211 |

Metrics []

212 | 213 |
214 | 215 | 216 | 217 |
218 | 219 |
220 | 221 | 222 | 223 |
224 | 225 |

License []

226 | 227 |
228 | 229 | 230 | 231 |
232 | 233 |

Code of Conduct []

234 | 235 |

This project follows the Contributor Covenant Code of Conduct.

236 | 237 |

Credits []

238 | 239 |
240 | 241 | | Author | Forked From | 242 | | ------------- | ------------- | 243 | | | | 244 | | TheWisker | Fsobolev | 245 | 246 |
247 | 248 | [description]: https://github.com/TheWisker/Cavasik#description- 249 | [features]: https://github.com/TheWisker/Cavasik#features- 250 | [screenshots]: https://github.com/TheWisker/Cavasik#screenshots- 251 | [installation]: https://github.com/TheWisker/Cavasik#installation- 252 | [dependencies]: https://github.com/TheWisker/Cavasik#dependencies- 253 | [contributions]: https://github.com/TheWisker/Cavasik#contributions- 254 | [translations]: https://github.com/TheWisker/Cavasik#translations- 255 | [metrics]: https://github.com/TheWisker/Cavasik#metrics- 256 | [license]: https://github.com/TheWisker/Cavasik#license- 257 | [coc]: https://github.com/TheWisker/Cavasik#code-of-conduct- 258 | [credits]: https://github.com/TheWisker/Cavasik#credits- 259 | [flathub]: https://flathub.org/ 260 | [flathub-cavasik]: https://flathub.org/apps/io.github.TheWisker.Cavasik 261 | [flatpak-setup]: https://flatpak.org/setup/ 262 | [aur]: https://aur.archlinux.org/ 263 | [aur-wiki]: https://wiki.archlinux.org/title/Arch_User_Repository 264 | [releases]: https://github.com/TheWisker/Cavasik/releases/ 265 | [meson]: https://mesonbuild.com/ 266 | [cava]: https://github.com/karlstav/cava 267 | [libadwaita]: https://gitlab.gnome.org/GNOME/libadwaita 268 | [python-gobject]: https://pygobject.readthedocs.io/en/latest/ 269 | [python-cairo]: https://pycairo.readthedocs.io/en/latest/ 270 | [python-pydbus]: https://pydbus.readthedocs.io/en/latest/gettingstarted.html 271 | [contributing]: ./CONTRIBUTING.md 272 | [contributors]: ./CONTRIBUTORS.md 273 | [lang-contributing]: ./lang/CONTRIBUTING.md 274 | [translator-credits]: ./lang/CREDITS.json 275 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 |

Cavasik

2 |
3 | 4 | 5 | 6 |
7 |

Audio visualizer based on CAVA

8 | 9 |

Support

10 | 11 | This page explains where and how to get help with **Cavasik**. 12 | Please **read** through the following **guidelines**. 13 | 14 | ## Support Index 15 | 16 | - [Issues][issues-hook] 17 | - [Discussions][discussions-hook] 18 | - [Contact][contact-hook] 19 | 20 | ## Issues [[↑][index]] 21 | 22 | If you are facing a **issue** or bug with the **Cavasik** application, you can **report it** through the [issues page][issues]. 23 | In said page you can report from **bugs** to possible **enhancements**, so feel free to do so! 24 | 25 | ## Discussions [[↑][index]] 26 | 27 | If you want to share a **new idea** for the **Cavasik** project, have some **questions** or just something more **general** to discuss, you can do so in the [discussions page][discussions]. 28 | 29 | ## Contact [[↑][index]] 30 | 31 | If you have been treated in a way that violates our [Code of Conduct][coc] you can contact me with this email: 32 | 33 | - TheWisker@protonmail.com 34 | 35 |

Author

36 |
37 | 38 | 39 | 40 |
41 |

TheWisker

42 | 43 | [index]: https://github.com/TheWisker/Cavasik/blob/master/SUPPORT.md#support-index 44 | [issues-hook]: https://github.com/TheWisker/Cavasik/blob/master/SUPPORT.md#issues- 45 | [discussions-hook]: https://github.com/TheWisker/Cavasik/blob/master/SUPPORT.md#discussions- 46 | [contact-hook]: https://github.com/TheWisker/Cavasik/blob/master/SUPPORT.md#contact- 47 | [issues]: https://github.com/TheWisker/Cavasik/issues 48 | [discussions]: https://github.com/TheWisker/Cavasik/discussions 49 | [coc]: ./.github/CODE_OF_CONDUCT.md -------------------------------------------------------------------------------- /assets/css/cavasik.css: -------------------------------------------------------------------------------- 1 | /* 2 | cavasik.css 3 | 4 | Copyright (c) 2023, TheWisker 5 | All rights reserved. 6 | 7 | This source code is licensed under the BSD-style license found in the 8 | LICENSE file in the root directory of this source tree. 9 | */ 10 | 11 | .sharp-corners { 12 | border-top-left-radius: 0px; 13 | border-top-right-radius: 0px; 14 | border-bottom-left-radius: 0px; 15 | border-bottom-right-radius: 0px; 16 | } 17 | 18 | .borderless-window { 19 | outline-width: 0px; 20 | box-shadow: none; 21 | } 22 | -------------------------------------------------------------------------------- /assets/fork_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/fork_profile.png -------------------------------------------------------------------------------- /assets/icons/io.github.TheWisker.Cavasik-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/icons/io.github.TheWisker.Cavasik.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/icons/io.github.TheWisker.Cavasik.png -------------------------------------------------------------------------------- /assets/icons/io.github.TheWisker.Cavasik.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 20 | 24 | 28 | 32 | 33 | 36 | 40 | 44 | 48 | 49 | 59 | 69 | 70 | 89 | 97 | 101 | 105 | 109 | 113 | 117 | 121 | 122 | 125 | 130 | 139 | 143 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /assets/icons/meson.build: -------------------------------------------------------------------------------- 1 | install_data( 2 | ('@0@.svg').format('io.github.TheWisker.Cavasik'), 3 | install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps') 4 | ) 5 | 6 | install_data( 7 | ('@0@-symbolic.svg').format('io.github.TheWisker.Cavasik'), 8 | install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'symbolic', 'apps') 9 | ) 10 | -------------------------------------------------------------------------------- /assets/io.github.TheWisker.Cavasik.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Cavasik 3 | Comment=Audio Visualizer 4 | Exec=cavasik 5 | Icon=io.github.TheWisker.Cavasik 6 | Terminal=false 7 | Type=Application 8 | Categories=AudioVideo;Audio;GTK;Amusement; 9 | StartupNotify=true 10 | -------------------------------------------------------------------------------- /assets/io.github.TheWisker.Cavasik.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Window size 6 | (400,200) 7 | 8 | 9 | Window maximized state 10 | Whether main window is maximized or not 11 | false 12 | 13 | 14 | Borderless window 15 | Whether to disable window shadow and borders. 16 | false 17 | 18 | 19 | Window controls 20 | Whether to show window control buttons. 21 | false 22 | 23 | 24 | Autohide headerbar 25 | Whether to hide headerbar when main window is not focused. 26 | false 27 | 28 | 29 | DBus colors 30 | Whether to use colors received through DBus (overwrites default profile). 31 | false 32 | 33 | 34 | DBus opacity 35 | Opacity for the colors received through DBus. 36 | 37 | 100 38 | 39 | 40 | Drawing mode 41 | Defines what the visualizer looks like. 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | "wave" 50 | 51 | 52 | Drawing direction 53 | 54 | 55 | 56 | 57 | 58 | 59 | "bottom-top" 60 | 61 | 62 | Mirror mode 63 | 64 | 65 | 66 | 67 | 68 | 69 | "none" 70 | 71 | 72 | Mirror offset 73 | Offset between mirrored images in normal mode 74 | 75 | 0 76 | 77 | 78 | Mirror opacity 79 | Opacity of the mirrored image in normal mode 80 | 81 | 100 82 | 83 | 84 | Mirror colors 85 | Color profile for mirrored images 86 | 0 87 | 88 | 89 | Sync mirror 90 | Sync mirror color profile with normal color profile 91 | true 92 | 93 | 94 | Mirror clones 95 | Clones to make in the overlapping mode 96 | 97 | 1 98 | 99 | 100 | Mirror ratio 101 | Scale ratio between overlapping clones 102 | 103 | 0.8 104 | 105 | 106 | Circle 107 | Whether to modify drawing modes to draw in circle 108 | false 109 | 110 | 111 | Wave - Show Inner Circle 112 | Whether to show the inner circle when using Wave mode Circle variant. 113 | false 114 | 115 | 116 | Radius 117 | Radius of base circle (in percent) 118 | 119 | 40 120 | 121 | 122 | Drawing area margin 123 | Size of gaps around drawing area (in pixels). 124 | 125 | 0 126 | 127 | 128 | Offset between items 129 | The size of spaces between elements in "levels", "particles" and "bars" modes (in percent). 130 | 131 | 10 132 | 133 | 134 | Roundness of items 135 | This setting affects "levels", "particles" and "spine" modes. 136 | 137 | 10 138 | 139 | 140 | Thickness of lines 141 | Thickness of lines when filling is off (in pixels). 142 | 143 | 15 144 | 145 | 146 | Filling 147 | Whether shapes should be filled or outlined. 148 | true 149 | 150 | 151 | Number of bars 152 | Number of bars in CAVA config 153 | 154 | 12 155 | 156 | 157 | Automatic sensitivity 158 | Attempt to decrease sensitivity if the bars peak. 159 | true 160 | 161 | 162 | Sensitivity 163 | Manual sensitivity. If automatic sensitivity is enabled, this will only be the initial value. 164 | 165 | 10.0 166 | 167 | 168 | Channels 169 | Mono or stereo 170 | 171 | 172 | 173 | 174 | "stereo" 175 | 176 | 177 | Smoothing 178 | 179 | 180 | 181 | 182 | "monstercat" 183 | 184 | 185 | Noise reduction 186 | This factor adjusts the integral and gravity filters to keep the signal smooth. 1 will be very slow and smooth, 0 will be fast but noisy. 187 | 188 | 0.77 189 | 190 | 191 | Reverse order 192 | true 193 | 194 | 195 | Frames per Second 196 | 197 | 60 198 | 199 | 200 | Widgets style 201 | Style used by Adwaita widgets. 202 | 203 | 204 | 205 | 206 | "dark" 207 | 208 | 209 | Sharp corners 210 | Whether the main window corners should be sharp. 211 | false 212 | 213 | 214 | Color Profiles 215 | [] 216 | 217 | 218 | Index of active color profile 219 | 0 220 | 221 | 222 | Color animation 223 | Whether the color animation is active 224 | false 225 | 226 | 227 | Color animation target 228 | The target color profile to fade in and out of 229 | 0 230 | 231 | 232 | Color animation mirror target 233 | The target color profile to fade in and out of for the mirrored image 234 | 0 235 | 236 | 237 | Color animation length 238 | The length in seconds of the color animation 239 | 240 | 5 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /assets/io.github.TheWisker.Cavasik.metainfo.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.TheWisker.Cavasik 4 | CC0-1.0 5 | GPL-3.0 6 | Cavasik 7 | Audio visualizer based on CAVA 8 | 9 |

Cavasik is an audio visualizer based on CAVA

10 |

Cavasik is a fork of Cavalier.

11 | 23 |
24 | io.github.TheWisker.Cavasik.desktop 25 | https://github.com/TheWisker/Cavasik 26 | https://github.com/TheWisker/Cavasik/issues 27 | TheWisker 28 | Cavasik 29 | 30 | 31 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/main.png 32 | 33 | 34 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/waves.png 35 | 36 | 37 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/levels.png 38 | 39 | 40 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/particles.png 41 | 42 | 43 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/spine.png 44 | 45 | 46 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/bars.png 47 | 48 | 49 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/wave_circle.png 50 | 51 | 52 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/bars_circle.png 53 | 54 | 55 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/mirror_normal.png 56 | 57 | 58 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/mirror_inverted.png 59 | 60 | 61 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/mirror_overlapping.png 62 | 63 | 64 | https://raw.githubusercontent.com/TheWisker/Cavasik/master/assets/screenshots/direction_top.png 65 | 66 | 67 | 68 | 69 | 70 |

Second release of Cavasik

71 |

Cavasik is an audio visualizer based on CAVA with customizable LibAdwaita interface.

72 |

Cavasik is a fork of Cavalier.

73 |
    74 |
  • Some minor changes!
  • 75 |
76 |
77 |
78 | 79 | 80 |

First release of Cavasik

81 |

Cavasik is an audio visualizer based on CAVA with customizable LibAdwaita interface.

82 |

Cavasik is a fork of Cavalier.

83 |
    84 |
  • Five normal drawing modes!
  • 85 |
  • Two circle drawing modes!
  • 86 |
  • Three mirror drawing modes!
  • 87 |
  • Four drawing directions!
  • 88 |
  • Set a single color or up to a 10 color linear gradient for background and foreground!
  • 89 |
  • Select different foreground colors for the mirrored images in mirror mode!
  • 90 |
  • Set up a color animation that changes the colors gradually in a loop!
  • 91 |
  • Configure smoothing, noise reduction and a few other CAVA settings!
  • 92 |
  • Change background and foreground colors through a DBus interface!
  • 93 |
94 |
95 |
96 |
97 | 98 | 99 | pointing 100 | keyboard 101 | touch 102 | 103 | 104 | 360 105 | 106 | 107 | workstation 108 | mobile 109 | 110 |
111 | -------------------------------------------------------------------------------- /assets/meson.build: -------------------------------------------------------------------------------- 1 | desktop_file = i18n.merge_file( 2 | input: 'io.github.TheWisker.Cavasik.desktop.in', 3 | output: 'io.github.TheWisker.Cavasik.desktop', 4 | type: 'desktop', 5 | po_dir: '../lang', 6 | install: true, 7 | install_dir: join_paths(get_option('datadir'), 'applications') 8 | ) 9 | 10 | desktop_utils = find_program('desktop-file-validate', required: false) 11 | if desktop_utils.found() 12 | test('Validate desktop file', desktop_utils, args: [desktop_file]) 13 | endif 14 | 15 | appstream_file = i18n.merge_file( 16 | input: 'io.github.TheWisker.Cavasik.metainfo.xml.in', 17 | output: 'io.github.TheWisker.Cavasik.metainfo.xml', 18 | po_dir: '../lang', 19 | install: true, 20 | install_dir: join_paths(get_option('datadir'), 'metainfo') 21 | ) 22 | 23 | appstream_util = find_program('appstream-util', required: false) 24 | if appstream_util.found() 25 | test('Validate appstream file', appstream_util, args: ['validate-relax', appstream_file]) 26 | endif 27 | 28 | install_data('io.github.TheWisker.Cavasik.gschema.xml', install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')) 29 | 30 | gschema_util = find_program('glib-compile-schemas', required: false) 31 | if gschema_util.found() 32 | test('Validate gschema file', gschema_util, args: ['--strict', '--dry-run', meson.current_source_dir()]) 33 | endif 34 | 35 | subdir('icons') -------------------------------------------------------------------------------- /assets/metrics/base.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 |

11 | 12 | 13 | 14 | TheWisker/Cavasik 15 |

16 |
17 |
18 |
19 | 20 | 21 | 22 | Created 1 year ago 23 |
24 |
25 | 26 | 27 | 28 | Deployed 728 times 29 |
30 |
31 | 32 | 33 | 34 | 7.91 MB used 35 |
36 |
37 | 38 | 39 | 40 | 131 views in last two weeks 41 |
42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 | 66 | 67 | 68 | 2 Environments 69 |
70 |
71 | 72 | 73 | 74 | 0 added, 0 removed 75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
-------------------------------------------------------------------------------- /assets/metrics/languages.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 |

11 | 12 | 13 | 14 | 4 Languages 15 |

16 |
17 |
18 |

Most used languages

19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |
33 | 34 | 35 | 36 | Python 37 |
38 | 39 |
1.28k lines
40 |
54.6 kB
41 |
88.88%
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | Shell 50 |
51 | 52 |
111 lines
53 |
2.33 kB
54 |
3.8%
55 |
56 |
57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | Meson 65 |
66 | 67 |
87 lines
68 |
4.13 kB
69 |
6.71%
70 |
71 |
72 |
73 |
74 | 75 | 76 | 77 | CSS 78 |
79 | 80 |
17 lines
81 |
382 bytes
82 |
0.62%
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
-------------------------------------------------------------------------------- /assets/metrics/licenses.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 |

11 | 12 | 13 | 14 | Licenses 15 |

16 |
17 |
18 | 19 | 20 | 21 | Unexpected error 22 |
23 |
24 |
25 |
26 |
27 |
28 |
-------------------------------------------------------------------------------- /assets/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/profile.png -------------------------------------------------------------------------------- /assets/screenshots/bars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/bars.png -------------------------------------------------------------------------------- /assets/screenshots/bars_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/bars_circle.png -------------------------------------------------------------------------------- /assets/screenshots/direction_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/direction_top.png -------------------------------------------------------------------------------- /assets/screenshots/levels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/levels.png -------------------------------------------------------------------------------- /assets/screenshots/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/main.png -------------------------------------------------------------------------------- /assets/screenshots/mirror_column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/mirror_column.png -------------------------------------------------------------------------------- /assets/screenshots/mirror_inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/mirror_inverted.png -------------------------------------------------------------------------------- /assets/screenshots/mirror_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/mirror_normal.png -------------------------------------------------------------------------------- /assets/screenshots/mirror_overlapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/mirror_overlapping.png -------------------------------------------------------------------------------- /assets/screenshots/particles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/particles.png -------------------------------------------------------------------------------- /assets/screenshots/spine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/spine.png -------------------------------------------------------------------------------- /assets/screenshots/wave_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/wave_circle.png -------------------------------------------------------------------------------- /assets/screenshots/waves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/assets/screenshots/waves.png -------------------------------------------------------------------------------- /aur/cavasik-git/.SRCINFO: -------------------------------------------------------------------------------- 1 | pkgbase = cavasik-git 2 | pkgdesc = Audio visualizer based on CAVA 3 | pkgver = v2.0.1 4 | pkgrel = 2 5 | url = https://github.com/TheWisker/Cavasik 6 | arch = any 7 | license = GPL3 8 | checkdepends = appstream-glib 9 | makedepends = git 10 | makedepends = meson 11 | depends = cava 12 | depends = libadwaita 13 | depends = python-cairo 14 | depends = python-gobject 15 | depends = python-pydbus 16 | conflicts = cavasik 17 | source = git+https://github.com/TheWisker/Cavasik.git 18 | b2sums = SKIP 19 | 20 | pkgname = cavasik-git 21 | -------------------------------------------------------------------------------- /aur/cavasik-git/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: TheWisker 2 | 3 | pkgname=cavasik-git 4 | pkgbase=cavasik-git 5 | pkgver=v2.0.1 6 | pkgrel=2 7 | pkgdesc='Audio visualizer based on CAVA' 8 | arch=(any) 9 | url=https://github.com/TheWisker/Cavasik 10 | license=('GPL3') 11 | depends=(cava libadwaita python-cairo python-gobject python-pydbus) 12 | makedepends=(git meson) 13 | checkdepends=(appstream-glib) 14 | optdepends=() 15 | provides=() 16 | conflicts=("${pkgname%-git}") 17 | replaces=() 18 | backup=() 19 | options=() 20 | install= 21 | changelog= 22 | source=("git+$url.git") 23 | b2sums=('SKIP') 24 | 25 | pkgver() { 26 | echo "$pkgver.r$pkgrel" 27 | } 28 | 29 | build() { 30 | arch-meson Cavasik build 31 | meson compile -C build 32 | } 33 | 34 | check() { 35 | meson test -C build --print-errorlog 36 | } 37 | 38 | package() { 39 | DESTDIR="$pkgdir" meson install -C build 40 | install -Dm644 Cavasik/LICENSE -t "$pkgdir/usr/share/licenses/${pkgname%-git}" 41 | } 42 | -------------------------------------------------------------------------------- /aur/cavasik/.SRCINFO: -------------------------------------------------------------------------------- 1 | pkgbase = cavasik 2 | pkgdesc = Audio visualizer based on CAVA 3 | pkgver = v2.0.1 4 | pkgrel = 1 5 | url = https://github.com/TheWisker/Cavasik 6 | arch = any 7 | license = GPL3 8 | checkdepends = appstream-glib 9 | makedepends = git 10 | makedepends = meson 11 | depends = cava 12 | depends = libadwaita 13 | depends = python-gobject 14 | depends = python-cairo 15 | depends = python-pydbus 16 | conflicts = cavasik-git 17 | source = git+https://github.com/TheWisker/Cavasik#tag=v2.0.1 18 | b2sums = SKIP 19 | 20 | pkgname = cavasik 21 | -------------------------------------------------------------------------------- /aur/cavasik/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: TheWisker 2 | 3 | pkgname=cavasik 4 | pkgbase=cavasik 5 | pkgver=v2.0.1 6 | pkgrel=1 7 | pkgdesc='Audio visualizer based on CAVA' 8 | arch=(any) 9 | url=https://github.com/TheWisker/${pkgname^} 10 | license=('GPL3') 11 | depends=(cava libadwaita python-gobject python-cairo python-pydbus) 12 | makedepends=(git meson) 13 | checkdepends=(appstream-glib) 14 | optdepends=() 15 | provides=() 16 | conflicts=("${pkgname}-git") 17 | replaces=() 18 | backup=() 19 | options=() 20 | install= 21 | changelog= 22 | source=("git+$url#tag=$pkgver") 23 | b2sums=('SKIP') 24 | 25 | build() { 26 | arch-meson ${pkgname^} build 27 | meson compile -C build 28 | } 29 | 30 | check() { 31 | meson test -C build --print-errorlog 32 | } 33 | 34 | package() { 35 | DESTDIR="$pkgdir" meson install -C build 36 | install -Dm644 ${pkgname^}/LICENSE -t "$pkgdir/usr/share/licenses/$pkgname" 37 | } 38 | -------------------------------------------------------------------------------- /cavasik.doap: -------------------------------------------------------------------------------- 1 | 6 | 7 | Cavasik 8 | Audio visualizer based on CAVA 9 | 10 | 11 | 12 | 13 | Python 14 | 15 | 16 | 17 | TheWisker 18 | thewisker 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /io.github.TheWisker.Cavasik.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id" : "io.github.TheWisker.Cavasik", 3 | "runtime" : "org.gnome.Platform", 4 | "runtime-version" : "44", 5 | "sdk" : "org.gnome.Sdk", 6 | "command" : "cavasik", 7 | "finish-args" : [ 8 | "--share=ipc", 9 | "--socket=wayland", 10 | "--socket=fallback-x11", 11 | "--socket=pulseaudio", 12 | "--device=dri" 13 | ], 14 | "cleanup" : [ 15 | "/include", 16 | "/lib/pkgconfig", 17 | "/man", 18 | "/share/doc", 19 | "/share/gtk-doc", 20 | "/share/man", 21 | "/share/pkgconfig", 22 | "*.la", 23 | "*.a" 24 | ], 25 | "modules" : [ 26 | "shared-modules/linux-audio/fftw3f.json", 27 | { 28 | "name" : "iniparser", 29 | "buildsystem" : "simple", 30 | "build-commands" : 31 | [ 32 | "make PREFIX=/app", 33 | "install -Dm0644 src/iniparser.h /app/include/iniparser.h", 34 | "install -Dm0644 src/dictionary.h /app/include/dictionary.h", 35 | "install -Dm0644 libiniparser.so.1 /app/lib/libiniparser.so.1", 36 | "ln -sf libiniparser.so.1 /app/lib/libiniparser.so" 37 | ], 38 | "sources" : [ 39 | { 40 | "type" : "git", 41 | "url" : "https://github.com/ndevilla/iniparser.git", 42 | "commit" : "deb85ad4936d4ca32cc2260ce43323d47936410d" 43 | } 44 | ] 45 | }, 46 | { 47 | "name" : "cava", 48 | "sources" : [ 49 | { 50 | "type" : "git", 51 | "url" : "https://github.com/karlstav/cava.git", 52 | "tag" : "0.8.3", 53 | "commit" : "746a3b1e6021e383aea9d0000f49d71fb24e1856" 54 | } 55 | ] 56 | }, 57 | { 58 | "name": "python3-pydbus", 59 | "buildsystem": "simple", 60 | "build-commands": [ 61 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pydbus\" --no-build-isolation" 62 | ], 63 | "sources": [ 64 | { 65 | "type": "file", 66 | "url": "https://files.pythonhosted.org/packages/92/56/27148014c2f85ce70332f18612f921f682395c7d4e91ec103783be4fce00/pydbus-0.6.0-py2.py3-none-any.whl", 67 | "sha256": "66b80106352a718d80d6c681dc2a82588048e30b75aab933e4020eb0660bf85e" 68 | } 69 | ] 70 | }, 71 | { 72 | "name" : "cavasik", 73 | "builddir" : true, 74 | "buildsystem" : "meson", 75 | "sources" : [ 76 | { 77 | "type" : "dir", 78 | "path" : "." 79 | } 80 | ] 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /lang/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

Cavasik

2 |
3 | 4 | 5 | 6 |
7 |

Audio visualizer based on CAVA

8 | 9 |

New Translations

10 | 11 | If you want to make a translation, thank you, you're awesome! 12 | 13 | Here are the instructions to make a translation: 14 | 15 | - Make a **fork** of the repository. 16 | - **Clone** said fork. 17 | - (**Optional**) Use a **specialized editor** like [poedit](https://poedit.net/). 18 | - Create a **`.po`** file in the `lang` directory. 19 | - **Copy** the contents of `cavasik.pot` **template** to said file. 20 | - Work with the new `.po` file using a normal or a specialized editor. 21 | - When the translation is **finished**, add the **locale code** to the `LINGUAS` file keeping it **alphabetically** sorted. 22 | - Then, to take **authorship**, **edit** the `CREDITS.json` file adding a **new entry** following the same pattern as the other ones. If you made multiple translations use an array of languages in the "lang" entry. 23 | - **Add**, **commit** and **push** the changes to the forked repository. 24 | - Lastly, open a new pull request with the new changes made! 25 | 26 | Thank you for being willing to sacrifice your time to help this project grow! 27 | 28 |

Author

29 |
30 | 31 | 32 | 33 |
34 |

TheWisker

-------------------------------------------------------------------------------- /lang/CREDITS.json: -------------------------------------------------------------------------------- 1 | { 2 | "Fyodor Sobolev": { 3 | "lang": "Russian", 4 | "url": "https://github.com/fsobolev" 5 | }, 6 | "Heimen Stoffels": { 7 | "lang": "Dutch", 8 | "url": "https://github.com/Vistaus" 9 | }, 10 | "Albano Battistella": { 11 | "lang": "Italian", 12 | "url": "https://github.com/albanobattistella" 13 | }, 14 | "gregorni": { 15 | "lang": "German", 16 | "url": "https://gitlab.com/gregorni" 17 | }, 18 | "Irénée Thirion": { 19 | "lang": "French", 20 | "url": "https://github.com/rene-coty" 21 | }, 22 | "Santiago F.": { 23 | "lang": "Spanish", 24 | "email": "sf@santyf.com" 25 | }, 26 | "TheWisker": { 27 | "lang": "Japanese", 28 | "url": "https://github.com/TheWisker" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lang/LINGUAS: -------------------------------------------------------------------------------- 1 | # List of avaiable translations (sorted alphabetically) 2 | de 3 | es 4 | fr 5 | it 6 | jp 7 | nl 8 | pt_BR 9 | ru 10 | -------------------------------------------------------------------------------- /lang/POTFILES.in: -------------------------------------------------------------------------------- 1 | # POTFILES.in 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | # 9 | 10 | # Data files 11 | assets/io.github.TheWisker.Cavasik.desktop.in 12 | assets/io.github.TheWisker.Cavasik.metainfo.xml.in 13 | assets/io.github.TheWisker.Cavasik.gschema.xml 14 | 15 | # Source files 16 | src/main.py 17 | src/window.py 18 | src/preferences_window.py 19 | src/shortcuts_dialog.ui 20 | -------------------------------------------------------------------------------- /lang/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('cavasik', preset: 'glib') 2 | 3 | install_data('CREDITS.json', install_dir: pkgdatadir) -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('cavasik', version: 'v2.0.1', meson_version: '>= 0.59.0', default_options: ['warning_level=2', 'werror=false']) 2 | 3 | i18n = import('i18n') 4 | gnome = import('gnome') 5 | 6 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 7 | 8 | subdir('assets') 9 | subdir('src') 10 | subdir('lang') 11 | 12 | gnome.post_install(glib_compile_schemas: true, gtk_update_icon_cache: true, update_desktop_database: true) -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWisker/Cavasik/65e8eaa46383070b5de165485dc9af984a00ec62/src/__init__.py -------------------------------------------------------------------------------- /src/cava.py: -------------------------------------------------------------------------------- 1 | # cava.py 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | import os 10 | import subprocess 11 | import struct 12 | from cavasik.settings import CavasikSettings 13 | 14 | class Cava: 15 | def __init__(self): 16 | self.BYTETYPE = "H" 17 | self.BYTESIZE = 2 18 | self.BYTENORM = 65535 19 | self.restarting = False 20 | 21 | self.settings = CavasikSettings.new() 22 | self.fps = self.settings['fps'] 23 | 24 | self.sample = [] 25 | 26 | if os.getenv('XDG_CONFIG_HOME'): 27 | self.config_dir = os.getenv('XDG_CONFIG_HOME') + '/cavasik' 28 | else: 29 | self.config_dir = os.getenv('HOME') + '/.config/cavasik' 30 | if not os.path.isdir(self.config_dir): 31 | os.makedirs(self.config_dir) 32 | self.config_file_path = self.config_dir + '/config' 33 | 34 | def run(self): 35 | self.load_settings() 36 | self.write_config() 37 | self.process = subprocess.Popen(["cava", "-p", self.config_file_path], \ 38 | stdout=subprocess.PIPE) 39 | source = self.process.stdout 40 | self.restarting = False 41 | self.chunk = self.BYTESIZE * self.bars 42 | self.fmt = self.BYTETYPE * self.bars 43 | while True: 44 | data = source.read(self.chunk) 45 | if len(data) < self.chunk or self.restarting: 46 | break 47 | self.sample = \ 48 | [i / self.BYTENORM for i in struct.unpack(self.fmt, data)] 49 | 50 | def stop(self): 51 | if not self.restarting: 52 | self.process.kill() 53 | 54 | def load_settings(self): 55 | # Cava config options 56 | self.bars = self.settings['bars'] 57 | self.autosens = int(self.settings['autosens']) 58 | self.sensitivity = self.settings['sensitivity'] 59 | self.channels = self.settings['channels'] 60 | self.monstercat = \ 61 | ['off', 'monstercat'].index(self.settings['smoothing']) 62 | self.noise_reduction = self.settings['noise-reduction'] 63 | print(self.noise_reduction) 64 | 65 | def write_config(self): 66 | try: 67 | f = open(self.config_file_path, 'w') 68 | conf = '\n'.join([ 69 | '[general]', 70 | f'bars = {self.bars}', 71 | f'autosens = {self.autosens}', 72 | f'sensitivity = {self.sensitivity ** 2}', 73 | f'framerate = {self.fps}', 74 | '[input]', 75 | 'method = pulse', 76 | '[output]', 77 | f'channels = {self.channels}', 78 | 'mono_option = average', 79 | 'method = raw', 80 | 'raw_target = /dev/stdout', 81 | 'bit_format = 16bit', 82 | '[smoothing]', 83 | f'monstercat = {self.monstercat}', 84 | f'noise_reduction = {self.noise_reduction}' 85 | ]) 86 | print(conf) 87 | f.write(conf) 88 | f.close() 89 | except Exception as e: 90 | print("Can't write config file for cava...'") 91 | print(e) 92 | 93 | -------------------------------------------------------------------------------- /src/cavasik.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ../assets/css/cavasik.css 5 | shortcuts_dialog.ui 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/cavasik.in: -------------------------------------------------------------------------------- 1 | #!@PYTHON@ 2 | 3 | # cavasik.in 4 | # 5 | # Copyright (c) 2023, TheWisker 6 | # All rights reserved. 7 | # 8 | # This source code is licensed under the BSD-style license found in the 9 | # LICENSE file in the root directory of this source tree. 10 | 11 | import os 12 | import sys 13 | import signal 14 | import locale 15 | import gettext 16 | 17 | VERSION = '@VERSION@' 18 | pkgdatadir = '@pkgdatadir@' 19 | localedir = '@localedir@' 20 | 21 | sys.path.insert(1, pkgdatadir) 22 | signal.signal(signal.SIGINT, signal.SIG_DFL) 23 | locale.bindtextdomain('cavasik', localedir) 24 | locale.textdomain('cavasik') 25 | gettext.install('cavasik', localedir) 26 | 27 | if __name__ == '__main__': 28 | import gi 29 | 30 | from gi.repository import Gio 31 | resource = Gio.Resource.load(os.path.join(pkgdatadir, 'cavasik.gresource')) 32 | resource._register() 33 | 34 | from cavasik import main 35 | sys.exit(main.main(VERSION)) 36 | -------------------------------------------------------------------------------- /src/dbus.py: -------------------------------------------------------------------------------- 1 | # dbus.py 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | from pydbus import SessionBus 10 | from gi.repository import Adw 11 | from cavasik.settings import CavasikSettings 12 | 13 | class BusInterface(object): 14 | """ 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | """ 28 | 29 | def __init__(self): 30 | self.window = None 31 | self.bus = SessionBus() 32 | self.bus.publish('io.github.TheWisker.Cavasik', self) 33 | self.settings = CavasikSettings.new(self.on_settings_changed) 34 | self.active = self.settings['dbus-colors'] 35 | 36 | def set_fg_colors(self, path): 37 | if self.active: 38 | return self._change_colors(path, True) # True for foreground colors 39 | return False 40 | 41 | def set_bg_colors(self, path): 42 | if self.active: 43 | return self._change_colors(path, False) # False for background colors 44 | return False 45 | 46 | def _change_colors(self, path, cid): 47 | try: 48 | colors = [] 49 | with open(path, "r") as file: 50 | for line in file: 51 | line = line.strip() # Remove leading/trailing whitespaces 52 | if line: # Skip empty lines 53 | color = [float(c) for c in line.split(",")] 54 | color += [self.settings['dbus-opacity']/100] if len(color) < 4 else [] 55 | colors.append(tuple(color)) 56 | profiles = self.settings['color-profiles'] 57 | profiles[0] = (profiles[0][0], colors if cid else profiles[0][1], profiles[0][2] if cid else colors) 58 | self.settings['active-color-profile'] = 0 59 | self.settings['color-profiles'] = profiles 60 | return True 61 | except FileNotFoundError: 62 | print(f"File not found: {path}") 63 | except IOError as e: 64 | print(f"An error occurred while reading the file: {e}") 65 | except Exception as e: 66 | print(f"An unexpected error occurred: {e}") 67 | return False 68 | 69 | def on_settings_changed(self): 70 | self.active = self.settings['dbus-colors'] 71 | 72 | -------------------------------------------------------------------------------- /src/draw_functions.py: -------------------------------------------------------------------------------- 1 | # draw_functions.py 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | import cairo 10 | import math 11 | 12 | def set_source(cr, height, colors, offset=0): 13 | if len(colors) > 1: 14 | pat = cairo.LinearGradient(0.0, height * offset, 0.0, \ 15 | height + height * offset) 16 | for i in range(len(colors)): 17 | (red, green, blue, alpha) = colors[i] 18 | pat.add_color_stop_rgba(1 / (len(colors) - 1) * i, \ 19 | red / 255, green / 255, blue / 255, alpha) 20 | cr.set_source(pat) 21 | else: 22 | (red, green, blue, alpha) = colors[0] 23 | cr.set_source_rgba(red / 255, green / 255, blue / 255, alpha) 24 | 25 | def set_source_radial(cr, x, y, r0, r1, colors): 26 | if len(colors) > 1: 27 | pat = cairo.RadialGradient(x, y, r0, x, y, r1) 28 | for i in range(len(colors)): 29 | (red, green, blue, alpha) = colors[i] 30 | pat.add_color_stop_rgba(1 / (i + 1), \ 31 | red / 255, green / 255, blue / 255, alpha) 32 | cr.set_source(pat) 33 | else: 34 | (red, green, blue, alpha) = colors[0] 35 | cr.set_source_rgba(red / 255, green / 255, blue / 255, alpha) 36 | 37 | def draw_element(cr, x, y, width, height, radius): 38 | degrees = math.pi / 180.0 39 | cr.new_sub_path() 40 | cr.arc(x + width * radius / 100, y + height * radius / 100, \ 41 | radius * min(width, height) / 100, -180 * degrees, -90 * degrees) 42 | cr.arc(x + width - width * radius / 100, y + height * radius / 100, \ 43 | radius * min(width, height) / 100, -90 * degrees, 0) 44 | cr.arc(x + width - width * radius / 100, y + height - height * radius / 100, \ 45 | radius * min(width, height) / 100, 0, 90 * degrees) 46 | cr.arc(x + width * radius / 100, y + height - height * radius / 100, \ 47 | radius * min(width, height) / 100, 90 * degrees, -180 * degrees) 48 | cr.close_path() 49 | 50 | def wave(sample, cr, width, height, colors, fill, thickness): 51 | set_source(cr, height, colors) 52 | ls = len(sample) 53 | cr.move_to(0, (1.0 - sample[0]) * height) 54 | for i in range(ls - 1): 55 | height_diff = (sample[i] - sample[i+1]) 56 | cr.rel_curve_to(width / (ls - 1) * 0.5, 0.0, \ 57 | width / (ls - 1) * 0.5, height_diff * height, \ 58 | width / (ls - 1), height_diff * height) 59 | if fill: 60 | cr.line_to(width, height) 61 | cr.line_to(0, height) 62 | cr.fill() 63 | else: 64 | cr.set_line_width(thickness) 65 | cr.stroke() 66 | 67 | def wave_circle(sample, cr, width, height, colors, radius, fill, thickness, inner_circle): 68 | ls = len(sample) 69 | cr.move_to(width / 2, height / 2) 70 | min_radius = min(width, height) * radius / 400 71 | max_radius = min(width, height) / 2 72 | set_source_radial(cr, width / 2, height / 2, min_radius, \ 73 | max_radius, colors) 74 | if inner_circle: 75 | cr.rectangle(0, 0, width, height) 76 | cr.arc_negative(width / 2, height / 2, min_radius - thickness / 2, \ 77 | 2 * math.pi, 0) 78 | cr.clip() 79 | cr.new_path() 80 | cr.move_to(width / 2 + min_radius, height / 2) 81 | cr.arc(width / 2, height / 2, min_radius, 0, 2 * math.pi) 82 | cr.set_line_width(thickness) 83 | cr.stroke() 84 | min_radius += thickness 85 | cr.move_to( \ 86 | width / 2 + math.cos(2 * math.pi / ls * (ls - 0.5) - 0.5 * math.pi) * \ 87 | (min_radius + sample[-1] * (max_radius - min_radius)), \ 88 | height / 2 + math.sin(2 * math.pi / ls * (ls - 0.5) - 0.5 * math.pi) * \ 89 | (min_radius + sample[-1] * (max_radius - min_radius)) 90 | ) 91 | cr.curve_to( \ 92 | width / 2 + math.cos(2 * math.pi / ls * (0) - 0.5 * math.pi) * \ 93 | (min_radius + sample[-1] * (max_radius - min_radius)), \ 94 | height / 2 + math.sin(2 * math.pi / ls * (0) - 0.5 * math.pi) * \ 95 | (min_radius + sample[-1] * (max_radius - min_radius)), \ 96 | width / 2 + math.cos(2 * math.pi / ls * (0) - 0.5 * math.pi) * \ 97 | (min_radius + sample[0] * (max_radius - min_radius)), \ 98 | height / 2 + math.sin(2 * math.pi / ls * (0) - 0.5 * math.pi) * \ 99 | (min_radius + sample[0] * (max_radius - min_radius)), \ 100 | width / 2 + math.cos(2 * math.pi / ls * (0.5) - 0.5 * math.pi) * \ 101 | (min_radius + sample[0] * (max_radius - min_radius)), \ 102 | height / 2 + math.sin(2 * math.pi / ls * (0.5) - 0.5 * math.pi) * \ 103 | (min_radius + sample[0] * (max_radius - min_radius)) 104 | ) 105 | for i in range(ls - 1): 106 | cr.curve_to( \ 107 | width / 2 + math.cos(2 * math.pi / ls * (i + 1) - 0.5 * math.pi) * \ 108 | (min_radius + sample[i] * (max_radius - min_radius)), \ 109 | height / 2 + math.sin(2 * math.pi / ls * (i + 1) - 0.5 * math.pi) * \ 110 | (min_radius + sample[i] * (max_radius - min_radius)), \ 111 | width / 2 + math.cos(2 * math.pi / ls * (i + 1) - 0.5 * math.pi) * \ 112 | (min_radius + sample[i+1] * (max_radius - min_radius)), \ 113 | height / 2 + math.sin(2 * math.pi / ls * (i + 1) - 0.5 * math.pi) * \ 114 | (min_radius + sample[i+1] * (max_radius - min_radius)), \ 115 | width / 2 + math.cos(2 * math.pi / ls * (i + 1.5) - 0.5 * math.pi) * \ 116 | (min_radius + sample[i+1] * (max_radius - min_radius)), \ 117 | height / 2 + math.sin(2 * math.pi / ls * (i + 1.5) - 0.5 * math.pi) * \ 118 | (min_radius + sample[i+1] * (max_radius - min_radius)) 119 | ) 120 | cr.close_path() # required to avoid artifact with thick lines 121 | cr.set_line_width(thickness) 122 | 123 | cr.fill() if fill else cr.stroke() 124 | 125 | 126 | def levels(sample, cr, width, height, colors, offset, radius, fill, thickness): 127 | set_source(cr, height, colors) 128 | ls = len(sample) 129 | step = width / ls 130 | offset_px = step * offset / 100 131 | if fill: 132 | thickness = 0 133 | else: 134 | thickness = min(thickness, \ 135 | (step - offset_px * 2) / 2, 136 | (height / 10 - offset_px) / 2) 137 | cr.set_line_width(thickness) 138 | for i in range(ls): 139 | q = int(round(sample[i], 1) * 10) 140 | for r in range(q): 141 | draw_element(cr, step * i + offset_px + thickness / 2, \ 142 | height - (height / 10 * (r + 1)) + offset_px / 2 + thickness / 2, \ 143 | max(step - offset_px * 2 - thickness, 1), \ 144 | max(height / 10 - offset_px - thickness, 1), radius) 145 | cr.fill() if fill else cr.stroke() 146 | 147 | def particles(sample, cr, width, height, colors, offset, radius, fill, thickness): 148 | set_source(cr, height, colors) 149 | ls = len(sample) 150 | step = width / ls 151 | offset_px = step * offset / 100 152 | if fill: 153 | thickness = 0 154 | else: 155 | thickness = min(thickness, \ 156 | (step - offset_px * 2) / 2, 157 | (height / 10) / 2) 158 | cr.set_line_width(thickness) 159 | for i in range(ls): 160 | draw_element(cr, step * i + offset_px + thickness / 2, \ 161 | height * 0.9 - height * 0.9 * sample[i] + thickness / 2, \ 162 | max(step - offset_px * 2 - thickness, 1), \ 163 | max(height / 10 - thickness, 1), radius) 164 | cr.fill() if fill else cr.stroke() 165 | 166 | def spine(sample, cr, width, height, colors, offset, radius, fill, thickness): 167 | ls = len(sample) 168 | cr.set_line_width(thickness) 169 | step = width / ls 170 | for i in range(ls): 171 | set_source(cr, height, colors, sample[i] - 0.45) 172 | offset_px = step * offset / 100 * sample[i] 173 | if not fill: 174 | offset_px += thickness / 2 175 | draw_element(cr, \ 176 | step * i + step / 2 - sample[i] * step / 2 + offset_px, \ 177 | height / 2 - sample[i] * step / 2 + offset_px, \ 178 | step * sample[i] - offset_px * 2, \ 179 | step * sample[i] - offset_px * 2, radius) 180 | cr.fill() if fill else cr.stroke() 181 | 182 | def bars(sample, cr, width, height, colors, offset, fill, thickness): 183 | set_source(cr, height, colors) 184 | ls = len(sample) 185 | step = width / ls 186 | offset_px = step * offset / 100 187 | if fill: 188 | thickness = 0 189 | else: 190 | thickness = min(thickness, (step - offset_px * 2) / 2) 191 | cr.set_line_width(thickness) 192 | for i in range(ls): 193 | cr.rectangle(step * i + offset_px + thickness / 2, \ 194 | height - height * sample[i] + thickness / 2, \ 195 | max(step - offset_px * 2 - thickness, 1), height * sample[i] - thickness) 196 | cr.fill() if fill else cr.stroke() 197 | 198 | def bars_circle(sample, cr, width, height, colors, offset, fill, thickness, \ 199 | radius): 200 | ls = len(sample) 201 | min_radius = min(width, height) * radius / 400 202 | max_radius = min(width, height) / 2 203 | set_source_radial(cr, width / 2, height / 2, min_radius, max_radius, colors) 204 | if fill: 205 | thickness = 0 206 | thickness_inner_angle = 0 207 | else: 208 | thickness = min(thickness, \ 209 | min_radius * math.pi * 2 / ls * 0.25) 210 | cr.set_line_width(thickness) 211 | thickness_inner_angle = math.asin(math.sin(0.25 * math.pi) / \ 212 | math.sqrt(min_radius ** 2 + (thickness / 2) ** 2 - 2 * \ 213 | min_radius * thickness / 2 * math.cos(0.25 * math.pi)) * \ 214 | thickness / 2) 215 | for i in range(ls): 216 | cr.move_to(width / 2 + math.cos( \ 217 | 2 * math.pi / ls * (i + offset / 100) - 0.5 * math.pi + \ 218 | thickness_inner_angle) * (min_radius + thickness / 2), 219 | height / 2 + math.sin( \ 220 | 2 * math.pi / ls * (i + offset / 100) - 0.5 * math.pi + \ 221 | thickness_inner_angle) * (min_radius + thickness / 2)) 222 | cr.line_to(width / 2 + math.cos( \ 223 | 2 * math.pi / ls * (i + offset / 100) - 0.5 * math.pi + \ 224 | math.asin(math.sin(0.25 * math.pi) / math.sqrt((min_radius + \ 225 | max(sample[i] * (max_radius - min_radius), thickness * 2)) ** 2 + \ 226 | (thickness / 2) ** 2 - 2 * (min_radius + sample[i] * \ 227 | (max_radius - min_radius)) * thickness / 2 * \ 228 | math.cos(0.25 * math.pi)) * thickness / 2)) * \ 229 | (min_radius + max(sample[i] * \ 230 | (max_radius - min_radius), thickness * 2) - thickness / 2), \ 231 | height / 2 + math.sin( \ 232 | 2 * math.pi / ls * (i + offset / 100) - 0.5 * math.pi + \ 233 | math.asin(math.sin(0.25 * math.pi) / math.sqrt((min_radius + \ 234 | max(sample[i] * (max_radius - min_radius), thickness * 2)) ** 2 + \ 235 | (thickness / 2) ** 2 - 2 * (min_radius + sample[i] * \ 236 | (max_radius - min_radius)) * thickness / 2 * \ 237 | math.cos(0.25 * math.pi)) * thickness / 2)) * \ 238 | (min_radius + max(sample[i] * \ 239 | (max_radius - min_radius), thickness * 2) - thickness / 2)) 240 | cr.line_to(width / 2 + math.cos( \ 241 | 2 * math.pi / ls * (i + 1 - offset / 100) - 0.5 * math.pi - \ 242 | math.asin(math.sin(0.25 * math.pi) / math.sqrt((min_radius + \ 243 | max(sample[i] * (max_radius - min_radius), thickness * 2)) ** 2 + \ 244 | (thickness / 2) ** 2 - 2 * (min_radius + max(sample[i] * \ 245 | (max_radius - min_radius), thickness * 2)) * thickness / 2 * \ 246 | math.cos(0.25 * math.pi)) * thickness / 2)) * \ 247 | (min_radius + max(sample[i] * \ 248 | (max_radius - min_radius), thickness * 2) - thickness / 2), \ 249 | height / 2 + math.sin( \ 250 | 2 * math.pi / ls * (i + 1 - offset / 100) - 0.5 * math.pi - \ 251 | math.asin(math.sin(0.25 * math.pi) / math.sqrt((min_radius + \ 252 | max(sample[i] * (max_radius - min_radius), thickness * 2)) ** 2 + \ 253 | (thickness / 2) ** 2 - 2 * (min_radius + max(sample[i] * \ 254 | (max_radius - min_radius), thickness * 2)) * thickness / 2 * \ 255 | math.cos(0.25 * math.pi)) * thickness / 2)) * \ 256 | (min_radius + max(sample[i] * \ 257 | (max_radius - min_radius), thickness * 2) - thickness / 2)) 258 | cr.line_to(width / 2 + math.cos( \ 259 | 2 * math.pi / ls * (i + 1 - offset / 100) - 0.5 * math.pi - \ 260 | thickness_inner_angle) * (min_radius + thickness / 2), \ 261 | height / 2 + math.sin( \ 262 | 2 * math.pi / ls * (i + 1 - offset / 100) - 0.5 * math.pi - \ 263 | thickness_inner_angle) * (min_radius + thickness / 2)) 264 | cr.close_path() 265 | cr.fill() if fill else cr.stroke() 266 | -------------------------------------------------------------------------------- /src/drawing_area.py: -------------------------------------------------------------------------------- 1 | # drawing_area.py 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | from gi.repository import Gtk, GObject 10 | from threading import Thread 11 | from cavasik.cava import Cava 12 | from cavasik.draw_functions import * 13 | from cavasik.settings import CavasikSettings 14 | 15 | class CavasikDrawingArea(Gtk.DrawingArea): 16 | __gtype_name__ = 'CavasikDrawingArea' 17 | 18 | def __init__(self, settings, **kwargs): 19 | super().__init__(**kwargs) 20 | 21 | def new(): 22 | cda = Gtk.DrawingArea.new() 23 | cda.__class__ = CavasikDrawingArea 24 | cda.set_vexpand(True) 25 | cda.set_hexpand(True) 26 | cda.set_draw_func(cda.draw_func, None, None) 27 | cda.cava = None 28 | cda.spinner = None 29 | cda.settings = CavasikSettings.new(cda.on_settings_changed) 30 | cda.connect('unrealize', cda.on_unrealize) 31 | cda.timeout = None 32 | cda.fps = 60 33 | return cda 34 | 35 | def run(self): 36 | self.fps = self.settings['fps'] 37 | self.on_settings_changed(None) 38 | if self.cava == None: 39 | self.cava = Cava() 40 | self.cava_thread = Thread(target=self.cava.run) 41 | self.cava_thread.start() 42 | if self.spinner != None: 43 | self.spinner.set_visible(False) 44 | if self.timeout: 45 | GObject.source_remove(self.timeout) 46 | self.cava_sample = self.cava.sample 47 | self.timeout = GObject.timeout_add(1000.0 / self.fps, self.redraw) 48 | 49 | def on_settings_changed(self, key): 50 | self.draw_mode = self.settings['mode'] 51 | self.mirror = self.settings['mirror'] 52 | self.mirror_offset = self.settings['mirror-offset'] 53 | self.mirror_opacity = self.settings['mirror-opacity'] / 100 54 | self.mirror_clones = self.settings['mirror-clones'] 55 | self.mirror_ratio = self.settings['mirror-ratio'] 56 | self.circle = self.settings['circle'] 57 | self.wave_inner_circle = self.settings['wave-inner-circle'] 58 | self.radius = self.settings['radius'] 59 | self.direction = self.settings['direction'] 60 | self.set_margin_top(self.settings['margin']) 61 | self.set_margin_bottom(self.settings['margin']) 62 | self.set_margin_start(self.settings['margin']) 63 | self.set_margin_end(self.settings['margin']) 64 | self.offset = self.settings['items-offset'] 65 | self.roundness = self.settings['items-roundness'] 66 | self.thickness = self.settings['line-thickness'] 67 | self.fill = self.settings['fill'] 68 | self.reverse_order = self.settings['reverse-order'] 69 | self.channels = self.settings['channels'] 70 | self.color_animation = self.settings['color-animation'] 71 | self.color_animation_length = self.settings['color-animation-length'] 72 | try: 73 | color_profile = self.settings['color-profiles'][ \ 74 | self.settings['active-color-profile']] 75 | self.colors = color_profile[1] 76 | except: 77 | self.colors = [] 78 | 79 | if len(self.colors) == 0: 80 | self.settings['color-profiles'] = [(_('Default'), \ 81 | [(53, 132, 228, 1.0)], [])] 82 | return 83 | 84 | try: 85 | if self.settings['mirror-sync']: 86 | self.mirror_colors = self.colors 87 | else: 88 | color_profile = self.settings['color-profiles'][ \ 89 | self.settings['mirror-colors']] 90 | self.mirror_colors = color_profile[1] 91 | except: 92 | self.settings['mirror-colors'] = 0 93 | return 94 | try: 95 | color_profile = self.settings['color-profiles'][ \ 96 | self.settings['color-animation-target']] 97 | self.color_animation_target = color_profile[1] 98 | except: 99 | self.settings['color-animation-target'] = 0 100 | return 101 | try: 102 | color_profile = self.settings['color-profiles'][ \ 103 | self.settings['color-animation-mirror-target']] 104 | self.color_animation_mirror_target = color_profile[1] 105 | except: 106 | self.settings['color-animation-mirror-target'] = 0 107 | return 108 | self.animation_count = 0 109 | self.animation_vector = [] 110 | self.mirror_animation_vector = [] 111 | if self.color_animation: 112 | self.animation_frames = self.fps * self.color_animation_length 113 | for c, tc in zip(self.colors, self.color_animation_target): 114 | self.animation_vector.append(((tc[0] - c[0])/self.animation_frames, (tc[1] - c[1])/self.animation_frames, \ 115 | (tc[2] - c[2])/self.animation_frames, (tc[3] - c[3])/self.animation_frames)) 116 | for c, tc in zip(self.colors, self.color_animation_mirror_target): 117 | self.mirror_animation_vector.append(((tc[0] - c[0])/self.animation_frames, (tc[1] - c[1])/self.animation_frames, \ 118 | (tc[2] - c[2])/self.animation_frames, (tc[3] - c[3])/self.animation_frames)) 119 | 120 | if key in ('bars', 'autosens', 'sensitivity', 'channels', \ 121 | 'smoothing', 'noise-reduction'): 122 | if not self.cava.restarting: 123 | self.cava.stop() 124 | self.cava.restarting = True 125 | if self.spinner != None: 126 | self.spinner.set_visible(True) 127 | self.cava.sample = [] 128 | GObject.timeout_add_seconds(3, self.run) 129 | 130 | def draw_func(self, area, cr, width, height, _, mirror_call): 131 | if len(self.cava_sample) > 0: 132 | if not mirror_call: 133 | if self.color_animation: 134 | self.animation_count += 1 135 | for i, (c, vc) in enumerate(zip(self.colors, self.animation_vector)): 136 | self.colors = self.colors[:i] + [(c[0] + vc[0], c[1] + vc[1], \ 137 | c[2] + vc[2], c[3] + vc[3])] + self.colors[i+1:] 138 | for i, (c, vc) in enumerate(zip(self.mirror_colors, self.mirror_animation_vector)): 139 | self.mirror_colors = self.mirror_colors[:i] + [(c[0] + vc[0], c[1] + vc[1], \ 140 | c[2] + vc[2], c[3] + vc[3])] + self.mirror_colors[i+1:] 141 | if self.animation_count >= self.animation_frames: 142 | self.animation_count = 0 143 | for i, vc in enumerate(self.animation_vector): 144 | self.animation_vector = self.animation_vector[:i] + [(-vc[0], -vc[1], -vc[2], -vc[3])] + self.animation_vector[i+1:] 145 | for i, vc in enumerate(self.mirror_animation_vector): 146 | self.mirror_animation_vector = self.mirror_animation_vector[:i] + [(-vc[0], -vc[1], -vc[2], -vc[3])] + self.mirror_animation_vector[i+1:] 147 | 148 | if self.direction == 'left-right': 149 | cr.rotate(0.5*math.pi) 150 | cr.translate(0, -width) 151 | width, height = height, width 152 | elif self.direction == 'top-bottom': 153 | cr.rotate(math.pi) 154 | cr.translate(-width, -height) 155 | elif self.direction == 'right-left': 156 | cr.rotate(1.5*math.pi) 157 | cr.translate(-height, 0) 158 | width, height = height, width 159 | 160 | if not self.circle and not self.draw_mode == 'spine': 161 | if self.mirror == 'normal': 162 | cr.scale(1, -0.5*(1-self.mirror_offset/2)) 163 | cr.translate(0, (1/(-0.5*(1-self.mirror_offset/2)))*height+(self.mirror_offset*(1/(-0.5*(1-self.mirror_offset/2)))*height/10)) 164 | prev_colors = [c for c in self.colors] 165 | self.colors = [c for c in self.mirror_colors] 166 | if not self.mirror_opacity == 1: 167 | for c,color in enumerate(self.colors): 168 | self.colors[c] = (self.colors[c][0], self.colors[c][1], self.colors[c][2], self.mirror_opacity) 169 | self.draw_func(area, cr, width, height, _, True) 170 | self.colors = prev_colors 171 | cr.scale(1, -1) 172 | cr.translate(0, (1/(-0.5*(1-self.mirror_offset/2)))*height+(self.mirror_offset*(1/(-0.5*(1-self.mirror_offset/2)))*height/5)+1) 173 | elif self.mirror == 'inverted': 174 | cr.scale(1, -0.5) 175 | cr.translate(0, -height) 176 | prev_colors = [c for c in self.colors] 177 | self.colors = [c for c in self.mirror_colors] 178 | self.draw_func(area, cr, width, height, _, True) 179 | self.colors = prev_colors 180 | cr.scale(1, -1) 181 | elif self.mirror == 'overlapping': 182 | self.draw_func(area, cr, width, height, _, True) 183 | for i in range(1, self.mirror_clones+1): 184 | cr.scale(1, self.mirror_ratio**i) 185 | cr.translate(0, (1 - self.mirror_ratio**i)/(self.mirror_ratio**i) * height) 186 | if i % 2 == 1: 187 | prev_colors = [c for c in self.colors] 188 | self.colors = [c for c in self.mirror_colors] 189 | self.draw_func(area, cr, width, height, _, True) 190 | if i % 2 == 1: 191 | self.colors = prev_colors 192 | 193 | if self.draw_mode == 'wave': 194 | if self.circle: 195 | wave_circle(self.cava_sample, cr, width, height, \ 196 | self.colors, self.radius, self.fill, self.thickness, \ 197 | self.wave_inner_circle) 198 | else: 199 | wave(self.cava_sample, cr, width, height, self.colors, \ 200 | self.fill, self.thickness) 201 | elif self.draw_mode == 'levels': 202 | levels(self.cava_sample, cr, width, height, self.colors, \ 203 | self.offset, self.roundness, self.fill, self.thickness) 204 | elif self.draw_mode == 'particles': 205 | particles(self.cava_sample, cr, width, height, self.colors, \ 206 | self.offset, self.roundness, self.fill, self.thickness) 207 | elif self.draw_mode == 'spine': 208 | spine(self.cava_sample, cr, width, height, self.colors, \ 209 | self.offset, self.roundness, self.fill, self.thickness) 210 | elif self.draw_mode == 'bars': 211 | if self.circle: 212 | bars_circle(self.cava_sample, cr, width, height, \ 213 | self.colors, self.offset, self.fill, \ 214 | self.thickness, self.radius) 215 | else: 216 | bars(self.cava_sample, cr, width, height, self.colors, \ 217 | self.offset, self.fill, self.thickness) 218 | 219 | 220 | def redraw(self): 221 | self.cava_sample = self.cava.sample 222 | if self.reverse_order: 223 | if self.channels == 'mono': 224 | self.cava_sample = self.cava_sample[::-1] 225 | else: 226 | self.cava_sample = \ 227 | self.cava_sample[0:int(len(self.cava_sample)/2):][::-1] + \ 228 | self.cava_sample[int(len(self.cava_sample)/2)::][::-1] 229 | self.queue_draw() 230 | return True 231 | 232 | def on_unrealize(self, obj): 233 | self.cava.stop() 234 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # main.py 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | import sys 10 | import gi 11 | 12 | gi.require_version('Gtk', '4.0') 13 | gi.require_version('Adw', '1') 14 | 15 | from gi.repository import Gtk, Gio, Adw 16 | from .window import CavasikWindow 17 | from .preferences_window import CavasikPreferencesWindow 18 | from .translation_credits import get_translation_credits 19 | 20 | from .dbus import BusInterface 21 | 22 | class CavasikApplication(Adw.Application): 23 | """The main application singleton class""" 24 | 25 | def __init__(self, version): 26 | super().__init__(application_id='io.github.TheWisker.Cavasik', 27 | flags=Gio.ApplicationFlags.FLAGS_NONE) 28 | self.version = version 29 | self.create_action('about', self.on_about_action) 30 | self.create_action('open-menu', self.on_menu_action, ['F10']) 31 | self.create_action('toggle-fullscreen', self.on_fullscreen_action, ['F11']) 32 | self.create_action('preferences', self.on_preferences_action, 33 | ['P']) 34 | self.create_action('shortcuts', self.on_shortcuts_action, \ 35 | ['H']) 36 | self.create_action('close', self.on_close_action, ['w']) 37 | self.create_action('quit', self.on_quit_action, ['q']) 38 | self.bus_interface = BusInterface() 39 | 40 | def do_activate(self): 41 | """Called when the application is activated. 42 | 43 | We raise the application's main window, creating it if 44 | necessary. 45 | """ 46 | self.win = self.props.active_window 47 | if not self.win: 48 | self.win = CavasikWindow(application=self) 49 | self.win.present() 50 | 51 | def on_about_action(self, *args): 52 | """Callback for the app.about action.""" 53 | about = Adw.AboutWindow(transient_for=self.props.active_window, 54 | application_name='Cavasik', 55 | application_icon='io.github.TheWisker.Cavasik', 56 | developer_name=_('TheWisker'), 57 | version=self.version, 58 | developers=[_('TheWisker')], 59 | copyright='© 2023 TheWisker', 60 | website='https://github.com/TheWisker/Cavasik', 61 | issue_url='https://github.com/TheWisker/Cavasik/issues', 62 | license_type=Gtk.License.GPL_3_0, 63 | translator_credits=get_translation_credits()) 64 | about.present() 65 | 66 | def on_preferences_action(self, widget, _): 67 | self.pref_win = None 68 | for w in self.get_windows(): 69 | if type(w) == CavasikPreferencesWindow: 70 | self.pref_win = w 71 | break 72 | if not self.pref_win: 73 | self.pref_win = CavasikPreferencesWindow(application=self) 74 | self.pref_win.present() 75 | 76 | def on_shortcuts_action(self, widget, _): 77 | self.shortcuts_win = None 78 | for w in self.get_windows(): 79 | if type(w) == Gtk.ShortcutsWindow: 80 | self.shortcuts_win = w 81 | break 82 | if not self.shortcuts_win: 83 | builder = Gtk.Builder.new_from_resource( \ 84 | '/io/github/TheWisker/Cavasik/shortcuts_dialog.ui') 85 | self.shortcuts_win = builder.get_object('dialog') 86 | self.shortcuts_win.present() 87 | 88 | def on_quit_action(self, widget, _): 89 | self.win.close() 90 | self.quit() 91 | 92 | def on_close_action(self, widget, _): 93 | win = self.props.active_window 94 | win.close() 95 | if type(win) == CavasikWindow: 96 | self.quit() 97 | 98 | def on_menu_action(self, widget, _): 99 | self.win.menu_button.activate() 100 | 101 | def on_fullscreen_action(self, widget, _): 102 | self.win.unfullscreen() if self.win.is_fullscreen() else self.win.fullscreen() 103 | 104 | def create_action(self, name, callback, shortcuts=None): 105 | """Add an application action. 106 | 107 | Args: 108 | name: the name of the action 109 | callback: the function to be called when the action is 110 | activated 111 | shortcuts: an optional list of accelerators 112 | """ 113 | action = Gio.SimpleAction.new(name, None) 114 | action.connect("activate", callback) 115 | self.add_action(action) 116 | if shortcuts: 117 | self.set_accels_for_action(f"app.{name}", shortcuts) 118 | 119 | 120 | def main(version): 121 | """The application's entry point.""" 122 | app = CavasikApplication(version) 123 | return app.run(sys.argv) 124 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | gnome = import('gnome') 2 | python = import('python') 3 | 4 | moduledir = join_paths(pkgdatadir, 'cavasik') 5 | 6 | cavasik_sources = [ 7 | '__init__.py', 8 | 'main.py', 9 | 'window.py', 10 | 'cava.py', 11 | 'drawing_area.py', 12 | 'draw_functions.py', 13 | 'settings.py', 14 | 'settings_import_export.py', 15 | 'preferences_window.py', 16 | 'shortcuts.py', 17 | 'dbus.py' 18 | ] 19 | 20 | conf = configuration_data() 21 | conf.set('PYTHON', python.find_installation('python3').path()) 22 | conf.set('VERSION', meson.project_version()) 23 | conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) 24 | conf.set('pkgdatadir', pkgdatadir) 25 | 26 | install_data(cavasik_sources, install_dir: moduledir) 27 | gnome.compile_resources('cavasik', 'cavasik.gresource.xml', gresource_bundle: true, install: true, install_dir: pkgdatadir) 28 | configure_file(input: 'cavasik.in', output: 'cavasik', configuration: conf, install: true, install_dir: get_option('bindir')) 29 | configure_file(input: 'translation_credits.py.in', output: 'translation_credits.py', configuration: conf, install: true, install_dir: moduledir) -------------------------------------------------------------------------------- /src/settings.py: -------------------------------------------------------------------------------- 1 | # settings.py 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | from gi.repository import Gio, GLib 10 | from inspect import signature 11 | 12 | class CavasikSettings(Gio.Settings): 13 | __gtype_name__ = 'CavasikSettings' 14 | 15 | def __init__(self): 16 | super().__init__(self) 17 | 18 | def new(callback_fn=None): 19 | gsettings = Gio.Settings.new('io.github.TheWisker.Cavasik') 20 | gsettings.__class__ = CavasikSettings 21 | if callback_fn: 22 | gsettings.connect('changed', gsettings.on_settings_changed) 23 | gsettings.callback_fn = callback_fn 24 | gsettings.callback_fn_sig_len = len(signature(callback_fn).parameters) 25 | return gsettings 26 | 27 | def on_settings_changed(self, obj, key): 28 | if self.callback_fn_sig_len > 0: 29 | self.callback_fn(key) 30 | else: 31 | self.callback_fn() 32 | 33 | -------------------------------------------------------------------------------- /src/settings_import_export.py: -------------------------------------------------------------------------------- 1 | # settings_import_export.py 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | import subprocess 10 | from gi.repository import Adw 11 | 12 | def import_settings(window, path): 13 | try: 14 | with open(path, 'r') as file: 15 | lines = file.readlines() 16 | for line in lines: 17 | if line != '\n': 18 | subprocess.run(['gsettings', 'set', \ 19 | 'io.github.TheWisker.Cavasik', line.split(' ')[0], \ 20 | line.replace(line.split(' ')[0], '').strip()]) 21 | toast_msg = _('Settings successfully imported') 22 | 23 | except Exception as e: 24 | print('Can\'t import settings from file: ' + path) 25 | print(e) 26 | toast_msg = _('Failed to import settings') 27 | 28 | window.add_toast(Adw.Toast.new(toast_msg)) 29 | 30 | 31 | def export_settings(window, path): 32 | gsettings_list = subprocess.run( \ 33 | ['gsettings', 'list-recursively', 'io.github.TheWisker.Cavasik'], \ 34 | stdout=subprocess.PIPE).stdout.decode('utf-8') 35 | try: 36 | with open(path, 'w') as file: 37 | for line in gsettings_list.split('\n'): 38 | file.write(' '.join(line.split(' ')[1::]) + '\n') 39 | toast_msg = _('File successfully saved') 40 | 41 | except Exception as e: 42 | print('Can\'t export settings to file: ' + path) 43 | print(e) 44 | toast_msg = _('Failed to save file') 45 | 46 | window.add_toast(Adw.Toast.new(toast_msg)) 47 | -------------------------------------------------------------------------------- /src/shortcuts.py: -------------------------------------------------------------------------------- 1 | # shortcuts.py 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | import gi 10 | gi.require_version('Gtk', '4.0') 11 | from gi.repository import Gio, Gtk 12 | 13 | def add_shortcuts(widget, settings): 14 | action_map = Gio.SimpleActionGroup.new() 15 | widget.insert_action_group("cavasik", action_map) 16 | shortcut_controller = Gtk.ShortcutController.new() 17 | shortcut_controller.set_scope(Gtk.ShortcutScope.MANAGED) 18 | widget.add_controller(shortcut_controller) 19 | 20 | act_next_mode = Gio.SimpleAction.new("next-mode", None) 21 | act_next_mode.connect('activate', change_option, settings, "mode", 1) 22 | action_map.add_action(act_next_mode) 23 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 24 | Gtk.ShortcutTrigger.parse_string("D"), \ 25 | Gtk.NamedAction.new("cavasik.next-mode"))) 26 | act_prev_mode = Gio.SimpleAction.new("prev-mode", None) 27 | act_prev_mode.connect('activate', change_option, settings, "mode", -1) 28 | action_map.add_action(act_prev_mode) 29 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 30 | Gtk.ShortcutTrigger.parse_string("D"), \ 31 | Gtk.NamedAction.new("cavasik.prev-mode"))) 32 | 33 | act_cycle_shape = Gio.SimpleAction.new("cycle-shape", None) 34 | act_cycle_shape.connect('activate', toggle_setting, settings, "circle") 35 | action_map.add_action(act_cycle_shape) 36 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 37 | Gtk.ShortcutTrigger.parse_string("S"), \ 38 | Gtk.NamedAction.new("cavasik.cycle-shape"))) 39 | 40 | act_next_mirror = Gio.SimpleAction.new("next-mirror", None) 41 | act_next_mirror.connect('activate', change_option, settings, "mirror", 1) 42 | action_map.add_action(act_next_mirror) 43 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 44 | Gtk.ShortcutTrigger.parse_string("M"), \ 45 | Gtk.NamedAction.new("cavasik.next-mirror"))) 46 | act_prev_mirror = Gio.SimpleAction.new("prev-mirror", None) 47 | act_prev_mirror.connect('activate', change_option, settings, "mirror", -1) 48 | action_map.add_action(act_prev_mirror) 49 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 50 | Gtk.ShortcutTrigger.parse_string("M"), \ 51 | Gtk.NamedAction.new("cavasik.prev-mirror"))) 52 | 53 | act_next_direction = Gio.SimpleAction.new("next-direction", None) 54 | act_next_direction.connect('activate', change_option, settings, "direction", 1) 55 | action_map.add_action(act_next_direction) 56 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 57 | Gtk.ShortcutTrigger.parse_string("A"), \ 58 | Gtk.NamedAction.new("cavasik.next-direction"))) 59 | act_prev_direction = Gio.SimpleAction.new("prev-direction", None) 60 | act_prev_direction.connect('activate', change_option, settings, "direction", -1) 61 | action_map.add_action(act_prev_direction) 62 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 63 | Gtk.ShortcutTrigger.parse_string("A"), \ 64 | Gtk.NamedAction.new("cavasik.prev-direction"))) 65 | 66 | act_toggle_fill = Gio.SimpleAction.new("toggle-fill", None) 67 | act_toggle_fill.connect('activate', toggle_setting, settings, 'fill') 68 | action_map.add_action(act_toggle_fill) 69 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 70 | Gtk.ShortcutTrigger.parse_string("F"), \ 71 | Gtk.NamedAction.new("cavasik.toggle-fill"))) 72 | 73 | act_toggle_dbus_colors = Gio.SimpleAction.new("toggle-dbus-colors", None) 74 | act_toggle_dbus_colors.connect('activate', toggle_setting, settings, \ 75 | 'dbus-colors') 76 | action_map.add_action(act_toggle_dbus_colors) 77 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 78 | Gtk.ShortcutTrigger.parse_string("T"), \ 79 | Gtk.NamedAction.new("cavasik.toggle-dbus-colors"))) 80 | 81 | act_toggle_controls = Gio.SimpleAction.new("toggle-controls", None) 82 | act_toggle_controls.connect('activate', toggle_setting, settings, \ 83 | 'window-controls') 84 | action_map.add_action(act_toggle_controls) 85 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 86 | Gtk.ShortcutTrigger.parse_string("O"), \ 87 | Gtk.NamedAction.new("cavasik.toggle-controls"))) 88 | 89 | act_inc_bars = Gio.SimpleAction.new("increase-bars", None) 90 | act_inc_bars.connect('activate', change_setting, settings, 'bars', 2) 91 | action_map.add_action(act_inc_bars) 92 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 93 | Gtk.ShortcutTrigger.parse_string("B"), \ 94 | Gtk.NamedAction.new("cavasik.increase-bars"))) 95 | act_dec_bars = Gio.SimpleAction.new("decrease-bars", None) 96 | act_dec_bars.connect('activate', change_setting, settings, 'bars', -2) 97 | action_map.add_action(act_dec_bars) 98 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 99 | Gtk.ShortcutTrigger.parse_string("B"), \ 100 | Gtk.NamedAction.new("cavasik.decrease-bars"))) 101 | 102 | act_inc_sensitivity = Gio.SimpleAction.new("increase-sensitivity", None) 103 | act_inc_sensitivity.connect('activate', change_setting, settings, 'sensitivity', 20) 104 | action_map.add_action(act_inc_sensitivity) 105 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 106 | Gtk.ShortcutTrigger.parse_string("V"), \ 107 | Gtk.NamedAction.new("cavasik.increase-sensitivity"))) 108 | act_dec_sensitivity = Gio.SimpleAction.new("decrease-sensitivity", None) 109 | act_dec_sensitivity.connect('activate', change_setting, settings, 'sensitivity', -20) 110 | action_map.add_action(act_dec_sensitivity) 111 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 112 | Gtk.ShortcutTrigger.parse_string("V"), \ 113 | Gtk.NamedAction.new("cavasik.decrease-sensitivity"))) 114 | 115 | act_toggle_reverse = Gio.SimpleAction.new("toggle-reverse", None) 116 | act_toggle_reverse.connect('activate', toggle_setting, settings, \ 117 | 'reverse-order') 118 | action_map.add_action(act_toggle_reverse) 119 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 120 | Gtk.ShortcutTrigger.parse_string("E"), \ 121 | Gtk.NamedAction.new("cavasik.toggle-reverse"))) 122 | 123 | act_toggle_style = Gio.SimpleAction.new("toggle-style", None) 124 | act_toggle_style.connect('activate', change_widgets_style, settings) 125 | action_map.add_action(act_toggle_style) 126 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 127 | Gtk.ShortcutTrigger.parse_string("W"), \ 128 | Gtk.NamedAction.new("cavasik.toggle-style"))) 129 | 130 | act_next_profile = Gio.SimpleAction.new("next-profile", None) 131 | act_next_profile.connect('activate', change_color_profile, settings, "color-profiles", "active-color-profile", 1) 132 | action_map.add_action(act_next_profile) 133 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 134 | Gtk.ShortcutTrigger.parse_string("C"), \ 135 | Gtk.NamedAction.new("cavasik.next-profile"))) 136 | act_prev_profile = Gio.SimpleAction.new("prev-profile", None) 137 | act_prev_profile.connect('activate', change_color_profile, settings, "color-profiles", "active-color-profile", -1) 138 | action_map.add_action(act_prev_profile) 139 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 140 | Gtk.ShortcutTrigger.parse_string("C"), \ 141 | Gtk.NamedAction.new("cavasik.prev-profile"))) 142 | 143 | act_next_mirror_profile = Gio.SimpleAction.new("next-mirror-profile", None) 144 | act_next_mirror_profile.connect('activate', change_color_profile, settings, "color-profiles", "mirror-colors", 1) 145 | action_map.add_action(act_next_mirror_profile) 146 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 147 | Gtk.ShortcutTrigger.parse_string("N"), \ 148 | Gtk.NamedAction.new("cavasik.next-mirror-profile"))) 149 | act_prev_mirror_profile = Gio.SimpleAction.new("prev-mirror-profile", None) 150 | act_prev_mirror_profile.connect('activate', change_color_profile, settings, "color-profiles", "mirror-colors", -1) 151 | action_map.add_action(act_prev_mirror_profile) 152 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 153 | Gtk.ShortcutTrigger.parse_string("N"), \ 154 | Gtk.NamedAction.new("cavasik.prev-mirror-profile"))) 155 | 156 | act_toggle_color_animation = Gio.SimpleAction.new("toggle-color-animation", None) 157 | act_toggle_color_animation.connect('activate', toggle_setting, settings, \ 158 | 'color-animation') 159 | action_map.add_action(act_toggle_color_animation) 160 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 161 | Gtk.ShortcutTrigger.parse_string("G"), \ 162 | Gtk.NamedAction.new("cavasik.toggle-color-animation"))) 163 | 164 | act_next_target_profile = Gio.SimpleAction.new("next-target-profile", None) 165 | act_next_target_profile.connect('activate', change_color_profile, settings, "color-profiles", "color-animation-target", 1) 166 | action_map.add_action(act_next_target_profile) 167 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 168 | Gtk.ShortcutTrigger.parse_string("J"), \ 169 | Gtk.NamedAction.new("cavasik.next-target-profile"))) 170 | act_prev_target_profile = Gio.SimpleAction.new("prev-target-profile", None) 171 | act_prev_target_profile.connect('activate', change_color_profile, settings, "color-profiles", "color-animation-target", -1) 172 | action_map.add_action(act_prev_target_profile) 173 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 174 | Gtk.ShortcutTrigger.parse_string("J"), \ 175 | Gtk.NamedAction.new("cavasik.prev-target-profile"))) 176 | 177 | act_next_mirror_target_profile = Gio.SimpleAction.new("next-mirror-target-profile", None) 178 | act_next_mirror_target_profile.connect('activate', change_color_profile, settings, "color-profiles", "color-animation-mirror-target", 1) 179 | action_map.add_action(act_next_mirror_target_profile) 180 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 181 | Gtk.ShortcutTrigger.parse_string("K"), \ 182 | Gtk.NamedAction.new("cavasik.next-mirror-target-profile"))) 183 | act_prev_mirror_target_profile = Gio.SimpleAction.new("prev-mirror-target-profile", None) 184 | act_prev_mirror_target_profile.connect('activate', change_color_profile, settings, "color-profiles", "color-animation-mirror-target", -1) 185 | action_map.add_action(act_prev_mirror_target_profile) 186 | shortcut_controller.add_shortcut(Gtk.Shortcut.new( \ 187 | Gtk.ShortcutTrigger.parse_string("K"), \ 188 | Gtk.NamedAction.new("cavasik.prev-mirror-target-profile"))) 189 | 190 | def change_option(action, parameter, settings, key, diff): 191 | modes = settings.get_range(key)[1] 192 | new_index = modes.index(settings[key]) + diff 193 | if new_index > len(modes) - 1: 194 | new_index = 0 195 | elif new_index < 0: 196 | new_index = len(modes) - 1 197 | settings[key] = modes[new_index] 198 | 199 | def change_widgets_style(action, parameter, settings): 200 | if settings['widgets-style'] == 'light': 201 | settings['widgets-style'] = 'dark' 202 | else: 203 | settings['widgets-style'] = 'light' 204 | 205 | def change_color_profile(action, parameter, settings, masterkey, key, diff): 206 | profiles = settings[masterkey] 207 | new_index = settings[key] + diff 208 | if new_index > len(profiles) - 1: 209 | new_index = 0 210 | elif new_index < 0: 211 | new_index = len(profiles) - 1 212 | settings[key] = new_index 213 | 214 | def change_setting(action, parameter, settings, key, diff): 215 | try: 216 | settings[key] += diff 217 | except: 218 | pass 219 | 220 | def toggle_setting(action, parameter, settings, key): 221 | settings[key] = not settings[key] 222 | -------------------------------------------------------------------------------- /src/shortcuts_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | true 7 | false 8 | 9 | 10 | 11 | 12 | Cavasik 13 | 14 | 15 | Change Shape Mode 16 | S 17 | 18 | 19 | 20 | 21 | Change Drawing Mode 22 | D <Shift>D 23 | 24 | 25 | 26 | 27 | Change Mirror Mode 28 | M <Shift>M 29 | 30 | 31 | 32 | 33 | Change Direction 34 | A <Shift>A 35 | 36 | 37 | 38 | 39 | Toggle Filling 40 | F 41 | 42 | 43 | 44 | 45 | Toggle DBus Colors 46 | T 47 | 48 | 49 | 50 | 51 | Toggle Window Controls 52 | O 53 | 54 | 55 | 56 | 57 | 58 | 59 | CAVA 60 | 61 | 62 | Change Number of Bars 63 | B <Shift>B 64 | 65 | 66 | 67 | 68 | Increase/Decrease Sensitivity 69 | V <Shift>V 70 | 71 | 72 | 73 | 74 | Toggle Reverse Order 75 | E 76 | 77 | 78 | 79 | 80 | 81 | 82 | Colors 83 | 84 | 85 | Toggle Widgets Style 86 | W 87 | 88 | 89 | 90 | 91 | Change Colors Profile 92 | C <Shift>C 93 | 94 | 95 | 96 | 97 | Change Mirror Profile 98 | N <Shift>N 99 | 100 | 101 | 102 | 103 | Toggle Color Animation 104 | G 105 | 106 | 107 | 108 | 109 | Change Target Profile 110 | J <Shift>J 111 | 112 | 113 | 114 | 115 | Change Mirror Target Profile 116 | K <Shift>K 117 | 118 | 119 | 120 | 121 | 122 | 123 | Window 124 | 125 | 126 | Open Menu 127 | F10 128 | 129 | 130 | 131 | 132 | Toggle Fullscreen 133 | F11 134 | 135 | 136 | 137 | 138 | Preferences 139 | P 140 | 141 | 142 | 143 | 144 | Show Shortcuts 145 | H 146 | 147 | 148 | 149 | 150 | Close Active Window 151 | <Ctrl>W 152 | 153 | 154 | 155 | 156 | Quit 157 | <Ctrl>Q 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /src/translation_credits.py.in: -------------------------------------------------------------------------------- 1 | # translation_credits.py 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | import json 10 | 11 | pkgdatadir= '@pkgdatadir@' 12 | 13 | def get_translation_credits(): 14 | translation_credits='' 15 | try: 16 | with open(f'{pkgdatadir}/CREDITS.json', 'r') as f: 17 | credits_json = json.load(f) 18 | for translator in sorted(credits_json.keys()): 19 | data = credits_json[translator] 20 | translation_credits += f'{translator} (' 21 | if type(data['lang']) == list: 22 | data['lang'].sort() 23 | translation_credits += ', '.join(data['lang']) 24 | else: 25 | translation_credits += data['lang'] 26 | translation_credits += ')' 27 | if 'url' in data.keys(): 28 | translation_credits += f" {data['url']}" 29 | elif 'email' in data.keys(): 30 | translation_credits += f" <{data['email']}>" 31 | translation_credits += '\n' 32 | translation_credits = translation_credits.rstrip('\n') 33 | return translation_credits 34 | except Exception as e: 35 | print('[Error] Something went wrong when opening translator credits.') 36 | print(e) 37 | return '' 38 | -------------------------------------------------------------------------------- /src/window.py: -------------------------------------------------------------------------------- 1 | # window.py 2 | # 3 | # Copyright (c) 2023, TheWisker 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | from gi.repository import Adw, Gtk, Gio, GObject 10 | 11 | from cavasik.settings import CavasikSettings 12 | from cavasik.drawing_area import CavasikDrawingArea 13 | from cavasik.shortcuts import add_shortcuts 14 | 15 | 16 | class CavasikWindow(Adw.ApplicationWindow): 17 | __gtype_name__ = 'CavasikWindow' 18 | 19 | def __init__(self, **kwargs): 20 | super().__init__(**kwargs) 21 | 22 | self.settings = CavasikSettings.new(self.on_settings_changed) 23 | self.cava_sample = [] 24 | 25 | self.build_ui() 26 | add_shortcuts(self, self.settings) 27 | self.connect('close-request', self.on_close_request) 28 | self.connect('notify::is-active', self.on_active_state_changed) 29 | 30 | def build_ui(self): 31 | self.set_title('Cavasik') 32 | self.set_size_request(170, 170) 33 | (width, height) = self.settings['size'] 34 | self.set_default_size(width, height) 35 | if self.settings['maximized']: 36 | self.maximize() 37 | self.set_name('cavasik-window') 38 | self.toggle_sharp_corners() 39 | self.set_style() 40 | self.css_provider = Gtk.CssProvider.new() 41 | self.apply_colors() 42 | 43 | self.overlay = Gtk.Overlay.new() 44 | self.set_content(self.overlay) 45 | 46 | self.main_box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 47 | self.main_box.set_hexpand(True) 48 | self.main_box.set_vexpand(True) 49 | self.overlay.add_overlay(self.main_box) 50 | 51 | self.header = Adw.HeaderBar.new() 52 | self.header.add_css_class('flat') 53 | self.header.set_show_start_title_buttons( \ 54 | self.settings['window-controls']) 55 | self.header.set_show_end_title_buttons(self.settings['window-controls']) 56 | self.header.set_title_widget(Gtk.Label.new('')) 57 | self.main_box.append(self.header) 58 | 59 | self.handle = Gtk.WindowHandle.new() 60 | self.handle.set_hexpand(True) 61 | self.handle.set_vexpand(True) 62 | self.main_box.append(self.handle) 63 | 64 | self.bin_spinner = Adw.Bin.new() 65 | self.bin_spinner.set_hexpand(True) 66 | self.bin_spinner.set_vexpand(True) 67 | self.handle.set_child(self.bin_spinner) 68 | 69 | self.spinner = Gtk.Spinner.new() 70 | self.spinner.set_spinning(True) 71 | self.spinner.set_size_request(50, -1) 72 | self.spinner.set_halign(Gtk.Align.CENTER) 73 | self.spinner.set_margin_bottom(40) # headerbar height 74 | self.bin_spinner.set_child(self.spinner) 75 | 76 | self.drawing_area = CavasikDrawingArea.new() 77 | self.drawing_area.spinner = self.spinner 78 | self.drawing_area.run() 79 | self.overlay.set_child(self.drawing_area) 80 | 81 | self.menu_button = Gtk.MenuButton.new() 82 | self.menu_button.set_valign(Gtk.Align.START) 83 | self.menu_button.set_icon_name('open-menu-symbolic') 84 | self.header.pack_start(self.menu_button) 85 | 86 | self.menu = Gio.Menu.new() 87 | self.menu.append(_('Preferences'), 'app.preferences') 88 | self.menu.append(_('Keyboard Shortcuts'), 'app.shortcuts') 89 | self.menu.append(_('About'), 'app.about') 90 | self.menu.append(_('Quit'), 'app.quit') 91 | self.menu_button.set_menu_model(self.menu) 92 | 93 | def set_style(self): 94 | if self.settings['widgets-style'] == 'light': 95 | Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.FORCE_LIGHT) 96 | else: 97 | Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.FORCE_DARK) 98 | 99 | def toggle_sharp_corners(self): 100 | if self.settings['sharp-corners']: 101 | self.add_css_class('sharp-corners') 102 | else: 103 | self.remove_css_class('sharp-corners') 104 | 105 | def apply_colors(self): 106 | try: 107 | color_profile = self.settings['color-profiles'][ \ 108 | self.settings['active-color-profile']] 109 | colors = color_profile[2] 110 | except: 111 | colors = [] 112 | if len(colors) == 0: 113 | self.get_style_context().remove_provider(self.css_provider) 114 | elif len(colors) == 1: 115 | self.css_data = '''#cavasik-window { 116 | background-color: rgba(%d, %d, %d, %f); 117 | }''' % colors[0] 118 | self.css_provider.load_from_data(self.css_data, -1) 119 | self.get_style_context().add_provider(self.css_provider, \ 120 | Gtk.STYLE_PROVIDER_PRIORITY_USER) 121 | elif len(colors) > 1: 122 | self.css_data = '''#cavasik-window { 123 | background: linear-gradient(to bottom, ''' 124 | for c in colors: 125 | self.css_data += 'rgba(%d, %d, %d, %f), ' % c 126 | self.css_data = self.css_data[:-2] 127 | self.css_data += ');}' 128 | self.css_provider.load_from_data(self.css_data, -1) 129 | self.get_style_context().add_provider(self.css_provider, \ 130 | Gtk.STYLE_PROVIDER_PRIORITY_USER) 131 | 132 | def on_settings_changed(self): 133 | if self.settings['borderless-window']: 134 | self.add_css_class('borderless-window') 135 | elif self.has_css_class('borderless-window'): 136 | self.remove_css_class('borderless-window') 137 | self.header.set_show_start_title_buttons( \ 138 | self.settings['window-controls']) 139 | self.header.set_show_end_title_buttons(self.settings['window-controls']) 140 | self.toggle_sharp_corners() 141 | self.set_style() 142 | self.apply_colors() 143 | try: 144 | self.on_active_state_changed() 145 | except: 146 | pass 147 | 148 | def on_close_request(self, obj): 149 | (width, height) = self.get_default_size() 150 | self.settings['size'] = (width, height) 151 | self.settings['maximized'] = self.is_maximized() 152 | if hasattr(self.get_application(), 'pref_win'): 153 | self.get_application().pref_win.close() 154 | 155 | def hide_header(self): 156 | if not self.is_active(): 157 | self.header.set_show_start_title_buttons(False) 158 | self.header.set_show_end_title_buttons(False) 159 | self.menu_button.set_visible(False) 160 | return False # we don't need to restart the function 161 | 162 | def on_active_state_changed(self, *args): 163 | if self.settings['autohide-header'] and not self.is_active(): 164 | # The window becomes inactive for a moment when 165 | # the menu button is pressed, making it impossible 166 | # to open the menu, so the delay is required 167 | GObject.timeout_add(100, self.hide_header) 168 | else: 169 | self.header.set_show_start_title_buttons( \ 170 | self.settings['window-controls']) 171 | self.header.set_show_end_title_buttons( \ 172 | self.settings['window-controls']) 173 | self.menu_button.set_visible(True) 174 | 175 | -------------------------------------------------------------------------------- /utils/flatpak-pip-generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Utility for generating flatpak module entries from a pip package 3 | # External to the Cavasik project 4 | 5 | __license__ = 'MIT' 6 | 7 | import argparse 8 | import json 9 | import hashlib 10 | import os 11 | import shutil 12 | import subprocess 13 | import sys 14 | import tempfile 15 | import urllib.request 16 | 17 | from collections import OrderedDict 18 | from typing import Dict 19 | 20 | try: 21 | import requirements 22 | except ImportError: 23 | exit('Requirements modules is not installed. Run "pip install requirements-parser"') 24 | 25 | parser = argparse.ArgumentParser() 26 | parser.add_argument('packages', nargs='*') 27 | parser.add_argument('--python2', action='store_true', 28 | help='Look for a Python 2 package') 29 | parser.add_argument('--cleanup', choices=['scripts', 'all'], 30 | help='Select what to clean up after build') 31 | parser.add_argument('--requirements-file', '-r', 32 | help='Specify requirements.txt file') 33 | parser.add_argument('--build-only', action='store_const', 34 | dest='cleanup', const='all', 35 | help='Clean up all files after build') 36 | parser.add_argument('--build-isolation', action='store_true', 37 | default=False, 38 | help=( 39 | 'Do not disable build isolation. ' 40 | 'Mostly useful on pip that does\'t ' 41 | 'support the feature.' 42 | )) 43 | parser.add_argument('--ignore-installed', 44 | type=lambda s: s.split(','), 45 | default='', 46 | help='Comma-separated list of package names for which pip ' 47 | 'should ignore already installed packages. Useful when ' 48 | 'the package is installed in the SDK but not in the ' 49 | 'runtime.') 50 | parser.add_argument('--checker-data', action='store_true', 51 | help='Include x-checker-data in output for the "Flatpak External Data Checker"') 52 | parser.add_argument('--output', '-o', 53 | help='Specify output file name') 54 | parser.add_argument('--runtime', 55 | help='Specify a flatpak to run pip inside of a sandbox, ensures python version compatibility') 56 | parser.add_argument('--yaml', action='store_true', 57 | help='Use YAML as output format instead of JSON') 58 | parser.add_argument('--ignore-errors', action='store_true', 59 | help='Ignore errors when downloading packages') 60 | opts = parser.parse_args() 61 | 62 | if opts.yaml: 63 | try: 64 | import yaml 65 | except ImportError: 66 | exit('PyYAML modules is not installed. Run "pip install PyYAML"') 67 | 68 | 69 | def get_pypi_url(name: str, filename: str) -> str: 70 | url = 'https://pypi.org/pypi/{}/json'.format(name) 71 | print('Extracting download url for', name) 72 | with urllib.request.urlopen(url) as response: 73 | body = json.loads(response.read().decode('utf-8')) 74 | for release in body['releases'].values(): 75 | for source in release: 76 | if source['filename'] == filename: 77 | return source['url'] 78 | raise Exception('Failed to extract url from {}'.format(url)) 79 | 80 | 81 | def get_tar_package_url_pypi(name: str, version: str) -> str: 82 | url = 'https://pypi.org/pypi/{}/{}/json'.format(name, version) 83 | with urllib.request.urlopen(url) as response: 84 | body = json.loads(response.read().decode('utf-8')) 85 | for ext in ['bz2', 'gz', 'xz', 'zip']: 86 | for source in body['urls']: 87 | if source['url'].endswith(ext): 88 | return source['url'] 89 | err = 'Failed to get {}-{} source from {}'.format(name, version, url) 90 | raise Exception(err) 91 | 92 | 93 | def get_package_name(filename: str) -> str: 94 | if filename.endswith(('bz2', 'gz', 'xz', 'zip')): 95 | segments = filename.split('-') 96 | if len(segments) == 2: 97 | return segments[0] 98 | return '-'.join(segments[:len(segments) - 1]) 99 | elif filename.endswith('whl'): 100 | segments = filename.split('-') 101 | if len(segments) == 5: 102 | return segments[0] 103 | candidate = segments[:len(segments) - 4] 104 | # Some packages list the version number twice 105 | # e.g. PyQt5-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux2014_x86_64.whl 106 | if candidate[-1] == segments[len(segments) - 4]: 107 | return '-'.join(candidate[:-1]) 108 | return '-'.join(candidate) 109 | else: 110 | raise Exception( 111 | 'Downloaded filename: {} does not end with bz2, gz, xz, zip, or whl'.format(filename) 112 | ) 113 | 114 | 115 | def get_file_version(filename: str) -> str: 116 | name = get_package_name(filename) 117 | segments = filename.split(name + '-') 118 | version = segments[1].split('-')[0] 119 | for ext in ['tar.gz', 'whl', 'tar.xz', 'tar.gz', 'tar.bz2', 'zip']: 120 | version = version.replace('.' + ext, '') 121 | return version 122 | 123 | 124 | def get_file_hash(filename: str) -> str: 125 | sha = hashlib.sha256() 126 | print('Generating hash for', filename.split('/')[-1]) 127 | with open(filename, 'rb') as f: 128 | while True: 129 | data = f.read(1024 * 1024 * 32) 130 | if not data: 131 | break 132 | sha.update(data) 133 | return sha.hexdigest() 134 | 135 | 136 | def download_tar_pypi(url: str, tempdir: str) -> None: 137 | with urllib.request.urlopen(url) as response: 138 | file_path = os.path.join(tempdir, url.split('/')[-1]) 139 | with open(file_path, 'x+b') as tar_file: 140 | shutil.copyfileobj(response, tar_file) 141 | 142 | 143 | def parse_continuation_lines(fin): 144 | for line in fin: 145 | line = line.rstrip('\n') 146 | while line.endswith('\\'): 147 | try: 148 | line = line[:-1] + next(fin).rstrip('\n') 149 | except StopIteration: 150 | exit('Requirements have a wrong number of line continuation characters "\\"') 151 | yield line 152 | 153 | 154 | def fprint(string: str) -> None: 155 | separator = '=' * 72 # Same as `flatpak-builder` 156 | print(separator) 157 | print(string) 158 | print(separator) 159 | 160 | 161 | packages = [] 162 | if opts.requirements_file: 163 | requirements_file = os.path.expanduser(opts.requirements_file) 164 | try: 165 | with open(requirements_file, 'r') as req_file: 166 | reqs = parse_continuation_lines(req_file) 167 | reqs_as_str = '\n'.join([r.split('--hash')[0] for r in reqs]) 168 | packages = list(requirements.parse(reqs_as_str)) 169 | except FileNotFoundError: 170 | pass 171 | 172 | elif opts.packages: 173 | packages = list(requirements.parse('\n'.join(opts.packages))) 174 | with tempfile.NamedTemporaryFile('w', delete=False, prefix='requirements.') as req_file: 175 | req_file.write('\n'.join(opts.packages)) 176 | requirements_file = req_file.name 177 | else: 178 | exit('Please specify either packages or requirements file argument') 179 | 180 | for i in packages: 181 | if i["name"].lower().startswith("pyqt"): 182 | print("PyQt packages are not supported by flapak-pip-generator") 183 | print("However, there is a BaseApp for PyQt available, that you should use") 184 | print("Visit https://github.com/flathub/com.riverbankcomputing.PyQt.BaseApp for more information") 185 | sys.exit(0) 186 | 187 | with open(requirements_file, 'r') as req_file: 188 | use_hash = '--hash=' in req_file.read() 189 | 190 | python_version = '2' if opts.python2 else '3' 191 | if opts.python2: 192 | pip_executable = 'pip2' 193 | else: 194 | pip_executable = 'pip3' 195 | 196 | if opts.runtime: 197 | flatpak_cmd = [ 198 | 'flatpak', 199 | '--devel', 200 | '--share=network', 201 | '--filesystem=/tmp', 202 | '--command={}'.format(pip_executable), 203 | 'run', 204 | opts.runtime 205 | ] 206 | if opts.requirements_file: 207 | requirements_file = os.path.expanduser(opts.requirements_file) 208 | if os.path.exists(requirements_file): 209 | prefix = os.path.realpath(requirements_file) 210 | flag = '--filesystem={}'.format(prefix) 211 | flatpak_cmd.insert(1,flag) 212 | else: 213 | flatpak_cmd = [pip_executable] 214 | 215 | if opts.output: 216 | output_package = opts.output 217 | elif opts.requirements_file: 218 | output_package = 'python{}-{}'.format( 219 | python_version, 220 | os.path.basename(opts.requirements_file).replace('.txt', ''), 221 | ) 222 | elif len(packages) == 1: 223 | output_package = 'python{}-{}'.format( 224 | python_version, packages[0].name, 225 | ) 226 | else: 227 | output_package = 'python{}-modules'.format(python_version) 228 | if opts.yaml: 229 | output_filename = output_package + '.yaml' 230 | else: 231 | output_filename = output_package + '.json' 232 | 233 | modules = [] 234 | vcs_modules = [] 235 | sources = {} 236 | 237 | tempdir_prefix = 'pip-generator-{}'.format(os.path.basename(output_package)) 238 | with tempfile.TemporaryDirectory(prefix=tempdir_prefix) as tempdir: 239 | pip_download = flatpak_cmd + [ 240 | 'download', 241 | '--exists-action=i', 242 | '--dest', 243 | tempdir, 244 | '-r', 245 | requirements_file 246 | ] 247 | if use_hash: 248 | pip_download.append('--require-hashes') 249 | 250 | fprint('Downloading sources') 251 | cmd = ' '.join(pip_download) 252 | print('Running: "{}"'.format(cmd)) 253 | try: 254 | subprocess.run(pip_download, check=True) 255 | except subprocess.CalledProcessError: 256 | print('Failed to download') 257 | print('Please fix the module manually in the generated file') 258 | if not opts.ignore_errors: 259 | print('Ignore the error by passing --ignore-errors') 260 | raise 261 | 262 | if not opts.requirements_file: 263 | try: 264 | os.remove(requirements_file) 265 | except FileNotFoundError: 266 | pass 267 | 268 | fprint('Downloading arch independent packages') 269 | for filename in os.listdir(tempdir): 270 | if not filename.endswith(('bz2', 'any.whl', 'gz', 'xz', 'zip')): 271 | version = get_file_version(filename) 272 | name = get_package_name(filename) 273 | url = get_tar_package_url_pypi(name, version) 274 | print('Deleting', filename) 275 | try: 276 | os.remove(os.path.join(tempdir, filename)) 277 | except FileNotFoundError: 278 | pass 279 | print('Downloading {}'.format(url)) 280 | download_tar_pypi(url, tempdir) 281 | 282 | files = {get_package_name(f): [] for f in os.listdir(tempdir)} 283 | 284 | for filename in os.listdir(tempdir): 285 | name = get_package_name(filename) 286 | files[name].append(filename) 287 | 288 | # Delete redundant sources, for vcs sources 289 | for name in files: 290 | if len(files[name]) > 1: 291 | zip_source = False 292 | for f in files[name]: 293 | if f.endswith('.zip'): 294 | zip_source = True 295 | if zip_source: 296 | for f in files[name]: 297 | if not f.endswith('.zip'): 298 | try: 299 | os.remove(os.path.join(tempdir, f)) 300 | except FileNotFoundError: 301 | pass 302 | 303 | vcs_packages = { 304 | x.name: {'vcs': x.vcs, 'revision': x.revision, 'uri': x.uri} 305 | for x in packages 306 | if x.vcs 307 | } 308 | 309 | fprint('Obtaining hashes and urls') 310 | for filename in os.listdir(tempdir): 311 | name = get_package_name(filename) 312 | sha256 = get_file_hash(os.path.join(tempdir, filename)) 313 | 314 | if name in vcs_packages: 315 | uri = vcs_packages[name]['uri'] 316 | revision = vcs_packages[name]['revision'] 317 | vcs = vcs_packages[name]['vcs'] 318 | url = 'https://' + uri.split('://', 1)[1] 319 | s = 'commit' 320 | if vcs == 'svn': 321 | s = 'revision' 322 | source = OrderedDict([ 323 | ('type', vcs), 324 | ('url', url), 325 | (s, revision), 326 | ]) 327 | is_vcs = True 328 | else: 329 | url = get_pypi_url(name, filename) 330 | source = OrderedDict([ 331 | ('type', 'file'), 332 | ('url', url), 333 | ('sha256', sha256)]) 334 | if opts.checker_data: 335 | source['x-checker-data'] = { 336 | 'type': 'pypi', 337 | 'name': name} 338 | if url.endswith(".whl"): 339 | source['x-checker-data']['packagetype'] = 'bdist_wheel' 340 | is_vcs = False 341 | sources[name] = {'source': source, 'vcs': is_vcs} 342 | 343 | # Python3 packages that come as part of org.freedesktop.Sdk. 344 | system_packages = ['cython', 'easy_install', 'mako', 'markdown', 'meson', 'pip', 'pygments', 'setuptools', 'six', 'wheel'] 345 | 346 | fprint('Generating dependencies') 347 | for package in packages: 348 | 349 | if package.name is None: 350 | print('Warning: skipping invalid requirement specification {} because it is missing a name'.format(package.line), file=sys.stderr) 351 | print('Append #egg= to the end of the requirement line to fix', file=sys.stderr) 352 | continue 353 | elif package.name.casefold() in system_packages: 354 | print(f"{package.name} is in system_packages. Skipping.") 355 | continue 356 | 357 | if len(package.extras) > 0: 358 | extras = '[' + ','.join(extra for extra in package.extras) + ']' 359 | else: 360 | extras = '' 361 | 362 | version_list = [x[0] + x[1] for x in package.specs] 363 | version = ','.join(version_list) 364 | 365 | if package.vcs: 366 | revision = '' 367 | if package.revision: 368 | revision = '@' + package.revision 369 | pkg = package.uri + revision + '#egg=' + package.name 370 | else: 371 | pkg = package.name + extras + version 372 | 373 | dependencies = [] 374 | # Downloads the package again to list dependencies 375 | 376 | tempdir_prefix = 'pip-generator-{}'.format(package.name) 377 | with tempfile.TemporaryDirectory(prefix='{}-{}'.format(tempdir_prefix, package.name)) as tempdir: 378 | pip_download = flatpak_cmd + [ 379 | 'download', 380 | '--exists-action=i', 381 | '--dest', 382 | tempdir, 383 | ] 384 | try: 385 | print('Generating dependencies for {}'.format(package.name)) 386 | subprocess.run(pip_download + [pkg], check=True, stdout=subprocess.DEVNULL) 387 | for filename in sorted(os.listdir(tempdir)): 388 | dep_name = get_package_name(filename) 389 | if dep_name.casefold() in system_packages: 390 | continue 391 | dependencies.append(dep_name) 392 | 393 | except subprocess.CalledProcessError: 394 | print('Failed to download {}'.format(package.name)) 395 | 396 | is_vcs = True if package.vcs else False 397 | package_sources = [] 398 | for dependency in dependencies: 399 | if dependency in sources: 400 | source = sources[dependency] 401 | elif dependency.replace('_', '-') in sources: 402 | source = sources[dependency.replace('_', '-')] 403 | else: 404 | continue 405 | 406 | if not (not source['vcs'] or is_vcs): 407 | continue 408 | 409 | package_sources.append(source['source']) 410 | 411 | if package.vcs: 412 | name_for_pip = '.' 413 | else: 414 | name_for_pip = pkg 415 | 416 | module_name = 'python{}-{}'.format(python_version, package.name) 417 | 418 | pip_command = [ 419 | pip_executable, 420 | 'install', 421 | '--verbose', 422 | '--exists-action=i', 423 | '--no-index', 424 | '--find-links="file://${PWD}"', 425 | '--prefix=${FLATPAK_DEST}', 426 | '"{}"'.format(name_for_pip) 427 | ] 428 | if package.name in opts.ignore_installed: 429 | pip_command.append('--ignore-installed') 430 | if not opts.build_isolation: 431 | pip_command.append('--no-build-isolation') 432 | 433 | module = OrderedDict([ 434 | ('name', module_name), 435 | ('buildsystem', 'simple'), 436 | ('build-commands', [' '.join(pip_command)]), 437 | ('sources', package_sources), 438 | ]) 439 | if opts.cleanup == 'all': 440 | module['cleanup'] = ['*'] 441 | elif opts.cleanup == 'scripts': 442 | module['cleanup'] = ['/bin', '/share/man/man1'] 443 | 444 | if package.vcs: 445 | vcs_modules.append(module) 446 | else: 447 | modules.append(module) 448 | 449 | modules = vcs_modules + modules 450 | if len(modules) == 1: 451 | pypi_module = modules[0] 452 | else: 453 | pypi_module = { 454 | 'name': output_package, 455 | 'buildsystem': 'simple', 456 | 'build-commands': [], 457 | 'modules': modules, 458 | } 459 | 460 | print() 461 | with open(output_filename, 'w') as output: 462 | if opts.yaml: 463 | class OrderedDumper(yaml.Dumper): 464 | def increase_indent(self, flow=False, indentless=False): 465 | return super(OrderedDumper, self).increase_indent(flow, False) 466 | 467 | def dict_representer(dumper, data): 468 | return dumper.represent_dict(data.items()) 469 | 470 | OrderedDumper.add_representer(OrderedDict, dict_representer) 471 | 472 | output.write("# Generated with flatpak-pip-generator " + " ".join(sys.argv[1:]) + "\n") 473 | yaml.dump(pypi_module, output, Dumper=OrderedDumper) 474 | else: 475 | output.write(json.dumps(pypi_module, indent=4)) 476 | print('Output saved to {}'.format(output_filename)) 477 | --------------------------------------------------------------------------------