[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\nmax_line_length=120\n\n[*.go]\nindent_style = tab\nindent_size = 4\ncharset = utf-8\ntrim_trailing_whitespace = true\n\n[*.html]\nindent_style = tab\nindent_size = 4\ncharset = utf-8\ntrim_trailing_whitespace = false\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".gitattributes",
    "content": "_site/* linguist-vendored\ndocs/* linguist-vendored\nvendor/* linguist-vendored\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: FelicianoTech\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Bug.md",
    "content": "---\nname:  🐞 Report a Bug\nabout: Tell us what's broken\n---\n\n## What's broken?\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Feature.md",
    "content": "---\nname: ⚡️ Request a Feature\nabout: Tell us what it should do\n---\n\n## What should it do?\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Support.md",
    "content": "---\nname: ❓Ask a Question\nabout: Tell us how we can help\n---\n\n## How can we help?\n\n\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/Improvement.md",
    "content": "---\nname: Improvement\nabout: You have some improvement to make wtf better?\n---\n\nThanks for submitting a pull request. Please provide enough information so that others can review your pull request.\n\n\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/Other.md",
    "content": "---\nname: Other\nabout: You have some other ideas you want to introduce?\n---\n\nThanks for submitting a pull request. Please provide enough information so that others can review your pull request.\n\n\n\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "\r\nThanks for submitting a pull request. Please provide enough information so that others can review your pull request.\r\n\r\n\r\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: gomod\n  directory: \"/\"\n  schedule:\n    interval: daily\n  open-pull-requests-limit: 10\n  assignees:\n  - FelicianoTech\n- package-ecosystem: \"github-actions\"\n  directory: \"/\"\n  schedule:\n    interval: daily\n  open-pull-requests-limit: 10\n  assignees:\n  - FelicianoTech\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Configuration for probot-stale - https://github.com/probot/stale\n\n# Number of days of inactivity before an Issue or Pull Request becomes stale\ndaysUntilStale: 180\n\n# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.\n# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.\ndaysUntilClose: false\n\n# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)\nonlyLabels: []\n\n# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable\nexemptLabels:\n  - pinned\n  - security\n  - \"[Status] Maybe Later\"\n\n# Set to true to ignore issues in a project (defaults to false)\nexemptProjects: false\n\n# Set to true to ignore issues in a milestone (defaults to false)\nexemptMilestones: false\n\n# Set to true to ignore issues with an assignee (defaults to false)\nexemptAssignees: true\n\n# Label to use when marking as stale\nstaleLabel: wontfix\n\n# Comment to post when marking as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n\n# Comment to post when removing the stale label.\n# unmarkComment: >\n#   Your comment here.\n\n# Comment to post when closing a stale Issue or Pull Request.\n# closeComment: >\n#   Your comment here.\n\n# Limit the number of actions per hour, from 1-30. Default is 30\nlimitPerRun: 30\n\n# Limit to only `issues` or `pulls`\n# only: issues\n\n# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':\n# pulls:\n#   daysUntilStale: 30\n#   markComment: >\n#     This pull request has been automatically marked as stale because it has not had\n#     recent activity. It will be closed if no further activity occurs. Thank you\n#     for your contributions.\n\n# issues:\n#   exemptLabels:\n#     - confirmed\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "name: \"CodeQL Analysis\"\n\non:\n  push:\n    branches:\n      - trunk\n  pull_request:\n\njobs:\n  analyze:\n    runs-on: ubuntu-24.04\n    steps:\n    - name: \"Checkout repository\"\n      uses: actions/checkout@v6.0.2\n    - name: \"Initialize CodeQL\"\n      uses: github/codeql-action/init@v4\n    - name: \"Compile code\"\n      uses: github/codeql-action/autobuild@v4\n    - name: \"Perform CodeQL Analysis\"\n      uses: github/codeql-action/analyze@v4\n"
  },
  {
    "path": ".github/workflows/golangci-lint.yml",
    "content": "name: golangci-lint\non:\n  push:\n    tags:\n      - v*\n    branches:\n      - trunk\n  pull_request:\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6.0.2\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v9\n        with:\n          version: v2.11\n          args: ./... --timeout=10m\n"
  },
  {
    "path": ".github/workflows/goreleaser.yml",
    "content": "name: goreleaser\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6.0.2\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v6.2.0\n        with:\n          go-version-file: 'go.mod'\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v6.4.0\n        with:\n          version: 2.14.0\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n          MASTODON_CLIENT_ID: ${{ secrets.MASTODON_CLIENT_ID }}\n          MASTODON_CLIENT_SECRET: ${{ secrets.MASTODON_CLIENT_SECRET }}\n          MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}\n          BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }}\n          DISCOURSE_API_KEY: ${{ secrets.DISCOURSE_API_KEY }}\n"
  },
  {
    "path": ".github/workflows/pr-checks.yml",
    "content": "name: \"PR Checks\"\n\non:\n  pull_request:\n    branches:\n      - trunk\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-24.04\n    steps:\n      - name: \"Checkout code\"\n        uses: actions/checkout@v6.0.2\n        with:\n          fetch-depth: 0\n      - name: \"Set up Go\"\n        uses: actions/setup-go@v6.2.0\n        with:\n          go-version-file: 'go.mod'\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v6.4.0\n        with:\n          version: 2.14.0\n          args: release --snapshot\n"
  },
  {
    "path": ".github/workflows/staticcheck.yml",
    "content": "name: static check\non: pull_request\n\njobs:\n  imports:\n    name: Imports\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6.0.2\n    - name: check\n      # uses: grandcolline/golang-github-actions@4356d0458ea4bfdb55fcb296437812acef970f9b\n      uses: senorprogrammer/golang-github-actions@c2675d08254b17c070e524b3d907cfaf05fbae6f\n      with:\n        run: imports\n        token: ${{ secrets.GITHUB_TOKEN }}\n\n  errcheck:\n    name: Errcheck\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6.0.2\n    - name: check\n      # uses: grandcolline/golang-github-actions@4356d0458ea4bfdb55fcb296437812acef970f9b\n      uses: senorprogrammer/golang-github-actions@c2675d08254b17c070e524b3d907cfaf05fbae6f\n      with:\n        run: errcheck\n        token: ${{ secrets.GITHUB_TOKEN }}\n\n  #lint:\n    #name: Lint\n    #runs-on: ubuntu-latest\n    #steps:\n    #- uses: actions/checkout@v6.0.2\n    #- name: check\n      #uses: grandcolline/golang-github-actions@4356d04\n      #with:\n        #run: lint\n        #token: ${{ secrets.GITHUB_TOKEN }}\n\n  shadow:\n    name: Shadow\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6.0.2\n    - name: check\n      # uses: grandcolline/golang-github-actions@4356d0458ea4bfdb55fcb296437812acef970f9b\n      uses: senorprogrammer/golang-github-actions@c2675d08254b17c070e524b3d907cfaf05fbae6f\n      with:\n        run: shadow\n        token: ${{ secrets.GITHUB_TOKEN }}\n\n  staticcheck:\n    name: StaticCheck\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6.0.2\n    - name: check\n      # uses: grandcolline/golang-github-actions@4356d0458ea4bfdb55fcb296437812acef970f9b\n      uses: senorprogrammer/golang-github-actions@c2675d08254b17c070e524b3d907cfaf05fbae6f\n      with:\n        run: staticcheck\n        token: ${{ secrets.GITHUB_TOKEN }}\n\n  #sec:\n    #name: Sec\n    #runs-on: ubuntu-latest\n    #steps:\n    #- uses: actions/checkout@v6.0.2\n    #- name: check\n      #uses: grandcolline/golang-github-actions@4356d04\n      #with:\n        #run: sec\n        #token: ${{ secrets.GITHUB_TOKEN }}\n        #flags: \"-exclude=G104\"\n"
  },
  {
    "path": ".gitignore",
    "content": "### Go ###\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\nftw*\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Misc\n.DS_Store\ngcal/client_secret.json\ngspreadsheets/client_secret.json\nprofile.pdf\nreport.*\n.vscode\n\n# All things node\nnode_modules/\npackage-lock.json\n\n#intellij idea\n.idea/\n\ndist/*\nbin/\n"
  },
  {
    "path": ".gitmodules",
    "content": ""
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\n\nrun:\n  timeout: 3m\n\nlinters:\n  enable:\n    - govet\n    - errcheck\n    - staticcheck\n    - unconvert\n  exclusions:\n    rules:\n      - linters:\n        - errcheck\n        source: \"^\\\\s*defer\\\\s+\"\n\nformatters:\n  enable:\n    - gofmt\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "version: 2\n\nbuilds:\n  - binary: wtfutil\n    goos:\n      - darwin\n      - linux\n    goarch:\n      - amd64\n      - arm\n      - arm64\n\narchives:\n  - id: default\n\nhomebrew_casks:\n  - name: wtfutil\n    homepage: 'https://wtfutil.com'\n    description: 'The personal information dashboard for your terminal.'\n    repository:\n      owner: wtfutil\n      name: homebrew-wtfutil\n    hooks:\n      post:\n        # This hook is needed until this binary is signed and notarized\n        install: |\n          if system_command(\"/usr/bin/xattr\", args: [\"-h\"]).exit_status == 0\n            system_command \"/usr/bin/xattr\", args: [\"-dr\", \"com.apple.quarantine\", \"#{staged_path}/wtfutil\"]\n          end\n  - name: wtfutil\n    homepage: 'https://wtfutil.com'\n    description: 'The personal information dashboard for your terminal.'\n    repository:\n      owner: linodians\n      name: homebrew-tap\n    hooks:\n      post:\n        # This hook is needed until this binary is signed and notarized\n        install: |\n          if system_command(\"/usr/bin/xattr\", args: [\"-h\"]).exit_status == 0\n            system_command \"/usr/bin/xattr\", args: [\"-dr\", \"com.apple.quarantine\", \"#{staged_path}/wtfutil\"]\n          end\n\nannounce:\n  mastodon:\n    enabled: true\n    server: \"https://social.linodians.com\"\n  bluesky:\n    enabled: true\n    username: \"wtfutil.bsky.social\"\n  discourse:\n    enabled: true\n    server: \"https://discuss.linodians.com\"\n    username: \"system\"\n    category_id: 7\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at chriscummer+wtf@me.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n## Pull Request Process\n\n1. Ensure any install or build dependencies are removed before the end of the layer when doing a\n   build.\n2. Update the static documentation with details of changes to the interface, this includes new environment\n   variables, useful file locations and configuration parameters.\n\nDocumentation lives at [wtfdocs](https://github.com/wtfutil/wtfdocs) and is a [Hugo](https://gohugo.io) app. See Hugo's documentation for usage.\n\n## Code of Conduct\n\n### Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\nnationality, personal appearance, race, religion, or sexual identity and\norientation.\n\n### Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\nadvances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n### Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n### Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n### Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project owner at chriscummer@me.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n### Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "LICENSE.md",
    "content": "*Mozilla Public License, version 2.0* \n\n1. Definitions \n \n1.1. “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software.\n\n1.2. “Contributor Version” means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution” means Covered Software of a particular Contributor.\n\n1.4. “Covered Software” means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof.\n\n1.5. “Incompatible With Secondary Licenses” means\n\nthat the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or\n\nthat the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License.\n\n1.6. “Executable Form” means any form of the work other than Source Code Form.\n\n1.7. “Larger Work” means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software.\n\n1.8. “License” means this document.\n\n1.9. “Licensable” means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License.\n\n1.10. “Modifications” means any of the following:\n\n  any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or\n\n  any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version.\n\n1.12. “Secondary License” means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form” means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”) means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.\n\n2. License Grants and Conditions \n \n2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:\n\nunder intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and\n\nunder Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version.\n\n2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution.\n\n2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor:\n\nfor any code that a Contributor has removed from Covered Software; or\n\nfor infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or\n\nunder Patent Claims infringed by Covered Software in the absence of its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3).\n\n2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1.\n\n3. Responsibilities\n\n3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then:\n\nsuch Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and\n\nYou may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s).\n\n3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation \n\nIf it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it.\n\n5. Termination \n\n5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination.\n\n6. Disclaimer of Warranty \n\nCovered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer.\n\n7. Limitation of Liability \n\nUnder no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation \n\nAny litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous \n\nThis License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor.\n\n10. Versions of the License \n\n10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number.\n\n10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward.\n\n10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice \n\nThis Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice \n\nThis Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: build clean contrib_check coverage docker-build docker-install help install isntall lint run size test uninstall\n\n# detect GOPATH if not set\nifndef $(GOPATH)\n\t$(info GOPATH is not set, autodetecting..)\n\tTESTPATH := $(dir $(abspath ../../..))\n\tDIRS := bin pkg src\n\n\t# create a ; separated line of tests and pass it to shell\n\tMISSING_DIRS := $(shell $(foreach entry,$(DIRS),test -d \"$(TESTPATH)$(entry)\" || echo \"$(entry)\";))\n\tifeq ($(MISSING_DIRS),)\n\t\t$(info Found GOPATH: $(TESTPATH))\n\t\texport GOPATH := $(TESTPATH)\n\telse\n\t\t$(info ..missing dirs \"$(MISSING_DIRS)\" in \"$(TESTDIR)\")\n\t\t$(info GOPATH autodetection failed)\n\tendif\nendif\n\n# Set go modules to on and use GoCenter for immutable modules\nexport GO111MODULE = on\nexport GOPROXY = https://proxy.golang.org,direct\n\n# Determines the path to this Makefile\nTHIS_FILE := $(lastword $(MAKEFILE_LIST))\n\nGOBIN := $(GOPATH)/bin\n\nAPP=wtfutil\n\ndefine HEADER\n____    __    ____ .___________. _______\n\\   \\  /  \\  /   / |           ||   ____|\n \\   \\/    \\/   /  `---|  |----`|  |__\n  \\            /       |  |     |   __|\n   \\    /\\    /        |  |     |  |\n    \\__/  \\__/         |__|     |__|\n\nendef\nexport HEADER\n\n# -------------------- Actions -------------------- #\n\n## build: builds a local version\nbuild:\n\t@echo \"$$HEADER\"\n\t@echo \"Building...\"\n\tgo build -o bin/${APP}\n\t@echo \"Done building\"\n\n## clean: removes old build cruft\nclean:\n\trm -rf ./dist\n\trm -rf ./bin/${APP}\n\t@echo \"Done cleaning\"\n\n## contrib-check: checks for any contributors who have not been given due credit\ncontrib-check:\n\tnpx all-contributors-cli check\n\n## coverage: figures out and displays test code coverage\ncoverage:\n\tgo test -coverprofile=coverage.out ./...\n\tgo tool cover -html=coverage.out\n\n## docker-build: builds in docker\ndocker-build:\n\t@echo \"Building ${APP} in Docker...\"\n\tdocker build -t wtfutil:build --build-arg=version=master -f Dockerfile.build .\n\t@echo \"Done with docker build\"\n\n## docker-install: installs a local version of the app from docker build\ndocker-install:\n\t@echo \"Installing...\"\n\tdocker create --name wtf_build wtfutil:build\n\tdocker cp wtf_build:/usr/local/bin/wtfutil ~/.local/bin/\n\t$(eval INSTALLPATH = $(shell which ${APP}))\n\t@echo \"${APP} installed into ${INSTALLPATH}\"\n\tdocker rm wtf_build\n\n## gosec: runs the gosec static security scanner against the source code\ngosec: $(GOBIN)/gosec\n\tgosec -tests ./...\n\n$(GOBIN)/gosec:\n\tcd && go install github.com/securego/gosec/v2/cmd/gosec@latest\n\n## help: prints this help message\nhelp:\n\t@echo \"Usage: \\n\"\n\t@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' |  sed -e 's/^/ /'\n\n## isntall: an alias for 'install'\nisntall:\n\t@$(MAKE) -f $(THIS_FILE) install\n\n## install: installs a local version of the app\ninstall:\n\t$(eval GOVERS = $(shell go version))\n\t@echo \"$$HEADER\"\n\t@echo \"Installing ${APP} with ${GOVERS}...\"\n\t@go clean\n\t@go install -ldflags=\"-s -w\"\n\t$(eval INSTALLPATH = $(shell which ${APP}))\n\t@echo \"${APP} installed into ${INSTALLPATH}\"\n\n## lint: runs a number of code quality checks against the source code\nlint: $(GOBIN)/golangci-lint\n\tgolangci-lint cache clean\n\tgolangci-lint run\n\n$(GOBIN)/golangci-lint:\n\tcd && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest\n\n# lint:\n# \t@echo \"\\033[35mhttps://github.com/kisielk/errcheck\\033[0m\"\n# \terrcheck ./app\n# \terrcheck ./cfg\n# \terrcheck ./flags\n# \terrcheck ./help\n# \terrcheck ./logger\n# \terrcheck ./modules/...\n# \terrcheck ./utils\n# \terrcheck ./view\n# \terrcheck ./wtf\n# \terrcheck ./main.go\n\n# \t@echo \"\\033[35mhttps://golang.org/cmd/vet/k\\033[0m\"\n# \tgo vet ./app\n# \tgo vet ./cfg\n# \tgo vet ./flags\n# \tgo vet ./help\n# \tgo vet ./logger\n# \tgo vet ./modules/...\n# \tgo vet ./utils\n# \tgo vet ./view\n# \tgo vet ./wtf\n# \tgo vet ./main.go\n\n# \t@echo \"\\033[35m# https://staticcheck.io/docs/k\\033[0m\"\n# \tstaticcheck ./app\n# \tstaticcheck ./cfg\n# \tstaticcheck ./flags\n# \tstaticcheck ./help\n# \tstaticcheck ./logger\n# \tstaticcheck ./modules/...\n# \tstaticcheck ./utils\n# \tstaticcheck ./view\n# \tstaticcheck ./wtf\n# \tstaticcheck ./main.go\n\n# \t@echo \"\\033[35m# https://github.com/mdempsky/unconvert\\033[0m\"\n# \tunconvert ./...\n\n## loc: displays the lines of code (LoC) count\nloc:\n\t@loc --exclude _sample_configs/ _site/ docs/ Makefile *.md\n\n## run: executes the locally-installed version\nrun: build\n\t@echo \"$$HEADER\"\n\tbin/${APP}\n\n## test: runs the test suite\ntest: build\n\t@echo \"$$HEADER\"\n\tgo test ./...\n\n## uninstall: uninstals a locally-installed version\nuninstall:\n\t@rm $(GOBIN)/${APP}\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <img src=\"./images/logo_transparent.png?raw=true\" title=\"WTF\" alt=\"WTF\" width=\"560\" height=\"560\" />\n</p>\n\n[![GitHub Release](https://img.shields.io/github/v/release/wtfutil/wtf?logo=github&style=for-the-badge)](https://github.com/wtfutil/wtf/releases)\n[![Go Report Card](https://goreportcard.com/badge/github.com/wtfutil/wtf?style=for-the-badge)](https://goreportcard.com/report/github.com/wtfutil/wtf)\n[![Discourse Status](https://img.shields.io/discourse/status?server=https%3A%2F%2Fdiscuss.linodians.com&logo=discourse&style=for-the-badge)](https://discuss.linodians.com/c/projects/wtf/7)\n[![Bluesky followers](https://img.shields.io/bluesky/followers/wtfutil.bsky.social?logo=bluesky&style=for-the-badge)](https://bsky.app/profile/wtfutil.bsky.social)\n[![Mastodon followers](https://img.shields.io/mastodon/follow/115007297910718188?domain=social.linodians.com&logo=mastodon&style=for-the-badge)](https://social.linodians.com/@WTFutil)\n![Static Badge](https://img.shields.io/badge/LICENSE-MPL--2.0-orange?style=for-the-badge)\n\n---\n\nWTF (aka 'wtfutil') is the personal information dashboard for your terminal, providing at-a-glance access to your very important but infrequently-needed stats and data.\n\nUsed by thousands of developers and tech people around the world, WTF is free and open-source. To support the continued use and development of WTF, please consider sponsoring WTF via [GitHub Sponsors](https://github.com/sponsors/FelicianoTech).\n\n### Are you a contributor or sponsor?\n\nAwesome! [See here](https://wtfutil.com/sponsors/exit_message/) for how you can change the exit message, the message WTF shows when quitting, to something special just for you.\n\n---\n\n* [Installation](#installation)\n    * [Installing via Homebrew](#installing-via-homebrew)\n    * [Installing via `go install`](#installing-via-go-install)\n    * [Installing via MacPorts](#installing-via-macports)\n    * [Installing a Binary](#installing-a-binary)\n    * [Installing from Source](#installing-from-source)\n    * [Running via Docker](#running-via-docker)\n* [Communication](#communication)\n    * [GitHub Discussions](#github-discussions)\n    * [Twitter](#twitter)\n* [Documentation](#documentation)\n* [Modules](#modules)\n* [Getting Bugs Fixed or Features Added](#getting-bugs-fixed-or-features-added)\n* [Contributing to the Source Code](#contributing-to-the-source-code)\n    * [Adding Dependencies](#adding-dependencies)\n* [Contributing to the Documentation](#contributing-to-the-documentation)\n* [Contributors](#contributors)\n* [Acknowledgements](#acknowledgments)\n\n<p align=\"center\">\n<img src=\"./images/screenshot.jpg\" title=\"screenshot\" width=\"720\" height=\"420\" />\n</p>\n\n## Installation\n\n### Installing via Homebrew\n\nThe simplest way from Homebrew:\n\n```console\nbrew install wtfutil\n\nwtfutil\n```\n\nThat version can sometimes lag a bit, as recipe updates take time to get accepted into `homebrew-core`. If you always want the bleeding edge of releases, you can tap it:\n\n```console\nbrew install linodians/tap/wtfutil\n\nwtfutil\n```\n\n### Installing via `go install`\n\nJust run\n\n```sh\ngo install github.com/wtfutil/wtf@latest\n```\n\n### Installing via MacPorts\n\nYou can also install via [MacPorts](https://www.macports.org/):\n\n```console\nsudo port selfupdate\nsudo port install wtfutil\n\nwtfutil\n```\n\n### Installing a Binary\n\n[Download the latest binary](https://github.com/wtfutil/wtf/releases) from GitHub.\n\nWTF is a stand-alone binary. Once downloaded, copy it to a location you can run executables from (ie: `/usr/local/bin/`), and set the permissions accordingly:\n\n```bash\nchmod a+x /usr/local/bin/wtfutil\n```\n\nand you should be good to go.\n\n### Installing from Source\n\nIf you want to run the build command from within your `$GOPATH`:\n\n```bash\n# Set the Go proxy\nexport GOPROXY=\"https://proxy.golang.org,direct\"\n\n# Disable the Go checksum database\nexport GOSUMDB=off\n\n# Enable Go modules\nexport GO111MODULE=on\n\ngo get -u github.com/wtfutil/wtf\ncd $GOPATH/src/github.com/wtfutil/wtf\nmake install\nmake run\n```\n\nIf you want to run the build command from a folder that is not in your `$GOPATH`:\n\n```bash\n# Set the Go proxy\nexport GOPROXY=\"https://proxy.golang.org,direct\"\n\ngo get -u github.com/wtfutil/wtf\ncd $GOPATH/src/github.com/wtfutil/wtf\nmake install\nmake run\n```\n\n### Installing via Arch User Repository\n\nArch Linux users can utilise the [wtfutil](https://aur.archlinux.org/packages/wtfutil) package to build it from source, or [wtfutil-bin](https://aur.archlinux.org/packages/wtfutil-bin/) to install pre-built binaries.\n\n\n## Documentation\n\nSee [https://wtfutil.com](https://wtfutil.com) for the definitive\ndocumentation. Here's some short-cuts:\n\n* [Installation](https://wtfutil.com/quick_start/)\n* [Configuration](https://wtfutil.com/configuration/files/)\n* [Module Documentation](https://wtfutil.com/modules/)\n\n\n## Modules\n\nModules are the chunks of functionality that make WTF useful. Modules are added and configured by including their configuration values in your `config.yml` file. The documentation for each module describes how to configure them.\n\nSome interesting modules you might consider adding to get you started:\n\n* [DigitalOcean](https://wtfutil.com/modules/digitalocean/)\n* [GitHub](https://wtfutil.com/modules/github/)\n* [Google Calendar](https://wtfutil.com/modules/google/gcal/)\n* [HackerNews](https://wtfutil.com/modules/hackernews/)\n* [Have I Been Pwned](https://wtfutil.com/modules/hibp/)\n* [NewRelic](https://wtfutil.com/modules/newrelic/)\n* [OpsGenie](https://wtfutil.com/modules/opsgenie/)\n* [Security](https://wtfutil.com/modules/security/)\n* [Transmission](https://wtfutil.com/modules/transmission/)\n* [Trello](https://wtfutil.com/modules/trello/)\n\n## Getting Bugs Fixed or Features Added\n\nWTF is open-source software, informally maintained by a small collection of volunteers who come and go at their leisure. There are absolutely no guarantees that, even if an issue is opened for them, bugs will be fixed or features added.\n\nIf there is a bug that you really need to have fixed or a feature you really want to have implemented, you can greatly increase your chances of that happening by creating a bounty on [BountySource](https://www.bountysource.com) to provide an incentive for someone to tackle it.\n\n## Contributing to the Source Code\n\nFirst, kindly read [Talk, then code](https://dave.cheney.net/2019/02/18/talk-then-code) by Dave Cheney. It's great advice and will often save a lot of time and effort.\n\nNext, kindly read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests.\n\nThen create your branch, write your code, submit your PR, and join the rest of the awesome people who've contributed their time and effort towards WTF. Without their contributors, WTF wouldn't be possible.\n\nDon't worry if you've never written Go before, or never contributed to an open source project before, or that your code won't be good enough. For a surprising number of people WTF has been their first Go project, or first open source contribution. If you're here, and you've read this far, you're the right stuff.\n\n## Contributing to the Documentation\n\nDocumentation now lives in its own repository here: [https://github.com/wtfutil/wtfdocs](https://github.com/wtfutil/wtfdocs).\n\nPlease make all additions and updates to documentation in that repository.\n\n### Adding Dependencies\n\nDependency management in WTF is handled by [Go modules](https://github.com/golang/go/wiki/Modules). Please check out that page for more details on how Go modules work.\n\n\n## Acknowledgments\n\nThe inspiration for `WTF` came from Monica Dinculescu's\n[tiny-care-terminal](https://github.com/notwaldorf/tiny-care-terminal).\n\nWTF is built atop [tcell](https://github.com/gdamore/tcell) and [tview](https://github.com/rivo/tview), fantastic projects both. WTF is built, packaged, and deployed via [GoReleaser](https://goreleaser.com).\n\n<p align=\"center\">\n<img src=\"./images/dude_wtf.png?raw=true\" title=\"Dude WTF\" width=\"251\" height=\"201\" />\n</p>\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nTo file a security issue, open a new Issue in the Issues tab.\n"
  },
  {
    "path": "_sample_configs/bargraph_config.yml",
    "content": "wtf:\n  colors:\n    border:\n      focusable: darkslateblue\n      focused: orange\n      normal: gray\n  grid:\n    columns: [40, 40]\n    rows: [13, 13, 4]\n  refreshInterval: 1\n  mods:\n    bargraph:\n      enabled: true\n      graphIcon: \"💀\"\n      graphStars: 25\n      position:\n        top: 1\n        left: 0\n        height: 2\n        width: 2\n      refreshInterval: 30"
  },
  {
    "path": "_sample_configs/dynamic_sizing.yml",
    "content": "wtf:\n  mods:\n    battery:\n      type: power\n      title: \"⚡️\"\n      enabled: true\n      position:\n        top: 0\n        left: 0\n        height: 1\n        width: 1\n      refreshInterval: 15\n    security_info:\n      type: security\n      enabled: true\n      position:\n        top: 0\n        left: 1\n        height: 1\n        width: 1\n      refreshInterval: 3600"
  },
  {
    "path": "_sample_configs/kubernetes_config.yml",
    "content": "wtf:\n  colors:\n    border:\n      focusable: darkslateblue\n      focused: orange\n      normal: gray\n  grid:\n    columns: [32, 32, 32, 32, 32, 32]\n    rows: [10, 10, 10, 10, 10, 10]\n  refreshInterval: 2\n  mods:\n    kubernetes:\n      enabled: true\n      kubeconfig: /Users/testuser/.kube/config\n      namespaces: [\"demo\", \"kube-system\"]\n      objects: [\"nodes\",\"deployments\", \"pods\"]\n      position:\n        top: 0\n        left: 0\n        height: 6\n        width: 3\n"
  },
  {
    "path": "_sample_configs/sample_config.yml",
    "content": "wtf:\n  colors:\n    background: black\n    border:\n      focusable: darkslateblue\n      focused: orange\n      normal: gray\n    checked: yellow\n    highlight:\n      fore: black\n      back: gray\n    rows:\n      even: yellow\n      odd: white\n  grid:\n    # How _wide_ the columns are, in terminal characters. In this case we have\n    # four columns, each of which are 35 characters wide.\n    columns: [35, 35, 35, 35]\n    # How _high_ the rows are, in terminal lines. In this case we have four rows\n    # that support ten line of text and one of four.\n    rows: [10, 10, 10, 10, 4]\n  refreshInterval: 1\n  openFileUtil: \"open\"\n  mods:\n    # You can have multiple widgets of the same type.\n    # The \"key\" is the name of the widget and the type is the actual\n    # widget you want to implement.\n    europe_time:\n      title: \"Europe\"\n      type: clocks\n      colors:\n        rows:\n          even: \"lightblue\"\n          odd: \"white\"\n      enabled: true\n      locations:\n        GMT: \"Etc/GMT\"\n        Amsterdam: \"Europe/Amsterdam\"\n        Berlin: \"Europe/Berlin\"\n        Barcelona: \"Europe/Madrid\"\n        Copenhagen: \"Europe/Copenhagen\"\n        London: \"Europe/London\"\n        Rome: \"Europe/Rome\"\n        Stockholm: \"Europe/Stockholm\"\n      position:\n        top: 0\n        left: 0\n        height: 1\n        width: 1\n      refreshInterval: 15\n      sort: \"alphabetical\"\n    americas_time:\n      title: \"Americas\"\n      type: clocks\n      colors:\n        rows:\n          even: \"lightblue\"\n          odd: \"white\"\n      enabled: true\n      locations:\n        UTC: \"Etc/UTC\"\n        Vancouver: \"America/Vancouver\"\n        New_York: \"America/New_York\"\n        Sao_Paulo: \"America/Sao_Paulo\"\n        Denver: \"America/Denver\"\n        Iqaluit: \"America/Iqaluit\"\n        Bahamas: \"America/Nassau\"\n        Chicago: \"America/Chicago\"\n      position:\n        top: 0\n        left: 1\n        height: 1\n        width: 1\n      refreshInterval: 15\n      sort: \"alphabetical\"\n    battery:\n      type: power\n      title: \"⚡️\"\n      enabled: true\n      position:\n        top: 1\n        left: 3\n        height: 1\n        width: 1\n      refreshInterval: 15\n    todolist:\n      type: todo\n      checkedIcon: \"X\"\n      colors:\n        checked: gray\n        highlight:\n          fore: \"black\"\n          back: \"orange\"\n      enabled: true\n      filename: \"todo.yml\"\n      position:\n        top: 1\n        left: 0\n        height: 2\n        width: 1\n      refreshInterval: 3600\n    ip:\n      type: ipinfo\n      title: \"My IP\"\n      colors:\n        name: \"lightblue\"\n        value: \"white\"\n      enabled: true\n      position:\n        top: 0\n        left: 2\n        height: 1\n        width: 2\n      refreshInterval: 150\n    security_info:\n      type: security\n      title: \"Staying safe\"\n      enabled: true\n      position:\n        top: 1\n        left: 2\n        height: 1\n        width: 1\n      refreshInterval: 3600\n    readme:\n      type: textfile\n      enabled: true\n      filePaths:\n        - \"~/.config/wtf/config.yml\"\n      format: true\n      formatStyle: \"monokai\"\n      position:\n        top: 1\n        left: 1\n        height: 1\n        width: 1\n      refreshInterval: 15\n    news:\n      type: hackernews\n      title: \"HackerNews\"\n      enabled: true\n      numberOfStories: 10\n      position:\n        top: 2\n        left: 1\n        height: 1\n        width: 3\n      storyType: top\n      refreshInterval: 900\n    resources:\n      type: resourceusage\n      enabled: true\n      position:\n        top: 3\n        left: 0\n        height: 2\n        width: 1\n      refreshInterval: 1\n    uptime:\n      type: cmdrunner\n      args: []\n      cmd: \"uptime\"\n      enabled: true\n      position:\n        top: 4\n        left: 1\n        height: 1\n        width: 3\n      refreshInterval: 30\n    disks:\n      type: cmdrunner\n      cmd: \"df\"\n      args: [\"-h\"]\n      enabled: true\n      position:\n        top: 3\n        left: 1\n        height: 1\n        width: 3\n      refreshInterval: 3600\n"
  },
  {
    "path": "_sample_configs/small_config.yml",
    "content": "wtf:\n  grid:\n    columns: [20, 20]\n    rows: [3, 3]\n  refreshInterval: 1\n  mods:\n    uptime:\n      type: cmdrunner\n      args: []\n      cmd: \"uptime\"\n      enabled: true\n      position:\n        top: 0\n        left: 0\n        height: 1\n        width: 1\n      refreshInterval: 30\n"
  },
  {
    "path": "_sample_configs/uniconfig.yml",
    "content": "wtf:\n  colors:\n    background: black\n    border:\n      focusable: darkslateblue\n  grid:\n    columns: [40, 40]\n    rows: [16]\n  refreshInterval: 1\n  mods:\n    americas_time:\n      title: \"Americas\"\n      type: clocks\n      enabled: true\n      locations:\n        UTC: \"Etc/UTC\"\n        Vancouver: \"America/Vancouver\"\n        New_York: \"America/New_York\"\n        Sao_Paolo: \"America/Sao_Paulo\"\n        Denver: \"America/Denver\"\n        Iqaluit: \"America/Iqaluit\"\n        Bahamas: \"America/Nassau\"\n        Chicago: \"America/Chicago\"\n      position:\n        top: 0\n        left: 0\n        height: 1\n        width: 1\n      refreshInterval: 15\n      sort: \"chronological\"\n    textfile:\n      enabled: true\n      filePaths:\n        - \"~/.config/wtf/config.yml\"\n      format: true\n      formatStyle: \"vim\"\n      position:\n        top: 0\n        left: 1\n        height: 1\n        width: 1\n      refreshInterval: 15\n"
  },
  {
    "path": "app/app_manager.go",
    "content": "package app\n\nimport (\n\t\"errors\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/rivo/tview\"\n)\n\n// WtfAppManager handles the instances of WtfApp, ensuring that they're displayed as requested\ntype WtfAppManager struct {\n\tWtfApps []*WtfApp\n\n\tselected int\n}\n\n// NewAppManager creates and returns an instance of AppManager\nfunc NewAppManager() WtfAppManager {\n\tappMan := WtfAppManager{\n\t\tWtfApps: []*WtfApp{},\n\t}\n\n\treturn appMan\n}\n\n// MakeNewWtfApp creates and starts a new instance of WtfApp from a set of configuration params\nfunc (appMan *WtfAppManager) MakeNewWtfApp(config *config.Config, configFilePath string) {\n\twtfApp := NewWtfApp(tview.NewApplication(), config, configFilePath)\n\tappMan.Add(wtfApp)\n\n\twtfApp.Start()\n}\n\n// Add adds a WtfApp to the collection of apps that the AppManager manages.\n// This app is then available for display onscreen.\nfunc (appMan *WtfAppManager) Add(wtfApp *WtfApp) {\n\tappMan.WtfApps = append(appMan.WtfApps, wtfApp)\n}\n\n// Current returns the currently-displaying instance of WtfApp\nfunc (appMan *WtfAppManager) Current() (*WtfApp, error) {\n\tif appMan.selected < 0 || appMan.selected >= len(appMan.WtfApps) {\n\t\treturn nil, errors.New(\"invalid app index selected\")\n\t}\n\n\treturn appMan.WtfApps[appMan.selected], nil\n}\n\n// Next cycles the WtfApps forward by one, making the next one in the list\n// the current one. If there are none after the current one, it wraps around.\nfunc (appMan *WtfAppManager) Next() (*WtfApp, error) {\n\tappMan.selected++\n\n\tif appMan.selected >= len(appMan.WtfApps) {\n\t\tappMan.selected = 0\n\t}\n\n\treturn appMan.Current()\n}\n\n// Prev cycles the WtfApps backwards by one, making the previous one in the\n// list the current one. If there are none before the current one, it wraps around.\nfunc (appMan *WtfAppManager) Prev() (*WtfApp, error) {\n\tappMan.selected--\n\n\tif appMan.selected < 0 {\n\t\tappMan.selected = len(appMan.WtfApps) - 1\n\t}\n\n\treturn appMan.Current()\n}\n"
  },
  {
    "path": "app/display.go",
    "content": "package app\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\n// Display is the container for the onscreen representation of a WtfApp\ntype Display struct {\n\tGrid   *tview.Grid\n\tconfig *config.Config\n}\n\n// NewDisplay creates and returns a Display\nfunc NewDisplay(widgets []wtf.Wtfable, config *config.Config) *Display {\n\tdisplay := Display{\n\t\tGrid:   tview.NewGrid(),\n\t\tconfig: config,\n\t}\n\n\tfirstWidget := widgets[0]\n\tdisplay.Grid.SetBackgroundColor(\n\t\twtf.ColorFor(\n\t\t\tfirstWidget.CommonSettings().Colors.Background,\n\t\t),\n\t)\n\n\tdisplay.build(widgets)\n\n\treturn &display\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (display *Display) add(widget wtf.Wtfable) {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\tdisplay.Grid.AddItem(\n\t\twidget.TextView(),\n\t\twidget.CommonSettings().Top,\n\t\twidget.CommonSettings().Left,\n\t\twidget.CommonSettings().Height,\n\t\twidget.CommonSettings().Width,\n\t\t0,\n\t\t0,\n\t\tfalse,\n\t)\n}\n\nfunc (display *Display) build(widgets []wtf.Wtfable) *tview.Grid {\n\tcols := utils.ToInts(display.config.UList(\"wtf.grid.columns\"))\n\trows := utils.ToInts(display.config.UList(\"wtf.grid.rows\"))\n\n\tdisplay.Grid.SetColumns(cols...)\n\tdisplay.Grid.SetRows(rows...)\n\tdisplay.Grid.SetBorder(false)\n\n\tfor _, widget := range widgets {\n\t\tdisplay.add(widget)\n\t}\n\n\treturn display.Grid\n}\n"
  },
  {
    "path": "app/exit_message.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/logrusorgru/aurora/v4\"\n\t\"github.com/olebedev/config\"\n)\n\nconst exitMessageHeader = `\n\t      ____    __    ____ .___________. _______\n\t      \\   \\  /  \\  /   / |           ||   ____|\n\t       \\   \\/    \\/   /  ----|  |-----|  |__\n\t        \\            /       |  |     |   __|\n\t         \\    /\\    /        |  |     |  |\n\t          \\__/  \\__/         |__|     |__|\n\n    the personal information dashboard for your terminal\n`\n\n// DisplayExitMessage displays the onscreen exit message when the app quits\nfunc (wtfApp *WtfApp) DisplayExitMessage() {\n\texitMessageIsDisplayable := readDisplayableConfig(wtfApp.config)\n\n\twtfApp.displayExitMsg(exitMessageIsDisplayable)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (wtfApp *WtfApp) displayExitMsg(exitMessageIsDisplayable bool) string {\n\t// If a sponsor or contributor and opt out of seeing the exit message, do not display it\n\tif (wtfApp.ghUser.IsContributor || wtfApp.ghUser.IsSponsor) && !exitMessageIsDisplayable {\n\t\treturn \"\"\n\t}\n\n\tmsgs := []string{}\n\n\tmsgs = append(msgs, aurora.Magenta(exitMessageHeader).String())\n\n\tif wtfApp.ghUser.IsContributor {\n\t\tmsgs = append(msgs, wtfApp.contributorThankYouMessage())\n\t}\n\n\tif wtfApp.ghUser.IsSponsor {\n\t\tmsgs = append(msgs, wtfApp.sponsorThankYouMessage())\n\t}\n\n\tif !wtfApp.ghUser.IsContributor && !wtfApp.ghUser.IsSponsor {\n\t\tmsgs = append(msgs, wtfApp.supportRequestMessage())\n\t}\n\n\tdisplayMsg := strings.Join(msgs, \"\\n\")\n\n\tfmt.Println(displayMsg)\n\n\treturn displayMsg\n}\n\n// readDisplayableConfig figures out whether or not the exit message should be displayed\n// per the user's wishes. It allows contributors and sponsors to opt out of the exit message\nfunc readDisplayableConfig(cfg *config.Config) bool {\n\tdisplayExitMsg := cfg.UBool(\"wtf.exitMessage.display\", true)\n\treturn displayExitMsg\n}\n\n// readGitHubAPIKey attempts to find a GitHub API key somewhere in the configuration file\nfunc readGitHubAPIKey(cfg *config.Config) string {\n\tapiKey := cfg.UString(\"wtf.exitMessage.githubAPIKey\", os.Getenv(\"WTF_GITHUB_TOKEN\"))\n\tif apiKey != \"\" {\n\t\treturn apiKey\n\t}\n\n\tmoduleConfig, err := cfg.Get(\"wtf.mods.github\")\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn moduleConfig.UString(\"apiKey\", \"\")\n}\n\n/* -------------------- Messaging -------------------- */\n\nfunc (wtfApp *WtfApp) contributorThankYouMessage() string {\n\tstr := \"    On behalf of all the users of WTF, thank you for contributing to the source code.\"\n\tstr += fmt.Sprintf(\" %s\", aurora.Green(\"\\n\\n    You rock.\"))\n\n\treturn str\n}\n\nfunc (wtfApp *WtfApp) sponsorThankYouMessage() string {\n\tstr := \"    Your sponsorship of WTF makes a difference. Thank you for sponsoring and supporting WTF.\"\n\tstr += fmt.Sprintf(\" %s\", aurora.Green(\"\\n\\n    You're awesome.\"))\n\n\treturn str\n}\n\nfunc (wtfApp *WtfApp) supportRequestMessage() string {\n\tstr := \"    The development and maintenance of WTF is supported by sponsorships.\\n\"\n\tstr += fmt.Sprintf(\"    Sponsor the development of WTF at %s\\n\", aurora.Green(\"https://github.com/sponsors/FelicianoTech\"))\n\n\treturn str\n}\n"
  },
  {
    "path": "app/exit_message_test.go",
    "content": "package app\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/wtfutil/wtf/support\"\n\t\"gotest.tools/assert\"\n)\n\nfunc Test_displayExitMessage(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tisDisplayable bool\n\t\tisContributor bool\n\t\tisSponsor     bool\n\t\tcompareWith   string\n\t\texpected      string\n\t}{\n\t\t{\n\t\t\tname:          \"when not displayable\",\n\t\t\tisDisplayable: false,\n\t\t\tisContributor: true,\n\t\t\tisSponsor:     true,\n\t\t\tcompareWith:   \"equals\",\n\t\t\texpected:      \"\",\n\t\t},\n\t\t{\n\t\t\tname:          \"when contributor\",\n\t\t\tisDisplayable: true,\n\t\t\tisContributor: true,\n\t\t\tcompareWith:   \"contains\",\n\t\t\texpected:      \"thank you for contributing\",\n\t\t},\n\t\t{\n\t\t\tname:          \"when sponsor\",\n\t\t\tisDisplayable: true,\n\t\t\tisSponsor:     true,\n\t\t\tcompareWith:   \"contains\",\n\t\t\texpected:      \"Thank you for sponsoring\",\n\t\t},\n\t\t{\n\t\t\tname:          \"when user\",\n\t\t\tisDisplayable: true,\n\t\t\tisContributor: false,\n\t\t\tisSponsor:     false,\n\t\t\tcompareWith:   \"contains\",\n\t\t\texpected:      \"supported by sponsorships\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\twtfApp := WtfApp{}\n\t\t\twtfApp.ghUser = &support.GitHubUser{\n\t\t\t\tIsContributor: tt.isContributor,\n\t\t\t\tIsSponsor:     tt.isSponsor,\n\t\t\t}\n\n\t\t\tactual := wtfApp.displayExitMsg(tt.isDisplayable)\n\n\t\t\tif tt.compareWith == \"equals\" {\n\t\t\t\tassert.Equal(t, actual, tt.expected)\n\t\t\t}\n\n\t\t\tif tt.compareWith == \"contains\" {\n\t\t\t\tassert.Equal(t, true, strings.Contains(actual, tt.expected))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/focus_tracker.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\n// FocusState is a custom type that differentiates focusable scopes\ntype FocusState int\n\nconst (\n\twidgetFocused FocusState = iota\n\tappBoardFocused\n\tneverFocused\n)\n\n// FocusTracker is used by the app to track which onscreen widget currently has focus,\n// and to move focus between widgets.\ntype FocusTracker struct {\n\tIdx       int\n\tIsFocused bool\n\tWidgets   []wtf.Wtfable\n\n\tconfig   *config.Config\n\ttviewApp *tview.Application\n}\n\n// NewFocusTracker creates and returns an instance of FocusTracker\nfunc NewFocusTracker(tviewApp *tview.Application, widgets []wtf.Wtfable, config *config.Config) FocusTracker {\n\tfocusTracker := FocusTracker{\n\t\ttviewApp:  tviewApp,\n\t\tIdx:       -1,\n\t\tIsFocused: false,\n\t\tWidgets:   widgets,\n\n\t\tconfig: config,\n\t}\n\n\tfocusTracker.assignHotKeys()\n\n\treturn focusTracker\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// FocusOn puts the focus on the item that belongs to the focus character passed in\nfunc (tracker *FocusTracker) FocusOn(char string) bool {\n\tif !tracker.useNavShortcuts() {\n\t\treturn false\n\t}\n\n\tif tracker.focusState() == appBoardFocused {\n\t\treturn false\n\t}\n\n\thasFocusable := false\n\n\tfor idx, focusable := range tracker.focusables() {\n\t\tif focusable.FocusChar() == char {\n\t\t\ttracker.blur(tracker.Idx)\n\t\t\ttracker.Idx = idx\n\t\t\ttracker.focus(tracker.Idx)\n\n\t\t\thasFocusable = true\n\t\t\ttracker.IsFocused = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn hasFocusable\n}\n\n// Next sets the focus on the next widget in the widget list. If the current widget is\n// the last widget, sets focus on the first widget.\nfunc (tracker *FocusTracker) Next() {\n\tif tracker.focusState() == appBoardFocused {\n\t\treturn\n\t}\n\n\ttracker.blur(tracker.Idx)\n\ttracker.increment()\n\ttracker.focus(tracker.Idx)\n\n\ttracker.IsFocused = true\n}\n\n// None removes focus from the currently-focused widget.\nfunc (tracker *FocusTracker) None() {\n\tif tracker.focusState() == appBoardFocused {\n\t\treturn\n\t}\n\n\ttracker.blur(tracker.Idx)\n}\n\n// Prev sets the focus on the previous widget in the widget list. If the current widget is\n// the last widget, sets focus on the last widget.\nfunc (tracker *FocusTracker) Prev() {\n\tif tracker.focusState() == appBoardFocused {\n\t\treturn\n\t}\n\n\ttracker.blur(tracker.Idx)\n\ttracker.decrement()\n\ttracker.focus(tracker.Idx)\n\n\ttracker.IsFocused = true\n}\n\n// Refocus forces the focus back to the currently-selected item\nfunc (tracker *FocusTracker) Refocus() {\n\ttracker.focus(tracker.Idx)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// AssignHotKeys assigns an alphabetic keyboard character to each focusable\n// widget so that the widget can be brought into focus by pressing that keyboard key\n// Valid numbers are between 1 and 9, inclusive\nfunc (tracker *FocusTracker) assignHotKeys() {\n\tif !tracker.useNavShortcuts() {\n\t\treturn\n\t}\n\n\tusedKeys := make(map[string]bool)\n\tfocusables := tracker.focusables()\n\n\t// First, block out the explicitly-defined characters so they can't be automatically\n\t// assigned to other modules\n\tfor _, focusable := range focusables {\n\t\tif focusable.FocusChar() != \"\" {\n\t\t\tusedKeys[focusable.FocusChar()] = true\n\t\t}\n\t}\n\n\tfocusNum := 1\n\n\t// Range over all the modules and assign focus characters to any that are focusable\n\t// and don't have explicitly-defined focus characters\n\tfor _, focusable := range focusables {\n\t\tif focusable.FocusChar() != \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, foundKey := usedKeys[fmt.Sprint(focusNum)]; foundKey {\n\t\t\tfor ; foundKey; _, foundKey = usedKeys[fmt.Sprint(focusNum)] {\n\t\t\t\tfocusNum++\n\t\t\t}\n\t\t}\n\n\t\t// Don't allow focus characters > \"9\"\n\t\tif focusNum >= 10 {\n\t\t\tbreak\n\t\t}\n\n\t\tfocusable.SetFocusChar(fmt.Sprint(focusNum))\n\t\tfocusNum++\n\t}\n}\n\nfunc (tracker *FocusTracker) blur(idx int) {\n\twidget := tracker.focusableAt(idx)\n\tif widget == nil {\n\t\treturn\n\t}\n\n\tview := widget.TextView()\n\tview.Blur()\n\n\tview.SetBorderColor(\n\t\twtf.ColorFor(\n\t\t\twidget.BorderColor(),\n\t\t),\n\t)\n\n\ttracker.IsFocused = false\n}\n\nfunc (tracker *FocusTracker) decrement() {\n\ttracker.Idx--\n\n\tif tracker.Idx < 0 {\n\t\ttracker.Idx = len(tracker.focusables()) - 1\n\t}\n}\n\nfunc (tracker *FocusTracker) focus(idx int) {\n\twidget := tracker.focusableAt(idx)\n\tif widget == nil {\n\t\treturn\n\t}\n\n\tview := widget.TextView()\n\tview.SetBorderColor(\n\t\twtf.ColorFor(\n\t\t\twidget.CommonSettings().Colors.Focused,\n\t\t),\n\t)\n\ttracker.tviewApp.SetFocus(view)\n}\n\nfunc (tracker *FocusTracker) focusables() []wtf.Wtfable {\n\tfocusable := []wtf.Wtfable{}\n\n\tfor _, widget := range tracker.Widgets {\n\t\tif widget.Focusable() {\n\t\t\tfocusable = append(focusable, widget)\n\t\t}\n\t}\n\n\t// Sort for deterministic ordering\n\tsort.SliceStable(focusable, func(i, j int) bool {\n\t\tiTop := focusable[i].CommonSettings().Top\n\t\tjTop := focusable[j].CommonSettings().Top\n\n\t\tif iTop < jTop {\n\t\t\treturn true\n\t\t}\n\t\tif iTop == jTop {\n\t\t\treturn focusable[i].CommonSettings().Left < focusable[j].CommonSettings().Left\n\t\t}\n\t\treturn false\n\t})\n\n\treturn focusable\n}\n\nfunc (tracker *FocusTracker) focusableAt(idx int) wtf.Wtfable {\n\tif idx < 0 || idx >= len(tracker.focusables()) {\n\t\treturn nil\n\t}\n\n\treturn tracker.focusables()[idx]\n}\n\nfunc (tracker *FocusTracker) focusState() FocusState {\n\tif tracker.Idx < 0 {\n\t\treturn neverFocused\n\t}\n\n\tfor _, widget := range tracker.Widgets {\n\t\tif widget.TextView() == tracker.tviewApp.GetFocus() {\n\t\t\treturn widgetFocused\n\t\t}\n\t}\n\n\treturn appBoardFocused\n}\n\nfunc (tracker *FocusTracker) increment() {\n\ttracker.Idx++\n\n\tif tracker.Idx == len(tracker.focusables()) {\n\t\ttracker.Idx = 0\n\t}\n}\n\nfunc (tracker *FocusTracker) useNavShortcuts() bool {\n\treturn tracker.config.UBool(\"wtf.navigation.shortcuts\", true)\n}\n"
  },
  {
    "path": "app/module_validator.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/logrusorgru/aurora/v4\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\n// ModuleValidator is responsible for validating the state of a module's configuration\ntype ModuleValidator struct{}\n\ntype widgetError struct {\n\tname             string\n\tvalidationErrors []cfg.Validatable\n}\n\n// NewModuleValidator creates and returns an instance of ModuleValidator\nfunc NewModuleValidator() *ModuleValidator {\n\treturn &ModuleValidator{}\n}\n\n// Validate rolls through all the enabled widgets and looks for configuration errors.\n// If it finds any it stringifies them, writes them to the console, and kills the app gracefully\nfunc (val *ModuleValidator) Validate(widgets []wtf.Wtfable) {\n\tvalidationErrors := validate(widgets)\n\n\tif len(validationErrors) > 0 {\n\t\tfmt.Println()\n\t\tfor _, error := range validationErrors {\n\t\t\tfor _, message := range error.errorMessages() {\n\t\t\t\tfmt.Println(message)\n\t\t\t}\n\t\t}\n\t\tfmt.Println()\n\n\t\tos.Exit(1)\n\t}\n}\n\nfunc validate(widgets []wtf.Wtfable) (widgetErrors []widgetError) {\n\tfor _, widget := range widgets {\n\t\terr := widgetError{name: widget.Name()}\n\n\t\tfor _, val := range widget.CommonSettings().Validations() {\n\t\t\tif val.HasError() {\n\t\t\t\terr.validationErrors = append(err.validationErrors, val)\n\t\t\t}\n\t\t}\n\n\t\tif len(err.validationErrors) > 0 {\n\t\t\twidgetErrors = append(widgetErrors, err)\n\t\t}\n\t}\n\n\treturn widgetErrors\n}\n\nfunc (err widgetError) errorMessages() (messages []string) {\n\twidgetMessage := fmt.Sprintf(\n\t\t\"%s in %s configuration\",\n\t\taurora.Red(\"Errors\"),\n\t\taurora.Yellow(\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"%s.position\",\n\t\t\t\terr.name,\n\t\t\t),\n\t\t),\n\t)\n\tmessages = append(messages, widgetMessage)\n\n\tfor _, e := range err.validationErrors {\n\t\tconfigMessage := fmt.Sprintf(\" - %s\\t%s %v\", e.String(), aurora.Red(\"Error:\"), e.Error())\n\n\t\tmessages = append(messages, configMessage)\n\t}\n\n\treturn messages\n}\n"
  },
  {
    "path": "app/module_validator_test.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/logrusorgru/aurora/v4\"\n\t\"github.com/olebedev/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\nconst (\n\tvalid = `\nwtf:\n  mods:\n    clocks:\n      enabled: true\n      position:\n        top: 0\n        left: 0\n        height: 1\n        width: 1\n      refreshInterval: 30`\n\n\tinvalid = `\nwtf:\n  mods:\n    clocks:\n      enabled: true\n      position:\n        top: abc\n        left: 0\n        height: 1\n        width: 1\n      refreshInterval: 30`\n)\n\nfunc Test_NewModuleValidator(t *testing.T) {\n\tassert.IsType(t, &ModuleValidator{}, NewModuleValidator())\n}\n\nfunc Test_validate(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tmoduleName string\n\t\tconfig     *config.Config\n\t\texpected   []string\n\t}{\n\t\t{\n\t\t\tname:       \"valid config\",\n\t\t\tmoduleName: \"clocks\",\n\t\t\tconfig: func() *config.Config {\n\t\t\t\tcfg, _ := config.ParseYaml(valid)\n\t\t\t\treturn cfg\n\t\t\t}(),\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:       \"invalid config\",\n\t\t\tmoduleName: \"clocks\",\n\t\t\tconfig: func() *config.Config {\n\t\t\t\tcfg, _ := config.ParseYaml(invalid)\n\t\t\t\treturn cfg\n\t\t\t}(),\n\t\t\texpected: []string{\n\t\t\t\tfmt.Sprintf(\"%s in %s configuration\", aurora.Red(\"Errors\"), aurora.Yellow(\"clocks.position\")),\n\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\" - Invalid value for %s:\t0\t%s strconv.ParseInt: parsing \\\"abc\\\": invalid syntax\",\n\t\t\t\t\taurora.Yellow(\"top\"),\n\t\t\t\t\taurora.Red(\"Error:\"),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\twidget := MakeWidget(nil, nil, tt.moduleName, tt.config, make(chan bool))\n\n\t\t\tif widget == nil {\n\t\t\t\tt.Logf(\"Failed to create widget %s\", tt.moduleName)\n\t\t\t\tt.FailNow()\n\t\t\t}\n\n\t\t\terrs := validate([]wtf.Wtfable{widget})\n\n\t\t\tif len(tt.expected) == 0 {\n\t\t\t\tassert.Empty(t, errs)\n\t\t\t} else {\n\t\t\t\tassert.NotEmpty(t, errs)\n\n\t\t\t\tvar actual []string\n\t\t\t\tfor _, err := range errs {\n\t\t\t\t\tactual = append(actual, err.errorMessages()...)\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/scheduler.go",
    "content": "package app\n\nimport (\n\t\"time\"\n\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\n// Schedule kicks off the first refresh of a module's data and then queues the rest of the\n// data refreshes on a timer\nfunc Schedule(widget wtf.Wtfable) {\n\twidget.Refresh()\n\n\tinterval := widget.CommonSettings().RefreshInterval\n\n\tif interval <= 0 {\n\t\treturn\n\t}\n\n\ttimer := time.NewTicker(interval)\n\n\tfor {\n\t\tselect {\n\t\tcase <-timer.C:\n\t\t\tif widget.Enabled() {\n\t\t\t\twidget.Refresh()\n\t\t\t} else {\n\t\t\t\ttimer.Stop()\n\t\t\t\treturn\n\t\t\t}\n\t\tcase quit := <-widget.QuitChan():\n\t\t\tif quit {\n\t\t\t\ttimer.Stop()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "app/scheduler_test.go",
    "content": "package app\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/olebedev/config\"\n)\n\nconst (\n\tconfigExample = `\n  wtf:\n    mods:\n      clocks:\n        enabled: true\n        position:\n          top: 0\n          left: 0\n          height: 1\n          width: 1\n        refreshInterval: 2`\n\n\tnew = `\n  wtf:\n    mods:\n      clocks:\n        enabled: true\n        position:\n          top: 0\n          left: 0\n          height: 1\n          width: 1\n        refreshInterval: 100ms`\n)\n\nfunc Test_RefreshInterval(t *testing.T) {\n\tt.Skip() // slow running test because a ticker is tested\n\ttests := []struct {\n\t\tname         string\n\t\tmoduleName   string\n\t\tconfig       *config.Config\n\t\ttestAttempts int\n\t\texpected     time.Duration\n\t}{\n\t\t{\n\t\t\tname:       \"slow ticking module\",\n\t\t\tmoduleName: \"clocks\",\n\t\t\tconfig: func() *config.Config {\n\t\t\t\tcfg, _ := config.ParseYaml(configExample)\n\t\t\t\treturn cfg\n\t\t\t}(),\n\t\t\ttestAttempts: 10,\n\t\t\texpected:     2 * time.Second,\n\t\t},\n\t\t{\n\t\t\tname:       \"fast ticking module\",\n\t\t\tmoduleName: \"clocks\",\n\t\t\tconfig: func() *config.Config {\n\t\t\t\tcfg, _ := config.ParseYaml(new)\n\t\t\t\treturn cfg\n\t\t\t}(),\n\t\t\ttestAttempts: 10,\n\t\t\texpected:     100 * time.Millisecond,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\twidget := MakeWidget(nil, nil, tt.moduleName, tt.config, make(chan bool))\n\n\t\t\tinterval := widget.CommonSettings().RefreshInterval // same declaration as in scheduler.go#Schedule\n\t\t\ttimer := time.NewTicker(interval)\n\n\t\t\tattempts := 0\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-timer.C:\n\t\t\t\t\tattempts++\n\t\t\t\t\tif attempts == tt.testAttempts {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t// allow for small window (50ms) where a timeout is not triggered\n\t\t\t\tcase <-time.After(tt.expected + 50*time.Millisecond):\n\t\t\t\t\tt.Error(\"Timeout\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/widget_maker.go",
    "content": "package app\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/modules/airbrake\"\n\t\"github.com/wtfutil/wtf/modules/asana\"\n\t\"github.com/wtfutil/wtf/modules/azuredevops\"\n\t\"github.com/wtfutil/wtf/modules/azurelogs\"\n\t\"github.com/wtfutil/wtf/modules/bamboohr\"\n\t\"github.com/wtfutil/wtf/modules/bargraph\"\n\t\"github.com/wtfutil/wtf/modules/buildkite\"\n\tcdsfavorites \"github.com/wtfutil/wtf/modules/cds/favorites\"\n\tcdsqueue \"github.com/wtfutil/wtf/modules/cds/queue\"\n\tcdsstatus \"github.com/wtfutil/wtf/modules/cds/status\"\n\t\"github.com/wtfutil/wtf/modules/circleci\"\n\t\"github.com/wtfutil/wtf/modules/clocks\"\n\t\"github.com/wtfutil/wtf/modules/cmdrunner\"\n\t\"github.com/wtfutil/wtf/modules/cryptocurrency/bittrex\"\n\t\"github.com/wtfutil/wtf/modules/cryptocurrency/blockfolio\"\n\t\"github.com/wtfutil/wtf/modules/cryptocurrency/cryptolive\"\n\t\"github.com/wtfutil/wtf/modules/cryptocurrency/mempool\"\n\t\"github.com/wtfutil/wtf/modules/datadog\"\n\t\"github.com/wtfutil/wtf/modules/devto\"\n\t\"github.com/wtfutil/wtf/modules/digitalclock\"\n\t\"github.com/wtfutil/wtf/modules/digitalocean\"\n\t\"github.com/wtfutil/wtf/modules/docker\"\n\t\"github.com/wtfutil/wtf/modules/feedreader\"\n\t\"github.com/wtfutil/wtf/modules/football\"\n\t\"github.com/wtfutil/wtf/modules/gcal\"\n\t\"github.com/wtfutil/wtf/modules/gerrit\"\n\t\"github.com/wtfutil/wtf/modules/git\"\n\t\"github.com/wtfutil/wtf/modules/github\"\n\t\"github.com/wtfutil/wtf/modules/gitlab\"\n\t\"github.com/wtfutil/wtf/modules/gitlabtodo\"\n\t\"github.com/wtfutil/wtf/modules/gitter\"\n\t\"github.com/wtfutil/wtf/modules/googleanalytics\"\n\t\"github.com/wtfutil/wtf/modules/grafana\"\n\t\"github.com/wtfutil/wtf/modules/gspreadsheets\"\n\t\"github.com/wtfutil/wtf/modules/hackernews\"\n\t\"github.com/wtfutil/wtf/modules/healthchecks\"\n\t\"github.com/wtfutil/wtf/modules/hibp\"\n\t\"github.com/wtfutil/wtf/modules/ipaddresses/ipapi\"\n\t\"github.com/wtfutil/wtf/modules/ipaddresses/ipinfo\"\n\t\"github.com/wtfutil/wtf/modules/jenkins\"\n\t\"github.com/wtfutil/wtf/modules/jira\"\n\t\"github.com/wtfutil/wtf/modules/krisinformation\"\n\t\"github.com/wtfutil/wtf/modules/kubernetes\"\n\t\"github.com/wtfutil/wtf/modules/logger\"\n\t\"github.com/wtfutil/wtf/modules/lunarphase\"\n\t\"github.com/wtfutil/wtf/modules/mercurial\"\n\t\"github.com/wtfutil/wtf/modules/nbascore\"\n\t\"github.com/wtfutil/wtf/modules/newrelic\"\n\t\"github.com/wtfutil/wtf/modules/nextbus\"\n\t\"github.com/wtfutil/wtf/modules/opsgenie\"\n\t\"github.com/wtfutil/wtf/modules/pagerduty\"\n\t\"github.com/wtfutil/wtf/modules/pihole\"\n\t\"github.com/wtfutil/wtf/modules/ping\"\n\t\"github.com/wtfutil/wtf/modules/pivotal\"\n\t\"github.com/wtfutil/wtf/modules/pocket\"\n\t\"github.com/wtfutil/wtf/modules/power\"\n\t\"github.com/wtfutil/wtf/modules/progress\"\n\t\"github.com/wtfutil/wtf/modules/resourceusage\"\n\t\"github.com/wtfutil/wtf/modules/rollbar\"\n\t\"github.com/wtfutil/wtf/modules/security\"\n\t\"github.com/wtfutil/wtf/modules/spacex\"\n\t\"github.com/wtfutil/wtf/modules/spotify\"\n\t\"github.com/wtfutil/wtf/modules/spotifyweb\"\n\t\"github.com/wtfutil/wtf/modules/status\"\n\t\"github.com/wtfutil/wtf/modules/steam\"\n\t\"github.com/wtfutil/wtf/modules/stocks/finnhub\"\n\t\"github.com/wtfutil/wtf/modules/stocks/yfinance\"\n\t\"github.com/wtfutil/wtf/modules/subreddit\"\n\t\"github.com/wtfutil/wtf/modules/textfile\"\n\t\"github.com/wtfutil/wtf/modules/todo\"\n\t\"github.com/wtfutil/wtf/modules/todo_plus\"\n\t\"github.com/wtfutil/wtf/modules/transmission\"\n\t\"github.com/wtfutil/wtf/modules/travisci\"\n\t\"github.com/wtfutil/wtf/modules/twitch\"\n\t\"github.com/wtfutil/wtf/modules/twitter\"\n\t\"github.com/wtfutil/wtf/modules/twitterstats\"\n\t\"github.com/wtfutil/wtf/modules/unknown\"\n\t\"github.com/wtfutil/wtf/modules/updown\"\n\t\"github.com/wtfutil/wtf/modules/uptimekuma\"\n\t\"github.com/wtfutil/wtf/modules/uptimerobot\"\n\t\"github.com/wtfutil/wtf/modules/urlcheck\"\n\t\"github.com/wtfutil/wtf/modules/victorops\"\n\t\"github.com/wtfutil/wtf/modules/weatherservices/arpansagovau\"\n\t\"github.com/wtfutil/wtf/modules/weatherservices/prettyweather\"\n\t\"github.com/wtfutil/wtf/modules/weatherservices/weather\"\n\t\"github.com/wtfutil/wtf/modules/zendesk\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\n// MakeWidget creates and returns instances of widgets\nfunc MakeWidget(\n\ttviewApp *tview.Application,\n\tpages *tview.Pages,\n\tmoduleName string,\n\tconfig *config.Config,\n\tredrawChan chan bool,\n) wtf.Wtfable {\n\tvar widget wtf.Wtfable\n\n\tmoduleConfig, _ := config.Get(\"wtf.mods.\" + moduleName)\n\n\t// Don' try to initialize modules that don't exist\n\tif moduleConfig == nil {\n\t\treturn nil\n\t}\n\n\t// Don't try to initialize modules that aren't enabled\n\tif enabled := moduleConfig.UBool(\"enabled\", false); !enabled {\n\t\treturn nil\n\t}\n\n\t// Always in alphabetical order\n\tswitch moduleConfig.UString(\"type\", moduleName) {\n\tcase \"airbrake\":\n\t\tsettings := airbrake.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = airbrake.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"arpansagovau\":\n\t\tsettings := arpansagovau.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = arpansagovau.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"asana\":\n\t\tsettings := asana.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = asana.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"azuredevops\":\n\t\tsettings := azuredevops.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = azuredevops.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"azurelogs\":\n\t\tsettings := azurelogs.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = azurelogs.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"bamboohr\":\n\t\tsettings := bamboohr.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = bamboohr.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"bargraph\":\n\t\tsettings := bargraph.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = bargraph.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"bittrex\":\n\t\tsettings := bittrex.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = bittrex.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"blockfolio\":\n\t\tsettings := blockfolio.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = blockfolio.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"buildkite\":\n\t\tsettings := buildkite.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = buildkite.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"cdsFavorites\":\n\t\tsettings := cdsfavorites.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = cdsfavorites.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"cdsQueue\":\n\t\tsettings := cdsqueue.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = cdsqueue.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"cdsStatus\":\n\t\tsettings := cdsstatus.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = cdsstatus.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"circleci\":\n\t\tsettings := circleci.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = circleci.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"clocks\":\n\t\tsettings := clocks.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = clocks.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"cmdrunner\":\n\t\tsettings := cmdrunner.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = cmdrunner.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"cryptolive\":\n\t\tsettings := cryptolive.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = cryptolive.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"datadog\":\n\t\tsettings := datadog.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = datadog.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"devto\":\n\t\tsettings := devto.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = devto.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"digitalclock\":\n\t\tsettings := digitalclock.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = digitalclock.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"digitalocean\":\n\t\tsettings := digitalocean.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = digitalocean.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"docker\":\n\t\tsettings := docker.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = docker.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"feedreader\":\n\t\tsettings := feedreader.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = feedreader.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"football\":\n\t\tsettings := football.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = football.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"gcal\":\n\t\tsettings := gcal.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = gcal.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"gerrit\":\n\t\tsettings := gerrit.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = gerrit.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"git\":\n\t\tsettings := git.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = git.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"github\":\n\t\tsettings := github.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = github.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"gitlab\":\n\t\tsettings := gitlab.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = gitlab.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"gitlabtodo\":\n\t\tsettings := gitlabtodo.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = gitlabtodo.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"gitter\":\n\t\tsettings := gitter.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = gitter.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"googleanalytics\":\n\t\tsettings := googleanalytics.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = googleanalytics.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"gspreadsheets\":\n\t\tsettings := gspreadsheets.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = gspreadsheets.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"grafana\":\n\t\tsettings := grafana.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = grafana.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"hackernews\":\n\t\tsettings := hackernews.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = hackernews.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"healthchecks\":\n\t\tsettings := healthchecks.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = healthchecks.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"hibp\":\n\t\tsettings := hibp.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = hibp.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"ipapi\":\n\t\tsettings := ipapi.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = ipapi.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"ipinfo\":\n\t\tsettings := ipinfo.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = ipinfo.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"jenkins\":\n\t\tsettings := jenkins.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = jenkins.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"jira\":\n\t\tsettings := jira.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = jira.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"kubernetes\":\n\t\tsettings := kubernetes.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = kubernetes.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"krisinformation\":\n\t\tsettings := krisinformation.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = krisinformation.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"logger\":\n\t\tsettings := logger.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = logger.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"lunarphase\":\n\t\tsettings := lunarphase.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = lunarphase.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"mercurial\":\n\t\tsettings := mercurial.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = mercurial.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"mempool\":\n\t\tsettings := mempool.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = mempool.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"nbascore\":\n\t\tsettings := nbascore.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = nbascore.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"newrelic\":\n\t\tsettings := newrelic.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = newrelic.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"nextbus\":\n\t\tsettings := nextbus.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = nextbus.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"opsgenie\":\n\t\tsettings := opsgenie.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = opsgenie.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"pagerduty\":\n\t\tsettings := pagerduty.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = pagerduty.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"pihole\":\n\t\tsettings := pihole.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = pihole.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"ping\":\n\t\tsettings := ping.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = ping.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"power\":\n\t\tsettings := power.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = power.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"prettyweather\":\n\t\tsettings := prettyweather.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = prettyweather.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"progress\":\n\t\tsettings := progress.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = progress.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"pocket\":\n\t\tsettings := pocket.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = pocket.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"resourceusage\":\n\t\tsettings := resourceusage.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = resourceusage.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"rollbar\":\n\t\tsettings := rollbar.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = rollbar.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"security\":\n\t\tsettings := security.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = security.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"spacex\":\n\t\tsettings := spacex.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = spacex.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"spotify\":\n\t\tsettings := spotify.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = spotify.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"spotifyweb\":\n\t\tsettings := spotifyweb.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = spotifyweb.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"status\":\n\t\tsettings := status.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = status.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"steam\":\n\t\tsettings := steam.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = steam.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"subreddit\":\n\t\tsettings := subreddit.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = subreddit.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"textfile\":\n\t\tsettings := textfile.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = textfile.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"todo\":\n\t\tsettings := todo.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = todo.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"todo_plus\":\n\t\tsettings := todo_plus.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = todo_plus.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"todoist\":\n\t\tsettings := todo_plus.FromTodoist(moduleName, moduleConfig, config)\n\t\twidget = todo_plus.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"transmission\":\n\t\tsettings := transmission.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = transmission.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"travisci\":\n\t\tsettings := travisci.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = travisci.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"trello\":\n\t\tsettings := todo_plus.FromTrello(moduleName, moduleConfig, config)\n\t\twidget = todo_plus.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"twitch\":\n\t\tsettings := twitch.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = twitch.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"twitter\":\n\t\tsettings := twitter.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = twitter.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"twitterstats\":\n\t\tsettings := twitterstats.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = twitterstats.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"updown\":\n\t\tsettings := updown.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = updown.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"uptimekuma\":\n\t\tsettings := uptimekuma.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = uptimekuma.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"uptimerobot\":\n\t\tsettings := uptimerobot.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = uptimerobot.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"urlcheck\":\n\t\tsettings := urlcheck.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = urlcheck.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"victorops\":\n\t\tsettings := victorops.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = victorops.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"weather\":\n\t\tsettings := weather.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = weather.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"zendesk\":\n\t\tsettings := zendesk.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = zendesk.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"pivotal\":\n\t\tsettings := pivotal.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = pivotal.NewWidget(tviewApp, redrawChan, pages, settings)\n\tcase \"finnhub\":\n\t\tsettings := finnhub.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = finnhub.NewWidget(tviewApp, redrawChan, settings)\n\tcase \"yfinance\":\n\t\tsettings := yfinance.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = yfinance.NewWidget(tviewApp, redrawChan, settings)\n\tdefault:\n\t\tsettings := unknown.NewSettingsFromYAML(moduleName, moduleConfig, config)\n\t\twidget = unknown.NewWidget(tviewApp, redrawChan, settings)\n\t}\n\n\treturn widget\n}\n\n// MakeWidgets creates and returns a collection of enabled widgets\nfunc MakeWidgets(tviewApp *tview.Application, pages *tview.Pages, config *config.Config, redrawChan chan bool) []wtf.Wtfable {\n\tvar widgets []wtf.Wtfable\n\n\tmoduleNames, _ := config.Map(\"wtf.mods\")\n\n\tfor moduleName := range moduleNames {\n\t\twidget := MakeWidget(tviewApp, pages, moduleName, config, redrawChan)\n\n\t\tif widget != nil {\n\t\t\twidgets = append(widgets, widget)\n\t\t}\n\t}\n\n\treturn widgets\n}\n"
  },
  {
    "path": "app/widget_maker_test.go",
    "content": "package app\n\nimport (\n\t\"testing\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/wtfutil/wtf/modules/clocks\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\nconst (\n\tdisabled = `\nwtf:\n  mods:\n    clocks:\n      enabled: false\n      position:\n        top: 0\n        left: 0\n        height: 1\n        width: 1\n      refreshInterval: 30`\n\n\tenabled = `\nwtf:\n  mods:\n    clocks:\n      enabled: true\n      position:\n        top: 0\n        left: 0\n        height: 1\n        width: 1\n      refreshInterval: 30`\n)\n\nfunc Test_MakeWidget(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tmoduleName string\n\t\tconfig     *config.Config\n\t\texpected   wtf.Wtfable\n\t}{\n\t\t{\n\t\t\tname:       \"invalid module\",\n\t\t\tmoduleName: \"\",\n\t\t\tconfig:     &config.Config{},\n\t\t\texpected:   nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"valid disabled module\",\n\t\t\tmoduleName: \"clocks\",\n\t\t\tconfig: func() *config.Config {\n\t\t\t\tcfg, _ := config.ParseYaml(disabled)\n\t\t\t\treturn cfg\n\t\t\t}(),\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"valid enabled module\",\n\t\t\tmoduleName: \"clocks\",\n\t\t\tconfig: func() *config.Config {\n\t\t\t\tcfg, _ := config.ParseYaml(enabled)\n\t\t\t\treturn cfg\n\t\t\t}(),\n\t\t\texpected: &clocks.Widget{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := MakeWidget(nil, nil, tt.moduleName, tt.config, make(chan bool))\n\t\t\tassert.IsType(t, tt.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/wtf_app.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\t_ \"github.com/gdamore/tcell/terminfo/extended\"\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/olebedev/config\"\n\t\"github.com/radovskyb/watcher\"\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/support\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\n// WtfApp is the container for a collection of widgets that are all constructed from a single\n// configuration file and displayed together\ntype WtfApp struct {\n\tTViewApp *tview.Application\n\n\tconfig         *config.Config\n\tconfigFilePath string\n\tdisplay        *Display\n\tfocusTracker   FocusTracker\n\tghUser         *support.GitHubUser\n\tpages          *tview.Pages\n\tvalidator      *ModuleValidator\n\twidgets        []wtf.Wtfable\n\tconfigWatcher  *watcher.Watcher\n\n\t// The redrawChan channel is used to allow modules to signal back to the main loop that\n\t// the screen needs to be explicitly redrawn, instead of waiting for tcell to redraw\n\t// on a user event, because something has visually changed\n\tredrawChan chan bool\n}\n\n// NewWtfApp creates and returns an instance of WtfApp\nfunc NewWtfApp(tviewApp *tview.Application, config *config.Config, configFilePath string) *WtfApp {\n\twtfApp := &WtfApp{\n\t\tTViewApp: tviewApp,\n\n\t\tconfig:         config,\n\t\tconfigFilePath: configFilePath,\n\t\tpages:          tview.NewPages(),\n\n\t\tredrawChan: make(chan bool, 1),\n\t}\n\n\twtfApp.TViewApp.SetBeforeDrawFunc(func(s tcell.Screen) bool {\n\t\ts.Clear()\n\t\treturn false\n\t})\n\n\twtfApp.widgets = MakeWidgets(wtfApp.TViewApp, wtfApp.pages, wtfApp.config, wtfApp.redrawChan)\n\tif len(wtfApp.widgets) == 0 {\n\t\tfmt.Println(\"No modules were defined. Make sure you have at least one properly defined widget\")\n\t\tos.Exit(1)\n\t}\n\n\twtfApp.display = NewDisplay(wtfApp.widgets, wtfApp.config)\n\twtfApp.focusTracker = NewFocusTracker(wtfApp.TViewApp, wtfApp.widgets, wtfApp.config)\n\twtfApp.validator = NewModuleValidator()\n\n\tgithubAPIKey := readGitHubAPIKey(wtfApp.config)\n\twtfApp.ghUser = support.NewGitHubUser(githubAPIKey)\n\n\twtfApp.pages.AddPage(\"grid\", wtfApp.display.Grid, true, true)\n\n\twtfApp.validator.Validate(wtfApp.widgets)\n\n\tfirstWidget := wtfApp.widgets[0]\n\twtfApp.pages.SetBackgroundColor(\n\t\twtf.ColorFor(\n\t\t\tfirstWidget.CommonSettings().Colors.Background,\n\t\t),\n\t)\n\n\twtfApp.TViewApp.SetInputCapture(wtfApp.keyboardIntercept)\n\twtfApp.TViewApp.SetRoot(wtfApp.pages, true)\n\n\t// Create a watcher to handle calls to redraw the screen\n\tgo handleRedraws(wtfApp.TViewApp, wtfApp.redrawChan)\n\n\treturn wtfApp\n}\n\nfunc handleRedraws(tviewApp *tview.Application, redrawChan chan bool) {\n\tif redrawChan == nil {\n\t\treturn\n\t}\n\n\tfor {\n\t\tdata, ok := <-redrawChan\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\n\t\tif data {\n\t\t\ttviewApp.Draw()\n\t\t}\n\t}\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Exit quits the app\nfunc (wtfApp *WtfApp) Exit() {\n\twtfApp.Stop()\n\twtfApp.TViewApp.Stop()\n\twtfApp.DisplayExitMessage()\n\tos.Exit(0)\n}\n\n// Execute starts the underlying tview app\nfunc (wtfApp *WtfApp) Execute() error {\n\tif err := wtfApp.TViewApp.Run(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Start initializes the app\nfunc (wtfApp *WtfApp) Start() {\n\tgo wtfApp.scheduleWidgets()\n\tgo wtfApp.watchForConfigChanges()\n\n\t// FIXME: This should be moved to the AppManager\n\tgo func() { _ = wtfApp.ghUser.Load() }()\n}\n\n// Stop kills all the currently-running widgets in this app\nfunc (wtfApp *WtfApp) Stop() {\n\twtfApp.stopAllWidgets()\n\tif wtfApp.configWatcher != nil {\n\t\twtfApp.configWatcher.Close()\n\t}\n\tclose(wtfApp.redrawChan)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (wtfApp *WtfApp) stopAllWidgets() {\n\tfor _, widget := range wtfApp.widgets {\n\t\twidget.Stop()\n\t}\n}\n\nfunc (wtfApp *WtfApp) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey {\n\t// These keys are global keys used by the app. Widgets should not implement these keys\n\tswitch event.Key() {\n\tcase tcell.KeyCtrlC:\n\t\twtfApp.Stop()\n\t\twtfApp.TViewApp.Stop()\n\t\twtfApp.DisplayExitMessage()\n\tcase tcell.KeyCtrlR:\n\t\twtfApp.refreshAllWidgets()\n\t\treturn nil\n\tcase tcell.KeyCtrlSpace:\n\t\t// FIXME: This can't reside in the app, the app doesn't know about\n\t\t// the AppManager. The AppManager needs to catch this one\n\t\tfmt.Println(\"Next app\")\n\t\treturn nil\n\tcase tcell.KeyTab:\n\t\twtfApp.focusTracker.Next()\n\tcase tcell.KeyBacktab:\n\t\twtfApp.focusTracker.Prev()\n\t\treturn nil\n\tcase tcell.KeyEsc:\n\t\twtfApp.focusTracker.None()\n\t}\n\n\t// Checks to see if any widget has been assigned the pressed key as its focus key\n\tif wtfApp.focusTracker.FocusOn(string(event.Rune())) {\n\t\treturn nil\n\t}\n\n\t// If no specific widget has focus, then allow the key presses to fall through to the app\n\tif !wtfApp.focusTracker.IsFocused {\n\t\tswitch string(event.Rune()) {\n\t\tcase \"q\":\n\t\t\twtfApp.Exit()\n\t\tcase \"/\":\n\t\t\treturn nil\n\t\tdefault:\n\t\t}\n\t}\n\n\treturn event\n}\n\nfunc (wtfApp *WtfApp) refreshAllWidgets() {\n\tfor _, widget := range wtfApp.widgets {\n\t\tgo widget.Refresh()\n\t}\n}\n\nfunc (wtfApp *WtfApp) scheduleWidgets() {\n\tfor _, widget := range wtfApp.widgets {\n\t\tgo Schedule(widget)\n\t}\n}\n\nfunc (wtfApp *WtfApp) watchForConfigChanges() {\n\twtfApp.configWatcher = watcher.New()\n\twatch := wtfApp.configWatcher\n\n\t// Notify write events\n\twatch.FilterOps(watcher.Write)\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-watch.Event:\n\t\t\t\twtfApp.Stop()\n\n\t\t\t\tconfig := cfg.LoadWtfConfigFile(wtfApp.configFilePath)\n\t\t\t\tnewApp := NewWtfApp(wtfApp.TViewApp, config, wtfApp.configFilePath)\n\t\t\t\topenURLUtil := utils.ToStrs(config.UList(\"wtf.openUrlUtil\", []interface{}{}))\n\t\t\t\tutils.Init(config.UString(\"wtf.openFileUtil\", \"open\"), openURLUtil)\n\n\t\t\t\tnewApp.Start()\n\t\t\tcase err := <-watch.Error:\n\t\t\t\tif err == watcher.ErrWatchedFileDeleted {\n\t\t\t\t\t// Usually happens because the watcher looks for the file as the OS is updating it\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tlog.Fatalln(err)\n\t\t\tcase <-watch.Closed:\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Watch config file for changes.\n\tabsPath, _ := utils.ExpandHomeDir(wtfApp.configFilePath)\n\tif err := watch.Add(absPath); err != nil {\n\t\tlog.Fatalln(err)\n\t}\n\n\t// Start the watching process - it'll check for changes every 100ms.\n\tif err := watch.Start(time.Millisecond * 100); err != nil {\n\t\tlog.Fatalln(err)\n\t}\n}\n"
  },
  {
    "path": "cfg/common_settings.go",
    "content": "package cfg\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/olebedev/config\"\n\t\"golang.org/x/text/language\"\n\t\"golang.org/x/text/message\"\n)\n\nconst (\n\tdefaultLanguageTag = \"en-CA\"\n)\n\ntype Module struct {\n\tName string\n\tType string\n}\n\ntype Sigils struct {\n\tCheckbox struct {\n\t\tChecked   string\n\t\tUnchecked string\n\t}\n\tPaging struct {\n\t\tNormal   string\n\t\tSelected string\n\t}\n}\n\n// Common defines a set of common configuration settings applicable to all modules\ntype Common struct {\n\tModule\n\tPositionSettings `help:\"Defines where in the grid this module's widget will be displayed.\"`\n\tSigils\n\n\tColors ColorTheme\n\tConfig *config.Config\n\n\tDocPath string\n\n\tBordered        bool          `help:\"Whether or not the module should be displayed with a border.\" values:\"true, false\" optional:\"true\" default:\"true\"`\n\tEnabled         bool          `help:\"Whether or not this module is executed and if its data displayed onscreen.\" values:\"true, false\" optional:\"true\" default:\"false\"`\n\tFocusable       bool          `help:\"Whether or  not this module is focusable.\" values:\"true, false\" optional:\"true\" default:\"false\"`\n\tLanguageTag     string        `help:\"The BCP 47 language tag to localize text to.\" values:\"Any supported BCP 47 language tag.\" optional:\"true\" default:\"en-CA\"`\n\tRefreshInterval time.Duration `help:\"How often this module will update its data.\" values:\"A positive integer followed by a time unit (ns, us, ms, s, m, h, or nothing which defaults to s)\" optional:\"true\"`\n\tTitle           string        `help:\"The title string to show when displaying this module\" optional:\"true\"`\n\n\tfocusChar int `help:\"Define one of the number keys as a short cut key to access the widget.\" optional:\"true\"`\n}\n\n// NewCommonSettingsFromModule returns a common settings configuration tailed to the given module\nfunc NewCommonSettingsFromModule(name, defaultTitle string, defaultFocusable bool, moduleConfig *config.Config, globalConfig *config.Config) *Common {\n\tbaseColors := NewDefaultColorTheme()\n\n\tcolorsConfig, err := globalConfig.Get(\"wtf.colors\")\n\tif err != nil && strings.Contains(err.Error(), \"Nonexistent map\") {\n\t\t// Create a default colors config to fill in for the missing one\n\t\t// This comes into play when the configuration file does not contain a `colors:` key, i.e:\n\t\t//\n\t\t//     wtf:\n\t\t//       # colors:                <- missing\n\t\t//       refreshInterval: 1\n\t\t//       openFileUtil: \"open\"\n\t\t//\n\t\tcolorsConfig, _ = NewDefaultColorConfig()\n\t}\n\n\t// And finally create a third instance to be the final default fallback in case there are empty or nil values in\n\t// the colors extracted from the config file (aka colorsConfig)\n\tdefaultColorTheme := NewDefaultColorTheme()\n\n\tbaseColors.Focusable = moduleConfig.UString(\"colors.border.focusable\", colorsConfig.UString(\"border.focusable\", defaultColorTheme.Focusable))\n\tbaseColors.Focused = moduleConfig.UString(\"colors.border.focused\", colorsConfig.UString(\"border.focused\", defaultColorTheme.Focused))\n\tbaseColors.Unfocusable = moduleConfig.UString(\"colors.border.normal\", colorsConfig.UString(\"border.normal\", defaultColorTheme.Unfocusable))\n\n\tbaseColors.Checked = moduleConfig.UString(\"colors.checked\", colorsConfig.UString(\"checked\", defaultColorTheme.Checked))\n\n\tbaseColors.EvenForeground = moduleConfig.UString(\"colors.rows.even\", colorsConfig.UString(\"rows.even\", defaultColorTheme.EvenForeground))\n\tbaseColors.OddForeground = moduleConfig.UString(\"colors.rows.odd\", colorsConfig.UString(\"rows.odd\", defaultColorTheme.OddForeground))\n\n\tbaseColors.Label = moduleConfig.UString(\"colors.label\", colorsConfig.UString(\"label\", defaultColorTheme.Label))\n\tbaseColors.Subheading = moduleConfig.UString(\"colors.subheading\", colorsConfig.UString(\"subheading\", defaultColorTheme.Subheading))\n\tbaseColors.Text = moduleConfig.UString(\"colors.text\", colorsConfig.UString(\"text\", defaultColorTheme.Text))\n\tbaseColors.Title = moduleConfig.UString(\"colors.title\", colorsConfig.UString(\"title\", defaultColorTheme.Title))\n\n\tbaseColors.Background = moduleConfig.UString(\"colors.background\", colorsConfig.UString(\"background\", defaultColorTheme.Background))\n\n\tcommon := Common{\n\t\tColors: baseColors,\n\n\t\tModule: Module{\n\t\t\tName: name,\n\t\t\tType: moduleConfig.UString(\"type\", name),\n\t\t},\n\n\t\tPositionSettings: NewPositionSettingsFromYAML(moduleConfig),\n\n\t\tBordered:        moduleConfig.UBool(\"border\", true),\n\t\tConfig:          moduleConfig,\n\t\tEnabled:         moduleConfig.UBool(\"enabled\", false),\n\t\tFocusable:       moduleConfig.UBool(\"focusable\", defaultFocusable),\n\t\tLanguageTag:     globalConfig.UString(\"wtf.language\", defaultLanguageTag),\n\t\tRefreshInterval: ParseTimeString(moduleConfig, \"refreshInterval\", \"300s\"),\n\t\tTitle:           moduleConfig.UString(\"title\", defaultTitle),\n\n\t\tfocusChar: moduleConfig.UInt(\"focusChar\", -1),\n\t}\n\n\tsigilsPath := \"wtf.sigils\"\n\tcommon.Checkbox.Checked = globalConfig.UString(sigilsPath+\".checkbox.checked\", \"x\")\n\tcommon.Checkbox.Unchecked = globalConfig.UString(sigilsPath+\".checkbox.unchecked\", \" \")\n\tcommon.Paging.Normal = globalConfig.UString(sigilsPath+\".paging.normal\", globalConfig.UString(\"wtf.paging.pageSigil\", \"*\"))\n\tcommon.Paging.Selected = globalConfig.UString(sigilsPath+\".paging.select\", globalConfig.UString(\"wtf.paging.selectedSigil\", \"_\"))\n\n\treturn &common\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (common *Common) DefaultFocusedRowColor() string {\n\treturn fmt.Sprintf(\n\t\t\"%s:%s\",\n\t\tcommon.Colors.HighlightedForeground,\n\t\tcommon.Colors.HighlightedBackground,\n\t)\n}\n\nfunc (common *Common) DefaultRowColor() string {\n\treturn fmt.Sprintf(\n\t\t\"%s:%s\",\n\t\tcommon.Colors.EvenForeground,\n\t\tcommon.Colors.EvenBackground,\n\t)\n}\n\n// FocusChar returns the keyboard number assigned to the widget used to give onscreen\n// focus to this widget, as a string. Focus characters can be a range between 1 and 9\nfunc (common *Common) FocusChar() string {\n\tif common.focusChar <= 0 {\n\t\treturn \"\"\n\t}\n\n\tif common.focusChar > 9 {\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprint(common.focusChar)\n}\n\n// LocalizedPrinter returns a message.Printer instance localized to the BCP 47 language\n// configuration value defined in 'wtf.language' config. If none exists, it defaults to\n// 'en-CA'. Use this to format numbers, etc.\nfunc (common *Common) LocalizedPrinter() (*message.Printer, error) {\n\tlangTag, err := language.Parse(common.LanguageTag)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprntr := message.NewPrinter(langTag)\n\n\treturn prntr, nil\n}\n\nfunc (common *Common) RowColor(idx int) string {\n\tif idx%2 == 0 {\n\t\treturn fmt.Sprintf(\n\t\t\t\"%s:%s\",\n\t\t\tcommon.Colors.EvenForeground,\n\t\t\tcommon.Colors.EvenBackground,\n\t\t)\n\t}\n\treturn fmt.Sprintf(\n\t\t\"%s:%s\",\n\t\tcommon.Colors.OddForeground,\n\t\tcommon.Colors.OddBackground,\n\t)\n}\n\nfunc (*Common) RightAlignFormat(width int) string {\n\tborderOffset := 2\n\treturn fmt.Sprintf(\"%%%ds\", width-borderOffset)\n}\n\n// PaginationMarker generates the pagination indicators that appear in the top-right corner\n// of multisource widgets\nfunc (common *Common) PaginationMarker(length, pos, width int) string {\n\tsigils := \"\"\n\n\tif length > 1 {\n\t\tsigils = strings.Repeat(common.Paging.Normal, pos)\n\t\tsigils += common.Paging.Selected\n\t\tsigils += strings.Repeat(common.Paging.Normal, length-1-pos)\n\n\t\tsigils = \"[lightblue]\" + fmt.Sprintf(common.RightAlignFormat(width), sigils) + \"[white]\"\n\t}\n\n\treturn sigils\n}\n\n// SetDocumentationPath is used to explicitly set the documentation path that should be opened\n// when the key to open the documentation is pressed.\n// Setting this is probably not necessary unless the module documentation is nested inside a\n// documentation subdirectory in the /wtfutildocs repo, or the module here has a different\n// name than the module's display name in the documentation (which ideally wouldn't be a thing).\nfunc (common *Common) SetDocumentationPath(path string) {\n\tcommon.DocPath = path\n}\n\n// Validations aggregates all the validations from all the sub-sections in Common into a\n// single array of validations\nfunc (common *Common) Validations() []Validatable {\n\tvar validatables []Validatable\n\n\tfor _, validation := range common.PositionSettings.Validations.validations {\n\t\tvalidatables = append(validatables, validation)\n\t}\n\n\treturn validatables\n}\n"
  },
  {
    "path": "cfg/common_settings_test.go",
    "content": "package cfg\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar (\n\ttestYaml = `\nwtf:\n  colors:\n`\n\n\tmoduleConfig, _   = config.ParseYaml(testYaml)\n\tglobalSettings, _ = config.ParseYaml(testYaml)\n\n\ttestCfg = NewCommonSettingsFromModule(\n\t\t\"test\",\n\t\t\"Test Config\",\n\t\ttrue,\n\t\tmoduleConfig,\n\t\tglobalSettings,\n\t)\n)\n\nfunc Test_NewCommonSettingsFromModule(t *testing.T) {\n\tassert.Equal(t, true, testCfg.Bordered)\n\tassert.Equal(t, false, testCfg.Enabled)\n\tassert.Equal(t, true, testCfg.Focusable)\n\tassert.Equal(t, \"test\", testCfg.Name)\n\tassert.Equal(t, \"test\", testCfg.Type)\n\tassert.Equal(t, \"\", testCfg.FocusChar())\n\tassert.Equal(t, 300*time.Second, testCfg.RefreshInterval)\n\tassert.Equal(t, \"Test Config\", testCfg.Title)\n}\n\nfunc Test_DefaultFocusedRowColor(t *testing.T) {\n\tassert.Equal(t, \"black:green\", testCfg.DefaultFocusedRowColor())\n}\n\nfunc Test_DefaultRowColor(t *testing.T) {\n\tassert.Equal(t, \"white:transparent\", testCfg.DefaultRowColor())\n}\n\nfunc Test_FocusChar(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tbefore       func(testCfg *Common)\n\t\texpectedChar string\n\t}{\n\t\t{\n\t\t\tname: \"with negative focus char\",\n\t\t\tbefore: func(testCfg *Common) {\n\t\t\t\ttestCfg.focusChar = -1\n\t\t\t},\n\t\t\texpectedChar: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"with positive focus char\",\n\t\t\tbefore: func(testCfg *Common) {\n\t\t\t\ttestCfg.focusChar = 3\n\t\t\t},\n\t\t\texpectedChar: \"3\",\n\t\t},\n\t\t{\n\t\t\tname: \"with zero focus char\",\n\t\t\tbefore: func(testCfg *Common) {\n\t\t\t\ttestCfg.focusChar = 0\n\t\t\t},\n\t\t\texpectedChar: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"with large focus char\",\n\t\t\tbefore: func(testCfg *Common) {\n\t\t\t\ttestCfg.focusChar = 10\n\t\t\t},\n\t\t\texpectedChar: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.before(testCfg)\n\n\t\t\tassert.Equal(t, tt.expectedChar, testCfg.FocusChar())\n\t\t})\n\t}\n}\n\nfunc Test_RowColor(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tidx           int\n\t\texpectedColor string\n\t}{\n\t\t{\n\t\t\tname:          \"odd rows, default\",\n\t\t\tidx:           3,\n\t\t\texpectedColor: \"lightblue:transparent\",\n\t\t},\n\t\t{\n\t\t\tname:          \"even rows, default\",\n\t\t\tidx:           8,\n\t\t\texpectedColor: \"white:transparent\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expectedColor, testCfg.RowColor(tt.idx))\n\t\t})\n\t}\n}\n\nfunc Test_RightAlignFormat(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\twidth    int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"with zero\",\n\t\t\twidth:    0,\n\t\t\texpected: \"%-2s\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with positive integer\",\n\t\t\twidth:    3,\n\t\t\texpected: \"%1s\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with negative integer\",\n\t\t\twidth:    -3,\n\t\t\texpected: \"%-5s\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, testCfg.RightAlignFormat(tt.width))\n\t\t})\n\t}\n}\n\nfunc Test_PaginationMarker(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tlen      int\n\t\tpos      int\n\t\twidth    int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"with zero pages\",\n\t\t\tlen:      0,\n\t\t\tpos:      1,\n\t\t\twidth:    5,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with one page\",\n\t\t\tlen:      1,\n\t\t\tpos:      1,\n\t\t\twidth:    5,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with multiple pages\",\n\t\t\tlen:      3,\n\t\t\tpos:      1,\n\t\t\twidth:    5,\n\t\t\texpected: \"[lightblue]*_*[white]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with negative pages\",\n\t\t\tlen:      -3,\n\t\t\tpos:      1,\n\t\t\twidth:    5,\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, testCfg.PaginationMarker(tt.len, tt.pos, tt.width))\n\t\t})\n\t}\n}\n\nfunc Test_Validations(t *testing.T) {\n\tassert.Equal(t, 4, len(testCfg.Validations()))\n}\n"
  },
  {
    "path": "cfg/config_files.go",
    "content": "package cfg\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\n\t\"github.com/olebedev/config\"\n)\n\nconst (\n\t// XdgConfigDir defines the path to the minimal XDG-compatible configuration directory\n\tXdgConfigDir = \"~/.config/\"\n\n\t// WtfConfigDirV1 defines the path to the first version of configuration. Do not use this\n\tWtfConfigDirV1 = \"~/.wtf/\"\n\n\t// WtfConfigDirV2 defines the path to the second version of the configuration. Use this.\n\tWtfConfigDirV2 = \"~/.config/wtf/\"\n\n\t// WtfConfigFile defines the name of the default config file\n\tWtfConfigFile = \"config.yml\"\n)\n\n/* -------------------- Exported Functions -------------------- */\n\n// CreateFile creates the named file in the config directory, if it does not already exist.\n// If the file exists it does not recreate it.\n// If successful, returns the absolute path to the file\n// If unsuccessful, returns an error\nfunc CreateFile(fileName string) (string, error) {\n\tconfigDir, err := WtfConfigDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfilePath := filepath.Join(configDir, fileName)\n\n\t// Check if the file already exists; if it does not, create it\n\t_, err = os.Stat(filePath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\t_, err = os.Create(filePath)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t} else {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\treturn filePath, nil\n}\n\n// Initialize takes care of settings up the initial state of WTF configuration\n// It ensures necessary directories and files exist\nfunc Initialize(hasCustom bool) {\n\tif !hasCustom {\n\t\tmigrateOldConfig()\n\t}\n\n\t// These always get created because this is where modules should write any permanent\n\t// data they need to persist between runs (i.e.: log, textfile, etc.)\n\tcreateWtfConfigDir()\n\n\tif !hasCustom {\n\t\tcreateWtfConfigFile()\n\t\tchmodConfigFile()\n\t}\n}\n\n// WtfConfigDir returns the absolute path to the configuration directory\nfunc WtfConfigDir() (string, error) {\n\tconfigDir := os.Getenv(\"XDG_CONFIG_HOME\")\n\tif configDir == \"\" {\n\t\tconfigDir = WtfConfigDirV2\n\t} else {\n\t\tconfigDir += \"/wtf/\"\n\t}\n\tconfigDir, err := expandHomeDir(configDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn configDir, nil\n}\n\n// LoadWtfConfigFile loads the specified config file\nfunc LoadWtfConfigFile(filePath string) *config.Config {\n\tabsPath, _ := expandHomeDir(filePath)\n\n\tcfg, err := config.ParseYamlFile(absPath)\n\tif err != nil {\n\t\tdisplayWtfConfigFileLoadError(absPath, err)\n\t\tos.Exit(1)\n\t}\n\n\treturn cfg\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// chmodConfigFile sets the mode of the config file to r+w for the owner only\nfunc chmodConfigFile() {\n\tconfigDir, _ := WtfConfigDir()\n\trelPath := filepath.Join(configDir, WtfConfigFile)\n\tabsPath, _ := expandHomeDir(relPath)\n\n\t_, err := os.Stat(absPath)\n\tif err != nil && os.IsNotExist(err) {\n\t\treturn\n\t}\n\n\terr = os.Chmod(absPath, 0600)\n\tif err != nil {\n\t\treturn\n\t}\n}\n\n// createWtfConfigDir creates the necessary directories for storing the default config file\n// If ~/.config/wtf is missing, it will try to create it\nfunc createWtfConfigDir() {\n\twtfConfigDir, _ := WtfConfigDir()\n\n\tif _, err := os.Stat(wtfConfigDir); os.IsNotExist(err) {\n\t\terr := os.MkdirAll(wtfConfigDir, os.ModePerm)\n\t\tif err != nil {\n\t\t\tdisplayWtfConfigDirCreateError(err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n}\n\n// createWtfConfigFile creates a simple config file in the config directory if\n// one does not already exist\nfunc createWtfConfigFile() {\n\tfilePath, err := CreateFile(WtfConfigFile)\n\tif err != nil {\n\t\tdisplayDefaultConfigCreateError(err)\n\t\tos.Exit(1)\n\t}\n\n\t// If the file is empty, write to it\n\tfile, _ := os.Stat(filePath)\n\n\tif file.Size() == 0 {\n\t\tif os.WriteFile(filePath, []byte(defaultConfigFile), 0600) != nil {\n\t\t\tdisplayDefaultConfigWriteError(err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n}\n\n// Expand expands the path to include the home directory if the path\n// is prefixed with `~`. If it isn't prefixed with `~`, the path is\n// returned as-is.\nfunc expandHomeDir(path string) (string, error) {\n\tif path == \"\" {\n\t\treturn path, nil\n\t}\n\n\tif path[0] != '~' {\n\t\treturn path, nil\n\t}\n\n\tif len(path) > 1 && path[1] != '/' && path[1] != '\\\\' {\n\t\treturn \"\", errors.New(\"cannot expand user-specific home dir\")\n\t}\n\n\tdir, err := home()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn filepath.Join(dir, path[1:]), nil\n}\n\n// Dir returns the home directory for the executing user.\n// An error is returned if a home directory cannot be detected.\nfunc home() (string, error) {\n\tcurrentUser, err := user.Current()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif currentUser.HomeDir == \"\" {\n\t\treturn \"\", errors.New(\"cannot find user-specific home dir\")\n\t}\n\n\treturn currentUser.HomeDir, nil\n}\n\n// migrateOldConfig copies any existing configuration from the old location\n// to the new, XDG-compatible location\nfunc migrateOldConfig() {\n\tsrcDir, _ := expandHomeDir(WtfConfigDirV1)\n\tdestDir, _ := WtfConfigDir()\n\n\t// If the old config directory doesn't exist, do not move\n\tif _, err := os.Stat(srcDir); os.IsNotExist(err) {\n\t\treturn\n\t}\n\n\t// If the new config directory already exists, do not move\n\tif _, err := os.Stat(destDir); err == nil {\n\t\treturn\n\t}\n\n\t// Time to move\n\terr := Copy(srcDir, destDir)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Delete the old directory if the new one exists\n\tif _, err := os.Stat(destDir); err == nil {\n\t\terr := os.RemoveAll(srcDir)\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cfg/copy.go",
    "content": "// Copied verbatim from:\n//\n//   https://github.com/otiai10/copy/blob/master/copy.go\n\npackage cfg\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// Copy copies src to dest, doesn't matter if src is a directory or a file\nfunc Copy(src, dest string) error {\n\tinfo, err := os.Stat(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn locationCopy(src, dest, info)\n}\n\n// \"info\" must be given here, NOT nil.\nfunc locationCopy(src, dest string, info os.FileInfo) error {\n\tif info.IsDir() {\n\t\treturn directoryCopy(src, dest, info)\n\t}\n\treturn fileCopy(src, dest, info)\n}\n\nfunc fileCopy(src, dest string, info os.FileInfo) error {\n\n\tf, err := os.Create(dest)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = f.Close() }()\n\n\tif err = os.Chmod(f.Name(), info.Mode()); err != nil {\n\t\treturn err\n\t}\n\n\ts, err := os.Open(filepath.Clean(src))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = f.Close() }()\n\n\t_, err = io.Copy(f, s)\n\treturn err\n}\n\nfunc directoryCopy(src, dest string, info os.FileInfo) error {\n\n\tif err := os.MkdirAll(dest, info.Mode()); err != nil {\n\t\treturn err\n\t}\n\n\tentries, err := os.ReadDir(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, entry := range entries {\n\t\tinfo, err := entry.Info()\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := locationCopy(\n\t\t\tfilepath.Join(src, info.Name()),\n\t\t\tfilepath.Join(dest, info.Name()),\n\t\t\tinfo,\n\t\t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cfg/default_color_theme.go",
    "content": "package cfg\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"gopkg.in/yaml.v2\"\n)\n\n// BorderTheme defines the default color scheme for drawing widget borders\ntype BorderTheme struct {\n\tFocusable   string\n\tFocused     string\n\tUnfocusable string\n}\n\n// CheckboxTheme defines the default color scheme for drawing checkable rows in widgets\ntype CheckboxTheme struct {\n\tChecked string\n}\n\n// RowTheme defines the default color scheme for row text\ntype RowTheme struct {\n\tEvenBackground string\n\tEvenForeground string\n\n\tOddBackground string\n\tOddForeground string\n\n\tHighlightedBackground string\n\tHighlightedForeground string\n}\n\n// TextTheme defines the default color scheme for text rendering\ntype TextTheme struct {\n\tLabel      string\n\tSubheading string\n\tText       string\n\tTitle      string\n}\n\n// WidgetTheme defines the default color scheme for the widget rect itself\ntype WidgetTheme struct {\n\tBackground string\n}\n\n// ColorTheme is an alamgam of all the default color settings\ntype ColorTheme struct {\n\tBorderTheme\n\tCheckboxTheme\n\tRowTheme\n\tTextTheme\n\tWidgetTheme\n}\n\n// NewDefaultColorTheme creates and returns an instance of DefaultColorTheme\nfunc NewDefaultColorTheme() ColorTheme {\n\tdefaultTheme := ColorTheme{\n\t\tBorderTheme: BorderTheme{\n\t\t\tFocusable:   \"blue\",\n\t\t\tFocused:     \"orange\",\n\t\t\tUnfocusable: \"gray\",\n\t\t},\n\n\t\tCheckboxTheme: CheckboxTheme{\n\t\t\tChecked: \"gray\",\n\t\t},\n\n\t\tRowTheme: RowTheme{\n\t\t\tEvenBackground: \"transparent\",\n\t\t\tEvenForeground: \"white\",\n\n\t\t\tOddBackground: \"transparent\",\n\t\t\tOddForeground: \"lightblue\",\n\n\t\t\tHighlightedForeground: \"black\",\n\t\t\tHighlightedBackground: \"green\",\n\t\t},\n\n\t\tTextTheme: TextTheme{\n\t\t\tLabel:      \"lightblue\",\n\t\t\tSubheading: \"red\",\n\t\t\tText:       \"white\",\n\t\t\tTitle:      \"green\",\n\t\t},\n\n\t\tWidgetTheme: WidgetTheme{\n\t\t\tBackground: \"transparent\",\n\t\t},\n\t}\n\n\treturn defaultTheme\n}\n\n// NewDefaultColorConfig creates and returns a config.Config-compatible configuration struct\n// using a DefaultColorTheme to pre-populate all the relevant values\nfunc NewDefaultColorConfig() (*config.Config, error) {\n\tcolorTheme := NewDefaultColorTheme()\n\n\tyamlBytes, err := yaml.Marshal(colorTheme)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcfg, err := config.ParseYamlBytes(yamlBytes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cfg, nil\n}\n"
  },
  {
    "path": "cfg/default_color_theme_test.go",
    "content": "package cfg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_NewDefaultColorTheme(t *testing.T) {\n\ttheme := NewDefaultColorTheme()\n\n\tassert.Equal(t, \"orange\", theme.Focused)\n\tassert.Equal(t, \"red\", theme.Subheading)\n\tassert.Equal(t, \"transparent\", theme.Background)\n}\n\nfunc Test_NewDefaultColorConfig(t *testing.T) {\n\tcfg, err := NewDefaultColorConfig()\n\n\tassert.Nil(t, err)\n\n\tassert.Equal(t, \"orange\", cfg.UString(\"bordertheme.focused\"))\n\tassert.Equal(t, \"red\", cfg.UString(\"texttheme.subheading\"))\n\tassert.Equal(t, \"transparent\", cfg.UString(\"widgettheme.background\"))\n\tassert.Equal(t, \"\", cfg.UString(\"widgettheme.missing\"))\n}\n"
  },
  {
    "path": "cfg/default_config_file.go",
    "content": "package cfg\n\nconst defaultConfigFile = `wtf:\n  colors:\n    border:\n      focusable: darkslateblue\n      focused: orange\n      normal: gray\n  grid:\n    columns: [32, 32, 32, 32, 90]\n    rows: [10, 10, 10, 4, 4, 90]\n  refreshInterval: 1\n  mods:\n    clocks_a:\n      colors:\n        rows:\n          even: \"lightblue\"\n          odd: \"white\"\n      enabled: true\n      locations:\n        Vancouver: \"America/Vancouver\"\n        Toronto: \"America/Toronto\"\n      position:\n        top: 0\n        left: 1\n        height: 1\n        width: 1\n      refreshInterval: 15\n      sort: \"alphabetical\"\n      title: \"Clocks A\"\n      type: \"clocks\"\n    clocks_b:\n      colors:\n        rows:\n          even: \"lightblue\"\n          odd: \"white\"\n      enabled: true\n      locations:\n        Paris: \"Europe/Paris\"\n        Barcelona: \"Europe/Madrid\"\n        Dubai: \"Asia/Dubai\"\n      position:\n        top: 0\n        left: 2\n        height: 1\n        width: 1\n      refreshInterval: 15\n      sort: \"alphabetical\"\n      title: \"Clocks B\"\n      type: \"clocks\"\n    feedreader:\n      enabled: true\n      feeds:\n      - https://feeds.bbci.co.uk/news/rss.xml\n      feedLimit: 10\n      position:\n        top: 1\n        left: 1\n        width: 2\n        height: 1\n      refreshInterval: 14400\n    ipinfo:\n      colors:\n        name: \"lightblue\"\n        value: \"white\"\n      enabled: true\n      position:\n        top: 2\n        left: 1\n        height: 1\n        width: 1\n      refreshInterval: 150\n    power:\n      enabled: true\n      position:\n        top: 2\n        left: 2\n        height: 1\n        width: 1\n      refreshInterval: 15\n      title: \"⚡️\"\n    textfile:\n      enabled: true\n      filePath: \"~/.config/wtf/config.yml\"\n      format: true\n      position:\n        top: 0\n        left: 0\n        height: 4\n        width: 1\n      refreshInterval: 30\n      wrapText: false\n    uptime:\n      args: []\n      cmd: \"uptime\"\n      enabled: true\n      position:\n        top: 3\n        left: 1\n        height: 1\n        width: 2\n      refreshInterval: 30\n      type: cmdrunner\n`\n"
  },
  {
    "path": "cfg/error_messages.go",
    "content": "package cfg\n\n// This file contains the error messages that get written to the terminal when\n// something goes wrong with the configuration process.\n//\n// As a general rule, if one of these has to be shown the app should then die\n// via os.Exit(1)\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/logrusorgru/aurora/v4\"\n)\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc displayError(err error) {\n\tfmt.Printf(\"%s %s\\n\\n\", aurora.Red(\"Error:\"), err.Error())\n}\n\nfunc displayDefaultConfigCreateError(err error) {\n\tfmt.Printf(\"\\n%s Could not create the default configuration file.\\n\", aurora.Red(\"ERROR\"))\n\tfmt.Println()\n\tdisplayError(err)\n}\n\nfunc displayDefaultConfigWriteError(err error) {\n\tfmt.Printf(\"\\n%s Could not write the default configuration file.\\n\", aurora.Red(\"ERROR\"))\n\tfmt.Println()\n\tdisplayError(err)\n}\n\nfunc displayWtfConfigDirCreateError(err error) {\n\tfmt.Printf(\"\\n%s Could not create the '%s' directory.\\n\", aurora.Red(\"ERROR\"), aurora.Yellow(WtfConfigDirV2))\n\tfmt.Println()\n\tdisplayError(err)\n}\n\nfunc displayWtfConfigFileLoadError(path string, err error) {\n\tfmt.Printf(\"\\n%s Could not load '%s'.\\n\", aurora.Red(\"ERROR\"), aurora.Yellow(path))\n\tfmt.Println()\n\tfmt.Println(\"This could mean one of two things:\")\n\tfmt.Println()\n\tfmt.Println(\"    1. That file doesn't exist.\")\n\tfmt.Println(\"    2. That file has a YAML syntax error. Try running it through http://www.yamllint.com to check for errors.\")\n\tfmt.Println()\n\tdisplayError(err)\n}\n"
  },
  {
    "path": "cfg/parsers.go",
    "content": "package cfg\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/olebedev/config\"\n)\n\n// ParseAsMapOrList takes a configuration key and attempts to parse it first as a map\n// and then as a list. Map entries are concatenated as \"key/value\"\nfunc ParseAsMapOrList(ymlConfig *config.Config, configKey string) []string {\n\tresult := []string{}\n\n\tmapItems, err := ymlConfig.Map(configKey)\n\tif err == nil {\n\t\tfor key, value := range mapItems {\n\t\t\tresult = append(result, fmt.Sprintf(\"%s/%s\", value, key))\n\t\t}\n\t\treturn result\n\t}\n\n\tlistItems := ymlConfig.UList(configKey)\n\tfor _, listItem := range listItems {\n\t\tresult = append(result, listItem.(string))\n\t}\n\n\treturn result\n}\n\n// ParseTimeString takes a configuration key and attempts to parse it first as an int\n// and then as a duration (int + time unit)\nfunc ParseTimeString(cfg *config.Config, configKey string, defaultValue string) time.Duration {\n\ti, err := cfg.Int(configKey)\n\tif err == nil {\n\t\treturn time.Duration(i) * time.Second\n\t}\n\n\tstr := cfg.UString(configKey, defaultValue)\n\td, err := time.ParseDuration(str)\n\tif err == nil {\n\t\treturn d\n\t}\n\n\treturn time.Second\n}\n"
  },
  {
    "path": "cfg/parsers_test.go",
    "content": "package cfg\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/olebedev/config\"\n)\n\nfunc Test_ParseAsMapOrList(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tconfigKey     string\n\t\tyaml          string\n\t\texpectedCount int\n\t}{\n\t\t{\n\t\t\tname:          \"as empty set\",\n\t\t\tconfigKey:     \"data\",\n\t\t\tyaml:          \"\",\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:          \"as map\",\n\t\t\tconfigKey:     \"data\",\n\t\t\tyaml:          \"data:\\n  a: cat\\n  b: dog\",\n\t\t\texpectedCount: 2,\n\t\t},\n\t\t{\n\t\t\tname:          \"as list\",\n\t\t\tconfigKey:     \"data\",\n\t\t\tyaml:          \"data:\\n  - cat\\n  - dog\\n  - rat\\n\",\n\t\t\texpectedCount: 3,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tymlConfig, err := config.ParseYaml(tt.yaml)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"\\nexpected: no error\\n     got: %v\", err)\n\t\t\t}\n\n\t\t\tactual := ParseAsMapOrList(ymlConfig, tt.configKey)\n\n\t\t\tif tt.expectedCount != len(actual) {\n\t\t\t\tt.Errorf(\"\\nexpected: %d\\n     got: %d\", tt.expectedCount, len(actual))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_ParseTimeString(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tconfigKey     string\n\t\tyaml          string\n\t\texpectedCount time.Duration\n\t}{\n\t\t{\n\t\t\tname:          \"normal integer\",\n\t\t\tconfigKey:     \"refreshInterval\",\n\t\t\tyaml:          \"refreshInterval: 3\",\n\t\t\texpectedCount: 3 * time.Second,\n\t\t},\n\t\t{\n\t\t\tname:          \"microseconds\",\n\t\t\tconfigKey:     \"refreshInterval\",\n\t\t\tyaml:          \"refreshInterval: 5µs\",\n\t\t\texpectedCount: 5 * time.Microsecond,\n\t\t},\n\t\t{\n\t\t\tname:          \"microseconds different notation\",\n\t\t\tconfigKey:     \"refreshInterval\",\n\t\t\tyaml:          \"refreshInterval: 5us\",\n\t\t\texpectedCount: 5 * time.Microsecond,\n\t\t},\n\t\t{\n\t\t\tname:          \"mixed duration\",\n\t\t\tconfigKey:     \"refreshInterval\",\n\t\t\tyaml:          \"refreshInterval: 2h45m\",\n\t\t\texpectedCount: 2*time.Hour + 45*time.Minute,\n\t\t},\n\t\t{\n\t\t\tname:          \"default\",\n\t\t\tconfigKey:     \"refreshInterval\",\n\t\t\tyaml:          \"\",\n\t\t\texpectedCount: 60 * time.Second,\n\t\t},\n\t\t{\n\t\t\tname:          \"bad input\",\n\t\t\tconfigKey:     \"refreshInterval\",\n\t\t\tyaml:          \"refreshInterval: abc\",\n\t\t\texpectedCount: 1 * time.Second,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tymlConfig, err := config.ParseYaml(tt.yaml)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"\\nexpected: no error\\n     got: %v\", err)\n\t\t\t}\n\n\t\t\tactual := ParseTimeString(ymlConfig, tt.configKey, \"60s\")\n\n\t\t\tif tt.expectedCount != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %d\\n     got: %v\", tt.expectedCount, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cfg/position_settings.go",
    "content": "package cfg\n\nimport (\n\t\"github.com/olebedev/config\"\n)\n\nconst (\n\tpositionPath = \"position\"\n)\n\n// PositionSettings represents the onscreen location of a widget\ntype PositionSettings struct {\n\tValidations *Validations\n\n\tHeight int\n\tLeft   int\n\tTop    int\n\tWidth  int\n}\n\n// NewPositionSettingsFromYAML creates and returns a new instance of cfg.Position\nfunc NewPositionSettingsFromYAML(moduleConfig *config.Config) PositionSettings {\n\tvar currVal int\n\tvar err error\n\n\tvalidations := NewValidations()\n\n\t// Parse the positional data from the config data\n\tcurrVal, err = moduleConfig.Int(positionPath + \".top\")\n\tvalidations.append(\"top\", newPositionValidation(\"top\", currVal, err))\n\n\tcurrVal, err = moduleConfig.Int(positionPath + \".left\")\n\tvalidations.append(\"left\", newPositionValidation(\"left\", currVal, err))\n\n\tcurrVal, err = moduleConfig.Int(positionPath + \".width\")\n\tvalidations.append(\"width\", newPositionValidation(\"width\", currVal, err))\n\n\tcurrVal, err = moduleConfig.Int(positionPath + \".height\")\n\tvalidations.append(\"height\", newPositionValidation(\"height\", currVal, err))\n\n\tpos := PositionSettings{\n\t\tValidations: validations,\n\n\t\tTop:    validations.intValueFor(\"top\"),\n\t\tLeft:   validations.intValueFor(\"left\"),\n\t\tWidth:  validations.intValueFor(\"width\"),\n\t\tHeight: validations.intValueFor(\"height\"),\n\t}\n\n\treturn pos\n}\n"
  },
  {
    "path": "cfg/position_validation.go",
    "content": "package cfg\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/logrusorgru/aurora/v4\"\n)\n\n// Common examples of invalid position configuration are:\n//\n//\tposition:\n//\t  top: -3\n//\t  left: 2\n//\t  width: 0\n//\t  height: 1\n//\n//\tposition:\n//\t  top: 3\n//\t  width: 2\n//\t  height: 1\n//\n//\tposition:\n//\t  top: 3\n//\t  # left: 2\n//\t  width: 2\n//\t  height: 1\n//\n//\tposition:\n//\ttop: 3\n//\tleft: 2\n//\twidth: 2\n//\theight: 1\ntype positionValidation struct {\n\terr    error\n\tname   string\n\tintVal int\n}\n\nfunc (posVal *positionValidation) Error() error {\n\treturn posVal.err\n}\n\nfunc (posVal *positionValidation) HasError() bool {\n\treturn posVal.err != nil\n}\n\nfunc (posVal *positionValidation) IntValue() int {\n\treturn posVal.intVal\n}\n\n// String returns the Stringer representation of the positionValidation\nfunc (posVal *positionValidation) String() string {\n\treturn fmt.Sprintf(\"Invalid value for %s:\\t%d\", aurora.Yellow(posVal.name), posVal.intVal)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc newPositionValidation(name string, intVal int, err error) *positionValidation {\n\tposVal := &positionValidation{\n\t\terr:    err,\n\t\tname:   name,\n\t\tintVal: intVal,\n\t}\n\n\treturn posVal\n}\n"
  },
  {
    "path": "cfg/position_validation_test.go",
    "content": "package cfg\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar (\n\tposVal = &positionValidation{\n\t\terr:    errors.New(\"Busted\"),\n\t\tname:   \"top\",\n\t\tintVal: -3,\n\t}\n)\n\nfunc Test_Attributes(t *testing.T) {\n\tassert.EqualError(t, posVal.Error(), \"Busted\")\n\tassert.Equal(t, true, posVal.HasError())\n\tassert.Equal(t, -3, posVal.IntValue())\n\n\tassert.Contains(t, posVal.String(), \"Invalid\")\n\tassert.Contains(t, posVal.String(), \"top\")\n\tassert.Contains(t, posVal.String(), \"-3\")\n}\n"
  },
  {
    "path": "cfg/secrets.go",
    "content": "package cfg\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime\"\n\n\t\"github.com/docker/docker-credential-helpers/client\"\n\t\"github.com/docker/docker-credential-helpers/credentials\"\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/logger\"\n)\n\ntype SecretLoadParams struct {\n\tname         string\n\tglobalConfig *config.Config\n\tservice      string\n\n\tsecret *string\n}\n\n// Load module secrets.\n//\n// The credential helpers impose this structure:\n//\n//\tSERVICE is mapped to a SECRET and USERNAME\n//\n// Only SECRET is secret, SERVICE and USERNAME are not, so this\n// API doesn't expose USERNAME.\n//\n// SERVICE was intended to be the URL of an API server, but\n// for hosted services that do not have or need a configurable\n// API server, its easier to just use the module name as the\n// SERVICE:\n//\n//\t   cfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n//\n//\tThe user will use the module name as the service, and the API key as\n//\tthe secret, for example:\n//\n//\t   % wtfutil save-secret circleci\n//\t   Secret: ...\n//\n// If a module (such as pihole, jenkins, or github) might have multiple\n// instantiations each using a different API service (with its own unique\n// API key), then the module should use the API URL to lookup the secret.\n// For example, for github:\n//\n//\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).\n//\t    Service(settings.baseURL).\n//\t    Load()\n//\n// The user will use the API URL as the service, and the API key as the\n// secret, for example, with github configured as:\n//\n//\t   -- config.yml\n//\t   mods:\n//\t     github:\n//\t       baseURL: \"https://github.mycompany.com/api/v3\"\n//\t       ...\n//\n//\tthe secret must be saved as:\n//\n//\t   % wtfutil save-secret https://github.mycompany.com/api/v3\n//\t   Secret: ...\n//\n//\tIf baseURL is not set in the configuration it will be the modules\n//\tdefault, and the SERVICE will default to the module name, \"github\",\n//\tand the user must save the secret as:\n//\n//\t   % wtfutil save-secret github\n//\t   Secret: ...\n//\n//\tIdeally, the individual module documentation would describe the\n//\tSERVICE name to use to save the secret.\nfunc ModuleSecret(name string, globalConfig *config.Config, secret *string) *SecretLoadParams {\n\treturn &SecretLoadParams{\n\t\tname:         name,\n\t\tglobalConfig: globalConfig,\n\t\tsecret:       secret,\n\t\tservice:      name, // Default the service to the module name\n\t}\n}\n\nfunc (slp *SecretLoadParams) Service(service string) *SecretLoadParams {\n\tif service != \"\" {\n\t\tslp.service = service\n\t}\n\treturn slp\n}\n\nfunc (slp *SecretLoadParams) Load() {\n\tconfigureSecret(\n\t\tslp.globalConfig,\n\t\tslp.service,\n\t\tslp.secret,\n\t)\n}\n\ntype Secret struct {\n\tService  string\n\tSecret   string\n\tUsername string\n\tStore    string\n}\n\nfunc configureSecret(\n\tglobalConfig *config.Config,\n\tservice string,\n\tsecret *string,\n) {\n\tif service == \"\" {\n\t\treturn\n\t}\n\n\tif secret == nil {\n\t\treturn\n\t}\n\n\t// Don't overwrite the secret if it was configured with yaml\n\tif *secret != \"\" {\n\t\treturn\n\t}\n\n\tcred, err := FetchSecret(globalConfig, service)\n\n\tif err != nil {\n\t\tlogger.Log(fmt.Sprintf(\"Loading secret failed: %s\", err.Error()))\n\t\treturn\n\t}\n\n\tif cred == nil {\n\t\t// No secret store configued.\n\t\treturn\n\t}\n\n\tif secret != nil && *secret == \"\" {\n\t\t*secret = cred.Secret\n\t}\n}\n\n// Fetch secret for `service`. Service is customarily a URL, but can be any\n// identifier uniquely used by wtf to identify the service, such as the name\n// of the module.  nil is returned if the secretStore global property is not\n// present or the secret is not found in that store.\nfunc FetchSecret(globalConfig *config.Config, service string) (*Secret, error) {\n\tprog := newProgram(globalConfig)\n\n\tif prog == nil {\n\t\t// No secret store configured.\n\t\treturn nil, nil\n\t}\n\n\tcred, err := client.Get(prog.runner, service)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get %v from %v: %w\", service, prog.store, err)\n\t}\n\n\treturn &Secret{\n\t\tService:  cred.ServerURL,\n\t\tSecret:   cred.Secret,\n\t\tUsername: cred.Username,\n\t\tStore:    prog.store,\n\t}, nil\n}\n\nfunc StoreSecret(globalConfig *config.Config, secret *Secret) error {\n\tprog := newProgram(globalConfig)\n\n\tif prog == nil {\n\t\treturn errors.New(\"cannot store secrets: wtf.secretStore is not configured\")\n\t}\n\n\tcred := &credentials.Credentials{\n\t\tServerURL: secret.Service,\n\t\tUsername:  secret.Username,\n\t\tSecret:    secret.Secret,\n\t}\n\n\t// docker-credential requires a username, but it isn't necessary for\n\t// all services. Use a default if a username was not set.\n\tif cred.Username == \"\" {\n\t\tcred.Username = \"default\"\n\t}\n\n\terr := client.Store(prog.runner, cred)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"store %v: %w\", prog.store, err)\n\t}\n\n\treturn nil\n}\n\ntype program struct {\n\tstore  string\n\trunner client.ProgramFunc\n}\n\nfunc newProgram(globalConfig *config.Config) *program {\n\tsecretStore := globalConfig.UString(\"wtf.secretStore\", \"(none)\")\n\n\tif secretStore == \"(none)\" {\n\t\treturn nil\n\t}\n\n\tif secretStore == \"\" {\n\t\tswitch runtime.GOOS {\n\t\tcase \"windows\":\n\t\t\tsecretStore = \"winrt\"\n\t\tcase \"darwin\":\n\t\t\tsecretStore = \"osxkeychain\"\n\t\tdefault:\n\t\t\tsecretStore = \"secretservice\"\n\t\t}\n\n\t}\n\n\treturn &program{\n\t\tsecretStore,\n\t\tclient.NewShellProgramFunc(\"docker-credential-\" + secretStore),\n\t}\n}\n"
  },
  {
    "path": "cfg/validatable.go",
    "content": "package cfg\n\n// Validatable is implemented by any value that validates a configuration setting\ntype Validatable interface {\n\tError() error\n\tHasError() bool\n\tString() string\n\tIntValue() int\n}\n"
  },
  {
    "path": "cfg/validations.go",
    "content": "package cfg\n\n// Validations represent a collection of config setting validations\ntype Validations struct {\n\tvalidations map[string]Validatable\n}\n\n// NewValidations creates and returns an instance of Validations\nfunc NewValidations() *Validations {\n\tvals := &Validations{\n\t\tvalidations: make(map[string]Validatable),\n\t}\n\n\treturn vals\n}\n\nfunc (vals *Validations) append(key string, posVal Validatable) {\n\tvals.validations[key] = posVal\n}\n\nfunc (vals *Validations) intValueFor(key string) int {\n\tval := vals.validations[key]\n\tif val != nil {\n\t\treturn val.IntValue()\n\t}\n\n\treturn 0\n}\n"
  },
  {
    "path": "cfg/validations_test.go",
    "content": "package cfg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar (\n\tvals = NewValidations()\n)\n\nfunc Test_intValueFor(t *testing.T) {\n\tvals.append(\"left\", newPositionValidation(\"left\", 3, nil))\n\n\ttests := []struct {\n\t\tname     string\n\t\tkey      string\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"with valid key\",\n\t\t\tkey:      \"left\",\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tname:     \"with invalid key\",\n\t\t\tkey:      \"cat\",\n\t\t\texpected: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, vals.intValueFor(tt.key))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "checklist/checklist.go",
    "content": "package checklist\n\nimport (\n\t\"time\"\n)\n\n// Checklist is a module for creating generic checklist implementations\n// See 'Todo' for an implementation example\ntype Checklist struct {\n\tItems []*ChecklistItem\n\n\tcheckedIcon   string\n\tselected      int\n\tuncheckedIcon string\n}\n\nfunc NewChecklist(checkedIcon, uncheckedIcon string) Checklist {\n\tlist := Checklist{\n\t\tcheckedIcon:   checkedIcon,\n\t\tselected:      -1,\n\t\tuncheckedIcon: uncheckedIcon,\n\t}\n\n\treturn list\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Add creates a new checklist item and adds it to the list\n// The new one is at the start or end of the list, based on newPos\nfunc (list *Checklist) Add(checked bool, date *time.Time, tags []string, text string, newPos ...string) {\n\titem := NewChecklistItem(\n\t\tchecked,\n\t\tdate,\n\t\ttags,\n\t\ttext,\n\t\tlist.checkedIcon,\n\t\tlist.uncheckedIcon,\n\t)\n\n\tif len(newPos) == 0 || newPos[0] == \"first\" {\n\t\tlist.Items = append([]*ChecklistItem{item}, list.Items...)\n\t} else if newPos[0] == \"last\" {\n\t\tlist.Items = append(list.Items, []*ChecklistItem{item}...)\n\t}\n}\n\n// CheckedItems returns a slice of all the checked items\nfunc (list *Checklist) CheckedItems() []*ChecklistItem {\n\titems := []*ChecklistItem{}\n\n\tfor _, item := range list.Items {\n\t\tif item.Checked {\n\t\t\titems = append(items, item)\n\t\t}\n\t}\n\n\treturn items\n}\n\n// Delete removes the selected item from the checklist\nfunc (list *Checklist) Delete(selectedIndex int) {\n\tif selectedIndex >= 0 && selectedIndex < len(list.Items) {\n\t\tlist.Items = append(list.Items[:selectedIndex], list.Items[selectedIndex+1:]...)\n\t}\n}\n\n// IsSelectable returns true if the checklist has selectable items, false if it does not\nfunc (list *Checklist) IsSelectable() bool {\n\treturn list.selected >= 0 && list.selected < len(list.Items)\n}\n\n// IsUnselectable returns true if the checklist has no selectable items, false if it does\nfunc (list *Checklist) IsUnselectable() bool {\n\treturn !list.IsSelectable()\n}\n\n// LongestLine returns the length of the longest checklist item's text\nfunc (list *Checklist) LongestLine() int {\n\tmaxLen := 0\n\n\tfor _, item := range list.Items {\n\t\tif len(item.Text) > maxLen {\n\t\t\tmaxLen = len(item.Text)\n\t\t}\n\t}\n\n\treturn maxLen\n}\n\n// IndexByItem returns the index of a giving item if found, otherwise returns 0 with ok set to false\nfunc (list *Checklist) IndexByItem(selectableItem *ChecklistItem) (index int, ok bool) {\n\tfor idx, item := range list.Items {\n\t\tif item == selectableItem {\n\t\t\treturn idx, true\n\t\t}\n\t}\n\treturn 0, false\n}\n\n// UncheckedItems returns a slice of all the unchecked items\nfunc (list *Checklist) UncheckedItems() []*ChecklistItem {\n\titems := []*ChecklistItem{}\n\n\tfor _, item := range list.Items {\n\t\tif !item.Checked {\n\t\t\titems = append(items, item)\n\t\t}\n\t}\n\n\treturn items\n}\n\n// Unselect removes the current select such that no item is selected\nfunc (list *Checklist) Unselect() {\n\tlist.selected = -1\n}\n\n/* -------------------- Sort Interface -------------------- */\n\nfunc (list *Checklist) Len() int {\n\treturn len(list.Items)\n}\n\nfunc (list *Checklist) Less(i, j int) bool {\n\treturn list.Items[i].Text < list.Items[j].Text\n}\n\nfunc (list *Checklist) Swap(i, j int) {\n\tlist.Items[i], list.Items[j] = list.Items[j], list.Items[i]\n}\n"
  },
  {
    "path": "checklist/checklist_item.go",
    "content": "package checklist\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\n// ChecklistItem is a module for creating generic checklist implementations\n// See 'Todo' for an implementation example\ntype ChecklistItem struct {\n\tChecked       bool\n\tCheckedIcon   string `yaml:\"-\"`\n\tDate          *time.Time\n\tTags          []string\n\tText          string\n\tUncheckedIcon string `yaml:\"-\"`\n}\n\nfunc NewChecklistItem(checked bool, date *time.Time, tags []string, text string, checkedIcon, uncheckedIcon string) *ChecklistItem {\n\titem := &ChecklistItem{\n\t\tChecked:       checked,\n\t\tCheckedIcon:   checkedIcon,\n\t\tDate:          date,\n\t\tTags:          tags,\n\t\tText:          text,\n\t\tUncheckedIcon: uncheckedIcon,\n\t}\n\n\treturn item\n}\n\n// CheckMark returns the string used to indicate a ChecklistItem is checked or unchecked\nfunc (item *ChecklistItem) CheckMark() string {\n\titem.ensureItemIcons()\n\n\tif item.Checked {\n\t\treturn item.CheckedIcon\n\t}\n\n\treturn item.UncheckedIcon\n}\n\n// EditText returns the content of the edit todo form, so includes formatted date and tags\nfunc (item *ChecklistItem) EditText() string {\n\tdatePrefix := \"\"\n\tif item.Date != nil {\n\t\tdatePrefix = fmt.Sprintf(\"%d-%02d-%02d\", item.Date.Year(), item.Date.Month(), item.Date.Day()) + \" \"\n\t}\n\n\ttagsPrefix := item.TagString()\n\n\treturn datePrefix + tagsPrefix + item.Text\n}\n\nfunc (item *ChecklistItem) TagString() string {\n\tif len(item.Tags) == 0 {\n\t\treturn \"\"\n\t}\n\n\ts := \"\"\n\tfor _, tag := range item.Tags {\n\t\ts += \"#\" + tag + \" \"\n\t}\n\n\treturn s\n}\n\n// Toggle changes the checked state of the ChecklistItem\n// If checked, it is unchecked. If unchecked, it is checked\nfunc (item *ChecklistItem) Toggle() {\n\titem.Checked = !item.Checked\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (item *ChecklistItem) ensureItemIcons() {\n\tif item.CheckedIcon == \"\" {\n\t\titem.CheckedIcon = \"x\"\n\t}\n\n\tif item.UncheckedIcon == \"\" {\n\t\titem.UncheckedIcon = \" \"\n\t}\n}\n"
  },
  {
    "path": "checklist/checklist_item_test.go",
    "content": "package checklist\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc testChecklistItem() *ChecklistItem {\n\titem := NewChecklistItem(\n\t\tfalse,\n\t\tnil,\n\t\tmake([]string, 0),\n\t\t\"test\",\n\t\t\"\",\n\t\t\"\",\n\t)\n\treturn item\n}\n\nfunc Test_CheckMark(t *testing.T) {\n\titem := testChecklistItem()\n\tassert.Equal(t, \" \", item.CheckMark())\n\n\titem.Toggle()\n\tassert.Equal(t, \"x\", item.CheckMark())\n}\n\nfunc Test_Toggle(t *testing.T) {\n\titem := testChecklistItem()\n\tassert.Equal(t, false, item.Checked)\n\n\titem.Toggle()\n\tassert.Equal(t, true, item.Checked)\n\n\titem.Toggle()\n\tassert.Equal(t, false, item.Checked)\n}\n"
  },
  {
    "path": "checklist/checklist_test.go",
    "content": "package checklist\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_NewCheckist(t *testing.T) {\n\tcl := NewChecklist(\"o\", \"-\")\n\n\tassert.IsType(t, Checklist{}, cl)\n\tassert.Equal(t, \"o\", cl.checkedIcon)\n\tassert.Equal(t, -1, cl.selected)\n\tassert.Equal(t, \"-\", cl.uncheckedIcon)\n\tassert.Equal(t, 0, len(cl.Items))\n}\n\nfunc Test_Add(t *testing.T) {\n\tcl := NewChecklist(\"o\", \"-\")\n\tcl.Add(true, nil, make([]string, 0), \"test item\")\n\n\tassert.Equal(t, 1, len(cl.Items))\n}\n\nfunc Test_CheckedItems(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\texpectedLen int\n\t\tcheckedLen  int\n\t\tbefore      func(cl *Checklist)\n\t}{\n\t\t{\n\t\t\tname:        \"with no items\",\n\t\t\texpectedLen: 0,\n\t\t\tcheckedLen:  0,\n\t\t\tbefore:      func(cl *Checklist) {},\n\t\t},\n\t\t{\n\t\t\tname:        \"with no checked items\",\n\t\t\texpectedLen: 1,\n\t\t\tcheckedLen:  0,\n\t\t\tbefore: func(cl *Checklist) {\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"unchecked item\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"with one checked item\",\n\t\t\texpectedLen: 2,\n\t\t\tcheckedLen:  1,\n\t\t\tbefore: func(cl *Checklist) {\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"unchecked item\")\n\t\t\t\tcl.Add(true, nil, make([]string, 0), \"checked item\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"with multiple checked items\",\n\t\t\texpectedLen: 3,\n\t\t\tcheckedLen:  2,\n\t\t\tbefore: func(cl *Checklist) {\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"unchecked item\")\n\t\t\t\tcl.Add(true, nil, make([]string, 0), \"checked item 11\")\n\t\t\t\tcl.Add(true, nil, make([]string, 0), \"checked item 2\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcl := NewChecklist(\"o\", \"-\")\n\t\t\ttt.before(&cl)\n\n\t\t\tassert.Equal(t, tt.expectedLen, len(cl.Items))\n\t\t\tassert.Equal(t, tt.checkedLen, len(cl.CheckedItems()))\n\t\t})\n\t}\n}\n\nfunc Test_Delete(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tidx         int\n\t\texpectedLen int\n\t}{\n\t\t{\n\t\t\tname:        \"with valid index\",\n\t\t\tidx:         0,\n\t\t\texpectedLen: 0,\n\t\t},\n\t\t{\n\t\t\tname:        \"with invalid index\",\n\t\t\tidx:         2,\n\t\t\texpectedLen: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcl := NewChecklist(\"o\", \"-\")\n\n\t\t\tcl.Add(true, nil, make([]string, 0), \"test item\")\n\t\t\tcl.Delete(tt.idx)\n\n\t\t\tassert.Equal(t, tt.expectedLen, len(cl.Items))\n\t\t})\n\t}\n}\n\nfunc Test_IsSelectable(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tselected int\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"nothing selected\",\n\t\t\tselected: -1,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid selection\",\n\t\t\tselected: 1,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid selection\",\n\t\t\tselected: 3,\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcl := NewChecklist(\"o\", \"-\")\n\t\t\tcl.Add(true, nil, make([]string, 0), \"test item 1\")\n\t\t\tcl.Add(false, nil, make([]string, 0), \"test item 2\")\n\n\t\t\tcl.selected = tt.selected\n\n\t\t\tassert.Equal(t, tt.expected, cl.IsSelectable())\n\t\t})\n\t}\n}\n\nfunc Test_IsUnselectable(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tselected int\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"nothing selected\",\n\t\t\tselected: -1,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid selection\",\n\t\t\tselected: 1,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid selection\",\n\t\t\tselected: 3,\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcl := NewChecklist(\"o\", \"-\")\n\t\t\tcl.Add(true, nil, make([]string, 0), \"test item 1\")\n\t\t\tcl.Add(false, nil, make([]string, 0), \"test item 2\")\n\n\t\t\tcl.selected = tt.selected\n\n\t\t\tassert.Equal(t, tt.expected, cl.IsUnselectable())\n\t\t})\n\t}\n}\n\nfunc Test_LongestLine(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\texpectedLen int\n\t\tbefore      func(cl *Checklist)\n\t}{\n\t\t{\n\t\t\tname:        \"with no items\",\n\t\t\texpectedLen: 0,\n\t\t\tbefore:      func(cl *Checklist) {},\n\t\t},\n\t\t{\n\t\t\tname:        \"with different-length items\",\n\t\t\texpectedLen: 12,\n\t\t\tbefore: func(cl *Checklist) {\n\t\t\t\tcl.Add(true, nil, make([]string, 0), \"test item 1\")\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"test item 22\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"with same-length items\",\n\t\t\texpectedLen: 11,\n\t\t\tbefore: func(cl *Checklist) {\n\t\t\t\tcl.Add(true, nil, make([]string, 0), \"test item 1\")\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"test item 2\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcl := NewChecklist(\"o\", \"-\")\n\t\t\ttt.before(&cl)\n\n\t\t\tassert.Equal(t, tt.expectedLen, cl.LongestLine())\n\t\t})\n\t}\n}\n\nfunc Test_IndexByItem(t *testing.T) {\n\tcl := NewChecklist(\"o\", \"-\")\n\tcl.Add(false, nil, make([]string, 0), \"unchecked item\")\n\tcl.Add(true, nil, make([]string, 0), \"checked item\")\n\n\ttests := []struct {\n\t\tname        string\n\t\titem        *ChecklistItem\n\t\texpectedIdx int\n\t\texpectedOk  bool\n\t}{\n\t\t{\n\t\t\tname:        \"with nil\",\n\t\t\titem:        nil,\n\t\t\texpectedIdx: 0,\n\t\t\texpectedOk:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"with valid item\",\n\t\t\titem:        cl.Items[1],\n\t\t\texpectedIdx: 1,\n\t\t\texpectedOk:  true,\n\t\t},\n\t\t{\n\t\t\tname:        \"with valid item\",\n\t\t\titem:        NewChecklistItem(false, nil, make([]string, 0), \"invalid\", \"x\", \" \"),\n\t\t\texpectedIdx: 0,\n\t\t\texpectedOk:  false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\n\t\t\tidx, ok := cl.IndexByItem(tt.item)\n\n\t\t\tassert.Equal(t, tt.expectedIdx, idx)\n\t\t\tassert.Equal(t, tt.expectedOk, ok)\n\t\t})\n\t}\n}\n\nfunc Test_UncheckedItems(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\texpectedLen int\n\t\tcheckedLen  int\n\t\tbefore      func(cl *Checklist)\n\t}{\n\t\t{\n\t\t\tname:        \"with no items\",\n\t\t\texpectedLen: 0,\n\t\t\tcheckedLen:  0,\n\t\t\tbefore:      func(cl *Checklist) {},\n\t\t},\n\t\t{\n\t\t\tname:        \"with no unchecked items\",\n\t\t\texpectedLen: 1,\n\t\t\tcheckedLen:  0,\n\t\t\tbefore: func(cl *Checklist) {\n\t\t\t\tcl.Add(true, nil, make([]string, 0), \"unchecked item\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"with one unchecked item\",\n\t\t\texpectedLen: 2,\n\t\t\tcheckedLen:  1,\n\t\t\tbefore: func(cl *Checklist) {\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"unchecked item\")\n\t\t\t\tcl.Add(true, nil, make([]string, 0), \"checked item\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"with multiple unchecked items\",\n\t\t\texpectedLen: 3,\n\t\t\tcheckedLen:  2,\n\t\t\tbefore: func(cl *Checklist) {\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"unchecked item\")\n\t\t\t\tcl.Add(true, nil, make([]string, 0), \"checked item 11\")\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"checked item 2\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcl := NewChecklist(\"o\", \"-\")\n\t\t\ttt.before(&cl)\n\n\t\t\tassert.Equal(t, tt.expectedLen, len(cl.Items))\n\t\t\tassert.Equal(t, tt.checkedLen, len(cl.UncheckedItems()))\n\t\t})\n\t}\n}\n\nfunc Test_Unselect(t *testing.T) {\n\tcl := NewChecklist(\"o\", \"-\")\n\tcl.Add(false, nil, make([]string, 0), \"unchecked item\")\n\n\tcl.selected = 0\n\tassert.Equal(t, 0, cl.selected)\n\n\tcl.Unselect()\n\tassert.Equal(t, -1, cl.selected)\n}\n\n/* -------------------- Sort Interface -------------------- */\n\nfunc Test_Len(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\texpectedLen int\n\t\tbefore      func(cl *Checklist)\n\t}{\n\t\t{\n\t\t\tname:        \"with no items\",\n\t\t\texpectedLen: 0,\n\t\t\tbefore:      func(cl *Checklist) {},\n\t\t},\n\t\t{\n\t\t\tname:        \"with one item\",\n\t\t\texpectedLen: 1,\n\t\t\tbefore: func(cl *Checklist) {\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"unchecked item\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"with multiple items\",\n\t\t\texpectedLen: 3,\n\t\t\tbefore: func(cl *Checklist) {\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"unchecked item\")\n\t\t\t\tcl.Add(true, nil, make([]string, 0), \"checked item 1\")\n\t\t\t\tcl.Add(false, nil, make([]string, 0), \"checked item 2\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcl := NewChecklist(\"o\", \"-\")\n\t\t\ttt.before(&cl)\n\n\t\t\tassert.Equal(t, tt.expectedLen, cl.Len())\n\t\t})\n\t}\n}\n\nfunc Test_Less(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfirst    string\n\t\tsecond   string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"same\",\n\t\t\tfirst:    \"\",\n\t\t\tsecond:   \"\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"last less\",\n\t\t\tfirst:    \"beta\",\n\t\t\tsecond:   \"alpha\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"first less\",\n\t\t\tfirst:    \"alpha\",\n\t\t\tsecond:   \"beta\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcl := NewChecklist(\"o\", \"-\")\n\t\t\tcl.Add(false, nil, make([]string, 0), tt.first)\n\t\t\tcl.Add(false, nil, make([]string, 0), tt.second)\n\n\t\t\tassert.Equal(t, tt.expected, cl.Less(0, 1))\n\t\t})\n\t}\n}\n\nfunc Test_Swap(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfirst    string\n\t\tsecond   string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:   \"same\",\n\t\t\tfirst:  \"\",\n\t\t\tsecond: \"\",\n\t\t},\n\t\t{\n\t\t\tname:   \"last less\",\n\t\t\tfirst:  \"alpha\",\n\t\t\tsecond: \"beta\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcl := NewChecklist(\"o\", \"-\")\n\t\t\tcl.Add(false, nil, make([]string, 0), tt.first)\n\t\t\tcl.Add(false, nil, make([]string, 0), tt.second)\n\n\t\t\tcl.Swap(0, 1)\n\n\t\t\tassert.Equal(t, tt.expected, cl.Items[0].Text == \"beta\")\n\t\t\tassert.Equal(t, tt.expected, cl.Items[1].Text == \"alpha\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "flags/flags.go",
    "content": "package flags\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"strings\"\n\n\t\"github.com/chzyer/readline\"\n\tgoFlags \"github.com/jessevdk/go-flags\"\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/help\"\n)\n\n// Flags is the container for command line flag data\ntype Flags struct {\n\tConfig  string `short:\"c\" long:\"config\" optional:\"no\" description:\"Path to config file\"`\n\tModule  string `short:\"m\" long:\"module\" optional:\"no\" description:\"Display info about a specific module, i.e.: 'wtfutil -m=todo'\"`\n\tProfile bool   `short:\"p\" long:\"profile\" optional:\"yes\" description:\"Profile application memory usage\"`\n\tVersion bool   `short:\"v\" long:\"version\" description:\"Show version info\"`\n\t// Work-around go-flags misfeatures. If any sub-command is defined\n\t// then `wtf` (no sub-commands, the common usage), is warned about.\n\tOpt struct {\n\t\tCmd  string   `positional-arg-name:\"command\"`\n\t\tArgs []string `positional-arg-name:\"args\"`\n\t} `positional-args:\"yes\"`\n\n\thasCustom bool\n}\n\nvar EXTRA = `\nCommands:\n  save-secret <service>\n    service      Service URL or module name of secret.\n  Save a secret into the secret store. The secret will be prompted for.\n  Requires wtf.secretStore to be configured.  See individual modules for\n  information on what service and secret means for their configuration,\n  not all modules use secrets.\n`\n\n// NewFlags creates an instance of Flags\nfunc NewFlags() *Flags {\n\tflags := Flags{}\n\treturn &flags\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// ConfigFilePath returns the path to the currently-loaded config file\nfunc (flags *Flags) ConfigFilePath() string {\n\treturn flags.Config\n}\n\n// RenderIf displays special-case information based on the flags passed\n// in, if any flags were passed in\nfunc (flags *Flags) RenderIf(config *config.Config) {\n\tif flags.HasModule() {\n\t\thelp.Display(flags.Module, config)\n\t\tos.Exit(0)\n\t}\n\n\tif flags.HasVersion() {\n\n\t\tinfo, ok := debug.ReadBuildInfo()\n\t\tif !ok {\n\t\t\tos.Exit(1)\n\t\t\treturn\n\t\t}\n\n\t\tvar official bool\n\t\tvar revision string\n\t\tversion := info.Main.Version\n\t\tdate := \"unknown\"\n\n\t\t// Check if this binary was built with git. If so, extract details.\n\t\tfor _, setting := range info.Settings {\n\t\t\tswitch setting.Key {\n\t\t\tcase \"vcs.revision\":\n\t\t\t\trevision = setting.Value[0:12] // only need the 12 char hash\n\t\t\tcase \"vcs.time\":\n\t\t\t\tdate = setting.Value\n\t\t\t}\n\t\t}\n\n\t\t// if we're built with git...\n\t\tif revision != \"\" {\n\t\t\tif !strings.Contains(version, revision) {\n\t\t\t\tofficial = true\n\t\t\t}\n\t\t}\n\n\t\tif official {\n\t\t\tfmt.Printf(\"WTF %s (built: %s)\\n\", version, date)\n\t\t} else {\n\t\t\tfmt.Printf(\"WTF %s\\nNote: This is an unofficial release.\\n\", version)\n\t\t}\n\n\t\tos.Exit(0)\n\t}\n\n\tif flags.Opt.Cmd == \"\" {\n\t\treturn\n\t}\n\n\tswitch cmd := flags.Opt.Cmd; cmd {\n\tcase \"save-secret\":\n\t\tvar service, secret string\n\t\targs := flags.Opt.Args\n\n\t\tif len(args) < 1 || args[0] == \"\" {\n\t\t\tfmt.Fprintf(os.Stderr, \"save-secret: service required, see `%s --help`\\n\", os.Args[0])\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tservice = args[0]\n\n\t\tif len(args) > 1 {\n\t\t\tfmt.Fprintf(os.Stderr, \"save-secret: too many arguments, see `%s --help`\\n\", os.Args[0])\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tb, err := readline.Password(\"Secret: \")\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Error: %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tsecret = string(b)\n\t\tsecret = strings.TrimSpace(secret)\n\n\t\tif secret == \"\" {\n\t\t\tfmt.Fprintf(os.Stderr, \"save-secret: secret required, see `%s --help`\\n\", os.Args[0])\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\terr = cfg.StoreSecret(config, &cfg.Secret{\n\t\t\tService:  service,\n\t\t\tSecret:   secret,\n\t\t\tUsername: \"default\",\n\t\t})\n\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Saving secret for service %q: %s\\n\", service, err.Error())\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tfmt.Printf(\"Saved secret for service %q\\n\", service)\n\t\tos.Exit(0)\n\tdefault:\n\t\tfmt.Fprintf(os.Stderr, \"Command `%s` is not supported, try `%s --help`\\n\", cmd, os.Args[0])\n\t\tos.Exit(1)\n\t}\n}\n\n// HasCustomConfig returns TRUE if a config path was passed in, FALSE if one was not\nfunc (flags *Flags) HasCustomConfig() bool {\n\treturn flags.hasCustom\n}\n\n// HasModule returns TRUE if a module name was passed in, FALSE if one was not\nfunc (flags *Flags) HasModule() bool {\n\treturn len(flags.Module) > 0\n}\n\n// HasVersion returns TRUE if the version flag was passed in, FALSE if it was not\nfunc (flags *Flags) HasVersion() bool {\n\treturn flags.Version\n}\n\n// Parse parses the incoming flags\nfunc (flags *Flags) Parse() {\n\tparser := goFlags.NewParser(flags, goFlags.Default)\n\tif _, err := parser.Parse(); err != nil {\n\t\tif flagsErr, ok := err.(*goFlags.Error); ok && flagsErr.Type == goFlags.ErrHelp {\n\t\t\tfmt.Println(EXTRA)\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\n\t// If we have a custom config, then we're done parsing parameters, we don't need to\n\t// generate the default value\n\tflags.hasCustom = (len(flags.Config) > 0)\n\tif flags.hasCustom {\n\t\treturn\n\t}\n\n\t// If no config file is explicitly passed in as a param\n\t// then try the `WTF_CONFIG` environment variable\n\t// then fallback to the default config file to define the default flag value\n\tconfigDir, err := cfg.WtfConfigDir()\n\tif err != nil {\n\t\tfmt.Printf(\"Error: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n\tenvCfg := os.Getenv(\"WTF_CONFIG\")\n\tif envCfg == \"\" {\n\t\tflags.Config = filepath.Join(configDir, \"config.yml\")\n\t} else {\n\t\tflags.Config = envCfg\n\t}\n}\n"
  },
  {
    "path": "generator/settings.tpl",
    "content": "package {{(Lower .Name)}}\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"{{(.Name)}}\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\tcommon *cfg.Common\n\n    // Define your settings attributes here\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n        common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n        // Configure your settings attributes here. See http://github.com/olebedev/config for type details\n\t}\n\n\treturn &settings\n}"
  },
  {
    "path": "generator/textwidget.go",
    "content": "//go:build ignore\n\n// This package takes care of generates for empty widgets. Each generator is named after the\n// type of widget it generate, so textwidget.go will generate the skeleton for a new TextWidget\n// using the textwidget.tpl template.\n// The TextWidget generator needs one environment variable, called WTF_WIDGET_NAME, which will\n// be the name of the TextWidget it generates. If the variable hasn't been set, the generator\n// will use \"NewTextWidget\". On Linux and macOS the command can be run as\n// 'WTF_WIDGET_NAME=MyNewWidget go generate -run=text'.\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"text/template\"\n)\n\nconst (\n\tdefaultWidgetName = \"NewTextWidget\"\n\twidgetMaker       = \"app/widget_maker.go\"\n)\n\nfunc main() {\n\twidgetName, present := os.LookupEnv(\"WTF_WIDGET_NAME\")\n\tif !present {\n\t\twidgetName = defaultWidgetName\n\t}\n\n\tdata := struct {\n\t\tName string\n\t}{\n\t\twidgetName,\n\t}\n\n\tcreateModuleDirectory(data)\n\n\tgenerateWidgetFile(data)\n\tgenerateSettingsFile(data)\n\tfmt.Println(\"Don't forget to register your module in file\", widgetMaker)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc createModuleDirectory(data struct{ Name string }) {\n\terr := os.MkdirAll(strings.ToLower(fmt.Sprintf(\"modules/%s\", data.Name)), os.ModePerm)\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t}\n}\n\nfunc generateWidgetFile(data struct{ Name string }) {\n\ttpl, _ := template.New(\"textwidget.tpl\").Funcs(template.FuncMap{\n\t\t\"Lower\": strings.ToLower,\n\t}).ParseFiles(\"generator/textwidget.tpl\")\n\n\tout, err := os.Create(fmt.Sprintf(\"modules/%s/widget.go\", strings.ToLower(data.Name)))\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t}\n\tdefer out.Close()\n\n\ttpl.Execute(out, data)\n}\n\nfunc generateSettingsFile(data struct{ Name string }) {\n\ttpl, _ := template.New(\"settings.tpl\").Funcs(template.FuncMap{\n\t\t\"Lower\": strings.ToLower,\n\t}).ParseFiles(\"generator/settings.tpl\")\n\n\tout, err := os.Create(fmt.Sprintf(\"modules/%s/settings.go\", strings.ToLower(data.Name)))\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t}\n\tdefer out.Close()\n\n\ttpl.Execute(out, data)\n}\n"
  },
  {
    "path": "generator/textwidget.tpl",
    "content": "package {{(Lower .Name)}}\n\nimport (\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget is the container for your module's data\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n}\n\n// NewWidget creates and returns an instance of Widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\n    // The last call should always be to the display function\n    widget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() string {\n\treturn \"This is my widget\"\n}\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(func() (string, string, bool) {\n\t\treturn widget.CommonSettings().Title, widget.content(), false\n\t})\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/wtfutil/wtf\n\ngo 1.26.1\n\nrequire (\n\tbitbucket.org/mikehouston/asana-go v0.0.0-20201102222432-715318d0343a\n\tcode.cloudfoundry.org/bytefmt v0.66.0\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/PagerDuty/go-pagerduty v1.8.0\n\tgithub.com/VictorAvelar/devto-api-go v1.0.0\n\tgithub.com/adlio/trello v1.12.0\n\tgithub.com/alecthomas/chroma v0.10.0\n\tgithub.com/andygrunwald/go-gerrit v1.1.1\n\tgithub.com/briandowns/openweathermap v0.21.1\n\tgithub.com/cenkalti/backoff v2.2.1+incompatible // indirect\n\tgithub.com/chzyer/readline v1.5.1\n\tgithub.com/creack/pty v1.1.24\n\tgithub.com/digitalocean/godo v1.175.0\n\tgithub.com/docker/docker v28.5.2+incompatible\n\tgithub.com/docker/docker-credential-helpers v0.9.5\n\tgithub.com/docker/go-connections v0.5.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1\n\tgithub.com/gdamore/tcell v1.4.1\n\tgithub.com/go-ole/go-ole v1.2.6 // indirect\n\tgithub.com/godbus/dbus v4.1.0+incompatible // indirect\n\tgithub.com/google/go-github/v32 v32.1.0\n\tgithub.com/jedib0t/go-pretty/v6 v6.7.8\n\tgithub.com/jessevdk/go-flags v1.6.1\n\tgithub.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5\n\tgithub.com/mmcdole/gofeed v1.3.0\n\tgithub.com/olebedev/config v0.0.0-20190528211619-364964f3a8e4\n\tgithub.com/olekukonko/tablewriter v0.0.5\n\tgithub.com/ovh/cds v0.55.1\n\tgithub.com/pborman/uuid v1.2.0 // indirect\n\tgithub.com/piquette/finance-go v1.1.0\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/pkg/profile v1.7.0\n\tgithub.com/radovskyb/watcher v1.0.7\n\tgithub.com/rivo/tview v0.42.0\n\tgithub.com/shirou/gopsutil v2.21.11+incompatible\n\tgithub.com/shopspring/decimal v1.3.1 // indirect\n\tgithub.com/shurcooL/githubv4 v0.0.0-20200802174311-f27d2ca7f6d5\n\tgithub.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect\n\tgithub.com/spf13/cobra v1.10.2 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/wtfutil/spotigopher v0.0.0-20191127141047-7d8168fe103a\n\tgithub.com/zmb3/spotify v1.3.0\n\tgithub.com/zorkian/go-datadog-api v2.30.0+incompatible\n\tgolang.org/x/oauth2 v0.35.0\n\tgolang.org/x/sync v0.20.0\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.34.0\n\tgoogle.golang.org/api v0.266.0\n\tgopkg.in/yaml.v2 v2.4.0\n\tgotest.tools v2.2.0+incompatible\n\tjaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7\n\tk8s.io/apimachinery v0.35.2\n\tk8s.io/client-go v0.35.2\n)\n\nrequire github.com/nicklaw5/helix/v2 v2.32.0\n\nrequire (\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1\n\tgithub.com/Azure/azure-sdk-for-go/sdk/monitor/azquery v1.2.0\n\tgithub.com/charmbracelet/bubbles v0.21.1\n\tgithub.com/gdamore/tcell/v2 v2.13.8\n\tgithub.com/gopherlibs/todoist v0.1.0\n\tgithub.com/hekmon/transmissionrpc/v2 v2.0.1\n\tgithub.com/logrusorgru/aurora/v4 v4.0.0\n\tgithub.com/muesli/reflow v0.3.0\n\tgithub.com/prometheus-community/pro-bing v0.8.0\n\tgitlab.com/gitlab-org/api/client-go v0.160.1\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tcloud.google.com/go/auth v0.18.1 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcontrib.go.opencensus.io/exporter/jaeger v0.2.1 // indirect\n\tcontrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect\n\tdario.cat/mergo v1.0.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect\n\tgithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect\n\tgithub.com/CycloneDX/cyclonedx-go v0.8.0 // indirect\n\tgithub.com/ProtonMail/go-crypto v1.1.6 // indirect\n\tgithub.com/PuerkitoBio/goquery v1.8.0 // indirect\n\tgithub.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91 // indirect\n\tgithub.com/andybalholm/brotli v1.1.0 // indirect\n\tgithub.com/andybalholm/cascadia v1.3.1 // indirect\n\tgithub.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1 // indirect\n\tgithub.com/aokoli/goutils v1.1.1 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/blang/semver v3.5.1+incompatible // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/charmbracelet/bubbletea v1.3.10 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.4.1 // indirect\n\tgithub.com/charmbracelet/harmonica v0.2.0 // indirect\n\tgithub.com/charmbracelet/lipgloss v1.1.0 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.11.5 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.15 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.9.0 // indirect\n\tgithub.com/clipperhouse/stringish v0.1.1 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.5.0 // indirect\n\tgithub.com/cloudflare/circl v1.6.1 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/cyphar/filepath-securejoin v0.4.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/dlclark/regexp2 v1.4.0 // indirect\n\tgithub.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect\n\tgithub.com/eapache/go-resiliency v1.3.0 // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.12.2 // indirect\n\tgithub.com/emirpasic/gods v1.18.1 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/fatih/color v1.16.0 // indirect\n\tgithub.com/felixge/fgprof v0.9.5 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/forPelevin/gomoji v1.1.8 // indirect\n\tgithub.com/fsamin/go-dump v1.0.9 // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.9.0 // indirect\n\tgithub.com/gdamore/encoding v1.0.1 // indirect\n\tgithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect\n\tgithub.com/go-git/go-billy/v5 v5.6.2 // indirect\n\tgithub.com/go-git/go-git/v5 v5.16.5 // indirect\n\tgithub.com/go-gorp/gorp v2.0.0+incompatible // indirect\n\tgithub.com/go-kit/log v0.2.1 // indirect\n\tgithub.com/go-logfmt/logfmt v0.5.1 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.21.0 // indirect\n\tgithub.com/go-openapi/jsonreference v0.20.2 // indirect\n\tgithub.com/go-openapi/swag v0.23.0 // indirect\n\tgithub.com/go-sql-driver/mysql v1.9.3 // indirect\n\tgithub.com/golang-jwt/jwt v3.2.2+incompatible // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.5.2 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.3.0 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/golang/snappy v0.0.4 // indirect\n\tgithub.com/google/gnostic-models v0.7.0 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.17.0 // indirect\n\tgithub.com/gookit/color v1.5.4 // indirect\n\tgithub.com/gorhill/cronexpr v0.0.0-20161205141322-d520615e531a // indirect\n\tgithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.7.8 // indirect\n\tgithub.com/hekmon/cunits/v2 v2.1.0 // indirect\n\tgithub.com/huandu/xstrings v1.4.0 // indirect\n\tgithub.com/iancoleman/orderedmap v0.3.0 // indirect\n\tgithub.com/imdario/mergo v0.3.13 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect\n\tgithub.com/jfrog/archiver/v3 v3.6.0 // indirect\n\tgithub.com/jfrog/build-info-go v1.9.23 // indirect\n\tgithub.com/jfrog/gofrog v1.6.0 // indirect\n\tgithub.com/jfrog/jfrog-client-go v1.37.1 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect\n\tgithub.com/kevinburke/ssh_config v1.2.0 // indirect\n\tgithub.com/klauspost/compress v1.17.4 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.3 // indirect\n\tgithub.com/klauspost/pgzip v1.2.6 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/maruel/panicparse/v2 v2.2.2 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.19 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/minio/sha256-simd v1.0.1 // indirect\n\tgithub.com/mitchellh/hashstructure v0.0.0-20170609045927-2bca23e0e452 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/sys/atomicwriter v0.1.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/termenv v0.16.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/nwaples/rardecode v1.1.3 // indirect\n\tgithub.com/onsi/ginkgo v1.16.5 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.0-rc6 // indirect\n\tgithub.com/ovh/cds/sdk/interpolate v0.0.0-20190319104452-71125b036b25 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.21 // indirect\n\tgithub.com/pjbgf/sha1cd v0.3.2 // indirect\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/prometheus/client_golang v1.16.0 // indirect\n\tgithub.com/prometheus/client_model v0.4.0 // indirect\n\tgithub.com/prometheus/common v0.44.0 // indirect\n\tgithub.com/prometheus/procfs v0.10.1 // indirect\n\tgithub.com/prometheus/statsd_exporter v0.22.7 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/rockbears/log v0.11.2 // indirect\n\tgithub.com/rockbears/yaml v0.4.0 // indirect\n\tgithub.com/rs/xid v1.2.1 // indirect\n\tgithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect\n\tgithub.com/sguiheux/go-coverage v0.0.0-20190710153556-287b082a7197 // indirect\n\tgithub.com/sguiheux/jsonschema v0.0.0-20240314085137-97ecc280683c // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/skeema/knownhosts v1.3.1 // indirect\n\tgithub.com/spf13/afero v1.11.0 // indirect\n\tgithub.com/spf13/cast v1.6.0 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.10 // indirect\n\tgithub.com/tklauser/numcpus v0.4.0 // indirect\n\tgithub.com/uber/jaeger-client-go v2.25.0+incompatible // indirect\n\tgithub.com/ulikunitz/xz v0.5.11 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgithub.com/xanzy/ssh-agent v0.3.3 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/xeipuuv/gojsonschema v1.2.0 // indirect\n\tgithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.2 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect\n\tgo.opentelemetry.io/otel v1.39.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.39.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.39.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.uber.org/zap v1.27.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect\n\tgolang.org/x/net v0.51.0 // indirect\n\tgolang.org/x/term v0.40.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect\n\tgoogle.golang.org/grpc v1.78.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/AlecAivazis/survey.v1 v1.7.1 // indirect\n\tgopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/warnings.v0 v0.1.2 // indirect\n\tgotest.tools/v3 v3.3.0 // indirect\n\tk8s.io/api v0.35.2 // indirect\n\tk8s.io/klog/v2 v2.130.1 // indirect\n\tk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect\n\tk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect\n\tsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect\n\tsigs.k8s.io/randfill v1.0.0 // indirect\n\tsigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect\n\tsigs.k8s.io/yaml v1.6.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "bitbucket.org/mikehouston/asana-go v0.0.0-20201102222432-715318d0343a h1:qH51iOpTres3x2kNb0f2R3ggMpbYCyCvaRrsvdndhvY=\nbitbucket.org/mikehouston/asana-go v0.0.0-20201102222432-715318d0343a/go.mod h1:HcP4iCG6i6uVAyX2X7yKOsjbzLFiTfX0EMT20CYn5Ig=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=\ncloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncode.cloudfoundry.org/bytefmt v0.66.0 h1:tSK2uf1Shxb5SIc7W9RE+FSKnwuFwzoEqkpwwVQx0SM=\ncode.cloudfoundry.org/bytefmt v0.66.0/go.mod h1:JrBuqsb9cQeqrVSdWvcDLjnxD6RXfAev2s8nwjskhhs=\ncontrib.go.opencensus.io/exporter/jaeger v0.2.1 h1:yGBYzYMewVL0yO9qqJv3Z5+IRhPdU7e9o/2oKpX4YvI=\ncontrib.go.opencensus.io/exporter/jaeger v0.2.1/go.mod h1:Y8IsLgdxqh1QxYxPC5IgXVmBaeLUeQFfBeBi9PbeZd0=\ncontrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg=\ncontrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ=\ndario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=\ndario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=\ngithub.com/Azure/azure-sdk-for-go/sdk/monitor/azquery v1.2.0 h1:s0SaQtHigowP0n3Kx4ieV94pNZAHlHhS+xjZyLCSVCQ=\ngithub.com/Azure/azure-sdk-for-go/sdk/monitor/azquery v1.2.0/go.mod h1:oI5SPI1vpNJYfP9MPWXthq7jDfh9xTAuQVBKPOu7DPo=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/CycloneDX/cyclonedx-go v0.8.0 h1:FyWVj6x6hoJrui5uRQdYZcSievw3Z32Z88uYzG/0D6M=\ngithub.com/CycloneDX/cyclonedx-go v0.8.0/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk=\ngithub.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/Netflix/go-expect v0.0.0-20180928190340-9d1f4485533b h1:sSQK05nvxs4UkgCJaxihteu+r+6ela3dNMm7NVmsS3c=\ngithub.com/Netflix/go-expect v0.0.0-20180928190340-9d1f4485533b/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=\ngithub.com/PagerDuty/go-pagerduty v1.8.0 h1:MTFqTffIcAervB83U7Bx6HERzLbyaSPL/+oxH3zyluI=\ngithub.com/PagerDuty/go-pagerduty v1.8.0/go.mod h1:nzIeAqyFSJAFkjWKvMzug0JtwDg+V+UoCWjFrfFH5mI=\ngithub.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=\ngithub.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=\ngithub.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=\ngithub.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=\ngithub.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91 h1:vX+gnvBc56EbWYrmlhYbFYRaeikAke1GL84N4BEYOFE=\ngithub.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91/go.mod h1:cDLGBht23g0XQdLjzn6xOGXDkLK182YfINAaZEQLCHQ=\ngithub.com/VictorAvelar/devto-api-go v1.0.0 h1:oXmzye3xYvlgBX18vX4+v6LVbjoihgIokpeOpzeJzqU=\ngithub.com/VictorAvelar/devto-api-go v1.0.0/go.mod h1:gX13cqzMdpo49qP8VtBR2uCnzW7d76LFrAVSX2eLifY=\ngithub.com/adlio/trello v1.12.0 h1:JqOE2GFHQ9YtEviRRRSnicSxPbt4WFOxhqXzjMOw8lw=\ngithub.com/adlio/trello v1.12.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo=\ngithub.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=\ngithub.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=\ngithub.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=\ngithub.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=\ngithub.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=\ngithub.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=\ngithub.com/andygrunwald/go-gerrit v1.1.1 h1:U1Aw5WrwWIVc8PK+jMFmMGzGTYZFI8re8JS6DEGNYfA=\ngithub.com/andygrunwald/go-gerrit v1.1.1/go.mod h1:SeP12EkHZxEVjuJ2HZET304NBtHGG2X6w2Gzd0QXAZw=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1 h1:X8MJ0fnN5FPdcGF5Ij2/OW+HgiJrRg3AfHAx1PJtIzM=\ngithub.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=\ngithub.com/aokoli/goutils v1.1.0/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=\ngithub.com/aokoli/goutils v1.1.1 h1:/hA+Ywo3AxoDZY5ZMnkiEkUvkK4BPp927ax110KCqqg=\ngithub.com/aokoli/goutils v1.1.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=\ngithub.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=\ngithub.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=\ngithub.com/briandowns/openweathermap v0.21.1 h1:TPbuixuF+aGJP1mpgTNny6eUkdbvj7gqODGXkwhss48=\ngithub.com/briandowns/openweathermap v0.21.1/go.mod h1:0GLnknqicWxXnGi1IqoOaZIw+kIe5hkt+YM5WY3j8+0=\ngithub.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=\ngithub.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=\ngithub.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/charmbracelet/bubbles v0.21.1 h1:nj0decPiixaZeL9diI4uzzQTkkz1kYY8+jgzCZXSmW0=\ngithub.com/charmbracelet/bubbles v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk=\ngithub.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=\ngithub.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=\ngithub.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=\ngithub.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=\ngithub.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=\ngithub.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4=\ngithub.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=\ngithub.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=\ngithub.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=\ngithub.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=\ngithub.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=\ngithub.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=\ngithub.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=\ngithub.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=\ngithub.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=\ngithub.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=\ngithub.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=\ngithub.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=\ngithub.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=\ngithub.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=\ngithub.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=\ngithub.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/digitalocean/godo v1.175.0 h1:tpfwJFkBzpePxvvFazOn69TXctdxuFlOs7DMVXsI7oU=\ngithub.com/digitalocean/godo v1.175.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=\ngithub.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=\ngithub.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=\ngithub.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=\ngithub.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=\ngithub.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=\ngithub.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=\ngithub.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=\ngithub.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/eapache/go-resiliency v1.3.0 h1:RRL0nge+cWGlxXbUzJ7yMcq6w2XBEr19dCN6HECGaT0=\ngithub.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=\ngithub.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=\ngithub.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=\ngithub.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=\ngithub.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=\ngithub.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=\ngithub.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=\ngithub.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=\ngithub.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/forPelevin/gomoji v1.1.8 h1:JElzDdt0TyiUlecy6PfITDL6eGvIaxqYH1V52zrd0qQ=\ngithub.com/forPelevin/gomoji v1.1.8/go.mod h1:8+Z3KNGkdslmeGZBC3tCrwMrcPy5GRzAD+gL9NAwMXg=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsamin/go-dump v1.0.9 h1:3MAneAJLnGfKTJtFEAdgrD+QqqK2Hwj7EJUQMQZcDls=\ngithub.com/fsamin/go-dump v1.0.9/go.mod h1:ZgKd2aOXAFFbbFuUgvQhu7mwTlI3d3qnTICMWdvAa9o=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=\ngithub.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=\ngithub.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=\ngithub.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=\ngithub.com/gdamore/tcell v1.4.1 h1:6T2+7Zl5U44SU3ensYi/w4SX5hpzbK6NDUDYmgCP3eQ=\ngithub.com/gdamore/tcell v1.4.1/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=\ngithub.com/gdamore/tcell/v2 v2.13.8 h1:Mys/Kl5wfC/GcC5Cx4C2BIQH9dbnhnkPgS9/wF3RlfU=\ngithub.com/gdamore/tcell/v2 v2.13.8/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=\ngithub.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=\ngithub.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=\ngithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=\ngithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=\ngithub.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=\ngithub.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=\ngithub.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=\ngithub.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=\ngithub.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=\ngithub.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gorp/gorp v2.0.0+incompatible h1:dIQPsBtl6/H1MjVseWuWPXa7ET4p6Dve4j3Hg+UjqYw=\ngithub.com/go-gorp/gorp v2.0.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=\ngithub.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=\ngithub.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=\ngithub.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=\ngithub.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=\ngithub.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=\ngithub.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=\ngithub.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=\ngithub.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=\ngithub.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=\ngithub.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=\ngithub.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=\ngithub.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=\ngithub.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=\ngithub.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=\ngithub.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=\ngithub.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=\ngithub.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=\ngithub.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=\ngithub.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=\ngithub.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=\ngithub.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=\ngithub.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=\ngithub.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=\ngithub.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=\ngithub.com/gopherlibs/todoist v0.1.0 h1:9a/fs7RWhiMG9HsEXgqsht3ItAIT+FXQs01PvZ+QlIU=\ngithub.com/gopherlibs/todoist v0.1.0/go.mod h1:SLnIuCt7Z0Vn9msbmwX8KeksWJKIpjsNSD8LWOflceU=\ngithub.com/gorhill/cronexpr v0.0.0-20161205141322-d520615e531a h1:yNuTIQkXLNAevCwQJ7ur3ZPoZPhbvAi6QXhJ/ylX6+8=\ngithub.com/gorhill/cronexpr v0.0.0-20161205141322-d520615e531a/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA=\ngithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=\ngithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=\ngithub.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hekmon/cunits/v2 v2.1.0 h1:k6wIjc4PlacNOHwKEMBgWV2/c8jyD4eRMs5mR1BBhI0=\ngithub.com/hekmon/cunits/v2 v2.1.0/go.mod h1:9r1TycXYXaTmEWlAIfFV8JT+Xo59U96yUJAYHxzii2M=\ngithub.com/hekmon/transmissionrpc/v2 v2.0.1 h1:WkILCEdbNy3n/N/w7mi449waMPdH2AA1THyw7TfnN/w=\ngithub.com/hekmon/transmissionrpc/v2 v2.0.1/go.mod h1:+s96Pkg7dIP3h2PT3fzhXPvNb3OdLryh5J8PIvQg3aA=\ngithub.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c h1:kp3AxgXgDOmIJFR7bIwqFhwJ2qWar8tEQSE5XXhCfVk=\ngithub.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=\ngithub.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=\ngithub.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=\ngithub.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=\ngithub.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=\ngithub.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=\ngithub.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=\ngithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=\ngithub.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o=\ngithub.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=\ngithub.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=\ngithub.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=\ngithub.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=\ngithub.com/jfrog/archiver/v3 v3.6.0 h1:OVZ50vudkIQmKMgA8mmFF9S0gA47lcag22N13iV3F1w=\ngithub.com/jfrog/archiver/v3 v3.6.0/go.mod h1:fCAof46C3rAXgZurS8kNRNdSVMKBbZs+bNNhPYxLldI=\ngithub.com/jfrog/build-info-go v1.9.23 h1:+TwUIBEJwRvz9skR8xBfY5ti8Vl4Z6iMCkFbkclnEN0=\ngithub.com/jfrog/build-info-go v1.9.23/go.mod h1:QHcKuesY4MrBVBuEwwBz4uIsX6mwYuMEDV09ng4AvAU=\ngithub.com/jfrog/gofrog v1.6.0 h1:jOwb37nHY2PnxePNFJ6e6279Pgkr3di05SbQQw47Mq8=\ngithub.com/jfrog/gofrog v1.6.0/go.mod h1:SZ1EPJUruxrVGndOzHd+LTiwWYKMlHqhKD+eu+v5Hqg=\ngithub.com/jfrog/jfrog-client-go v1.37.1 h1:BqIWGPajC5vhUo5dcQ9KEJr0EVANr/O4cfEqRYvzvRg=\ngithub.com/jfrog/jfrog-client-go v1.37.1/go.mod h1:y+zeO0LeT2uHoHs4/fXHrm5dfF02bg6Dw3cNJxgJ5LY=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=\ngithub.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=\ngithub.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=\ngithub.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=\ngithub.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=\ngithub.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=\ngithub.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=\ngithub.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=\ngithub.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=\ngithub.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=\ngithub.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUpJYn9JA=\ngithub.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ=\ngithub.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=\ngithub.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/maruel/panicparse/v2 v2.2.2 h1:4Gu/Z5oLpJCE/0/NwxrUkyn7alpqOQdJAUuchB2OoJU=\ngithub.com/maruel/panicparse/v2 v2.2.2/go.mod h1:WizmeHJfpyKYYKGInKv8ax8jh7DJnQE5yFDuzFfHzIU=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=\ngithub.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=\ngithub.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s=\ngithub.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY=\ngithub.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=\ngithub.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=\ngithub.com/mitchellh/hashstructure v0.0.0-20170609045927-2bca23e0e452 h1:hOY53G+kBFhbYFpRVxHl5eS7laP6B1+Cq+Z9Dry1iMU=\ngithub.com/mitchellh/hashstructure v0.0.0-20170609045927-2bca23e0e452/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=\ngithub.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=\ngithub.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk=\ngithub.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=\ngithub.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=\ngithub.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/nicklaw5/helix/v2 v2.32.0 h1:ZRPt+wRUMQqpny6yZKVY9rUGNwv+ZmIh75fSiopMXuY=\ngithub.com/nicklaw5/helix/v2 v2.32.0/go.mod h1:KaXa2mb2kBzsDana9RbXevTgnfU95DMoSORWo2hqlWA=\ngithub.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=\ngithub.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/olebedev/config v0.0.0-20190528211619-364964f3a8e4 h1:JnVsYEQzhEcOspy6ngIYNF2u0h2mjkXZptzX0IzZQ4g=\ngithub.com/olebedev/config v0.0.0-20190528211619-364964f3a8e4/go.mod h1:RL5+WRxWTAXqqCi9i+eZlHrUtO7AQujUqWi+xMohmc4=\ngithub.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=\ngithub.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=\ngithub.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=\ngithub.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=\ngithub.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=\ngithub.com/ovh/cds v0.55.1 h1:HswwKxa3cWmrA0ldbJQPo8NlbuaM7XWbWLnmB1QDOQ8=\ngithub.com/ovh/cds v0.55.1/go.mod h1:o0/2LYGTbcZ1Ozo3Hi4O2CBPoJ3E5F7yKDtU8zYYw5o=\ngithub.com/ovh/cds/sdk/interpolate v0.0.0-20190319104452-71125b036b25 h1:pV462fyYKs6YRXCrj5OI0OgA6wVWSiQmtG8SAJdT1WQ=\ngithub.com/ovh/cds/sdk/interpolate v0.0.0-20190319104452-71125b036b25/go.mod h1:qrsz1nc0EPZnhuTLZpKK4Y7awUQdeKmkY5M6DbKi6Ws=\ngithub.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=\ngithub.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=\ngithub.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=\ngithub.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=\ngithub.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=\ngithub.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=\ngithub.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/piquette/finance-go v1.1.0 h1:3J5VBP6aPhvrj9Eg6Eus8eM6QJlX4l/wCfrJhONjS3k=\ngithub.com/piquette/finance-go v1.1.0/go.mod h1:jaHaD5JJEWpl5mW712M8gRboc2xvhjshF3lqw/ke7AA=\ngithub.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=\ngithub.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=\ngithub.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus-community/pro-bing v0.8.0 h1:CEY/g1/AgERRDjxw5P32ikcOgmrSuXs7xon7ovx6mNc=\ngithub.com/prometheus-community/pro-bing v0.8.0/go.mod h1:Idyxz8raDO6TgkUN6ByiEGvWJNyQd40kN9ZUeho3lN0=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=\ngithub.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=\ngithub.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=\ngithub.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=\ngithub.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=\ngithub.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=\ngithub.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=\ngithub.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=\ngithub.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=\ngithub.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=\ngithub.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=\ngithub.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0=\ngithub.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=\ngithub.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=\ngithub.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=\ngithub.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=\ngithub.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rockbears/log v0.11.2 h1:YjM+lAyXv4UA5/23trG1VXW3UveHqU7Vcav+PJN8cNw=\ngithub.com/rockbears/log v0.11.2/go.mod h1:cRirhSHaq6iYYTy3Sf6moRdIEE5+hZOjqNMoi9XuFJw=\ngithub.com/rockbears/yaml v0.4.0 h1:Mvxo/KXPdZ2x3XOMM+xj0Vvm3sb6E2uh4jeoCtdHab4=\ngithub.com/rockbears/yaml v0.4.0/go.mod h1:8cDJx2PWQJMtfGgsRCvHVbIB61SV3dvy8o6EGv2cIpg=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=\ngithub.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=\ngithub.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=\ngithub.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=\ngithub.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=\ngithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=\ngithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=\ngithub.com/sguiheux/go-coverage v0.0.0-20190710153556-287b082a7197 h1:qu90yDtRE5WEfRT5mn9v0Xz9RaopLguhbPwZKx4dHq8=\ngithub.com/sguiheux/go-coverage v0.0.0-20190710153556-287b082a7197/go.mod h1:0hhKrsUsoT7yvxwNGKa+TSYNA26DNWMqReeZEQq/9FI=\ngithub.com/sguiheux/jsonschema v0.0.0-20240314085137-97ecc280683c h1:hQYbZuIznuaJ8YLitOm0exsrP3qO9thLGG5eDGRpmEw=\ngithub.com/sguiheux/jsonschema v0.0.0-20240314085137-97ecc280683c/go.mod h1:9NwRfsAcwe0ZCLkSCziM+PtKQYJAzjydh4d8dMxggT0=\ngithub.com/shirou/gopsutil v2.21.11+incompatible h1:lOGOyCG67a5dv2hq5Z1BLDUqqKp3HkbjPcz5j6XMS0U=\ngithub.com/shirou/gopsutil v2.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=\ngithub.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=\ngithub.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=\ngithub.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shurcooL/githubv4 v0.0.0-20200802174311-f27d2ca7f6d5 h1:CA6Mjshr+g5YHENwllpQNR0UaYO7VGKo6TzJLM64WJQ=\ngithub.com/shurcooL/githubv4 v0.0.0-20200802174311-f27d2ca7f6d5/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=\ngithub.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=\ngithub.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=\ngithub.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=\ngithub.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=\ngithub.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=\ngithub.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=\ngithub.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=\ngithub.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=\ngithub.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=\ngithub.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=\ngithub.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=\ngithub.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo=\ngithub.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=\ngithub.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=\ngithub.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=\ngithub.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=\ngithub.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=\ngithub.com/uber/jaeger-client-go v2.25.0+incompatible h1:IxcNZ7WRY1Y3G4poYlx24szfsn/3LvK9QHCq9oQw8+U=\ngithub.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=\ngithub.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=\ngithub.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/wtfutil/spotigopher v0.0.0-20191127141047-7d8168fe103a h1:2eyMT9EpTPS4PiVfvXvqA8PKB5FoSl6gGjgb3CQ0cug=\ngithub.com/wtfutil/spotigopher v0.0.0-20191127141047-7d8168fe103a/go.mod h1:AlO4kKlF1zyOHTq2pBzxEERdBDStJev0VZNukFEqz/E=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=\ngithub.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=\ngithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=\ngithub.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngithub.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=\ngithub.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=\ngithub.com/zmb3/spotify v1.3.0 h1:6Z2F1IMx0Hviq/dpf8nFwvKPppFEMXn8yfReSBVi16k=\ngithub.com/zmb3/spotify v1.3.0/go.mod h1:GD7AAEMUJVYc2Z7p2a2S0E3/5f/KxM/vOnErNr4j+Tw=\ngithub.com/zorkian/go-datadog-api v2.30.0+incompatible h1:R4ryGocppDqZZbnNc5EDR8xGWF/z/MxzWnqTUijDQes=\ngithub.com/zorkian/go-datadog-api v2.30.0+incompatible/go.mod h1:PkXwHX9CUQa/FpB9ZwAD45N1uhCW4MT/Wj7m36PbKss=\ngitlab.com/gitlab-org/api/client-go v0.160.1 h1:7kEgo1yQ3ZMRps/2JbXzqbRb4Rs8n2ECkAv+6MadJw8=\ngitlab.com/gitlab-org/api/client-go v0.160.1/go.mod h1:YqKcnxyV9OPAL5U99mpwBVEgBPz1PK/3qwqq/3h6bao=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngo.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=\ngo.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=\ngo.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=\ngolang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=\ngolang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=\ngoogle.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=\ngoogle.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=\ngoogle.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/AlecAivazis/survey.v1 v1.7.1 h1:mzQIVyOPSXJaQWi1m6AFCjrCEPIwQBSOn48Ri8ZpzAg=\ngopkg.in/AlecAivazis/survey.v1 v1.7.1/go.mod h1:2Ehl7OqkBl3Xb8VmC4oFW2bItAhnUfzIjrOzwRxCrOU=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=\ngopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=\ngotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=\ngotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=\ngotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\njaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:mub0MmFLOn8XLikZOAhgLD1kXJq8jgftSrrv7m00xFo=\njaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=\nk8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=\nk8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=\nk8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=\nk8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=\nk8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=\nk8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=\nk8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=\nk8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=\nk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=\nk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=\nsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=\nsigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=\nsigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=\nsigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=\nsigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=\nsigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=\nsigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=\n"
  },
  {
    "path": "help/help.go",
    "content": "package help\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/app\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\n// Display displays the output of the --help argument\nfunc Display(moduleName string, cfg *config.Config) {\n\tif moduleName == \"\" {\n\t\tfmt.Println(\"\\n  --module takes a module name as an argument, i.e: '--module=github'\")\n\t} else {\n\t\tfmt.Printf(\"%s\\n\", helpFor(moduleName, cfg))\n\t}\n}\n\nfunc helpFor(moduleName string, cfg *config.Config) string {\n\terr := cfg.Set(\"wtf.mods.\"+moduleName+\".enabled\", true)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\twidget := app.MakeWidget(nil, nil, moduleName, cfg, nil)\n\n\t// Since we are forcing enabled config, if no module\n\t// exists, we will get the unknown one\n\tif widget.CommonSettings().Title == \"Unknown\" {\n\t\treturn \"Unable to find module \" + moduleName\n\t}\n\n\tresult := \"\"\n\tresult += utils.StripColorTags(widget.HelpText())\n\tresult += \"\\n\"\n\tresult += \"Configuration Attributes\"\n\tresult += widget.ConfigText()\n\treturn result\n}\n"
  },
  {
    "path": "logger/log.go",
    "content": "package logger\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc Log(msg string) {\n\tif LogFileMissing() {\n\t\treturn\n\t}\n\n\tf, err := os.OpenFile(LogFilePath(), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)\n\tif err != nil {\n\t\tlog.Fatalf(\"error opening file: %v\", err)\n\t}\n\tdefer func() { _ = f.Close() }()\n\n\tlog.SetOutput(f)\n\tlog.Println(msg)\n}\n\nfunc LogFileMissing() bool {\n\treturn LogFilePath() == \"\"\n}\n\nfunc LogFilePath() string {\n\tdir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn filepath.Join(dir, \".config\", \"wtf\", \"log.txt\")\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\t// Blank import of tzdata embeds the timezone database to allow Windows hosts to find timezone\n\t// information even if the timezone database is not available on the local system. See release\n\t// notes at https://golang.org/doc/go1.15#time/tzdata for details. This prevents \"no timezone\n\t// data available\" errors in clocks module.\n\t_ \"time/tzdata\"\n\n\t\"github.com/logrusorgru/aurora/v4\"\n\t\"github.com/pkg/profile\"\n\n\t\"github.com/wtfutil/wtf/app\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/flags\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\n/* -------------------- Main -------------------- */\n\nfunc main() {\n\tlog.SetFlags(log.LstdFlags | log.Lshortfile)\n\n\t// Parse and handle flags\n\tflags := flags.NewFlags()\n\tflags.Parse()\n\n\t// Load the configuration file\n\tcfg.Initialize(flags.HasCustomConfig())\n\tconfig := cfg.LoadWtfConfigFile(flags.ConfigFilePath())\n\n\twtf.SetTerminal(config)\n\n\tflags.RenderIf(config)\n\n\tif flags.Profile {\n\t\tdefer profile.Start(profile.MemProfile).Stop()\n\t}\n\n\topenFileUtil := config.UString(\"wtf.openFileUtil\", \"open\")\n\topenURLUtil := utils.ToStrs(config.UList(\"wtf.openUrlUtil\", []interface{}{}))\n\tutils.Init(openFileUtil, openURLUtil)\n\n\t/* Initialize the App Manager */\n\tappMan := app.NewAppManager()\n\tappMan.MakeNewWtfApp(config, flags.Config)\n\n\tcurrentApp, err := appMan.Current()\n\tif err != nil {\n\t\tfmt.Printf(\"\\n%s %v\\n\", aurora.Red(\"ERROR\"), err)\n\t\tos.Exit(1)\n\t}\n\n\terr = currentApp.Execute()\n\tif err != nil {\n\t\tfmt.Printf(\"\\n%s %v\\n\", aurora.Red(\"ERROR\"), err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "modules/airbrake/client.go",
    "content": "package airbrake\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc project(projectID int, authToken string) (*Project, error) {\n\turl := fmt.Sprintf(\n\t\t\"https://api.airbrake.io/api/v4/projects/%d?key=%s\",\n\t\tprojectID, authToken)\n\treq, err := http.NewRequest(\"GET\", url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tp := &ProjectJSON{}\n\terr = utils.ParseJSON(p, resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &p.Project, nil\n}\n\nfunc groups(projectID int, authToken string) ([]Group, error) {\n\turl := fmt.Sprintf(\n\t\t\"https://api.airbrake.io/api/v4/projects/%d/groups?key=%s&limit=10&order=last_notice&resolved=false\",\n\t\tprojectID, authToken)\n\treq, err := http.NewRequest(\"GET\", url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tj := &GroupJSON{}\n\terr = utils.ParseJSON(j, resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn j.Groups, nil\n}\n\nfunc resolveGroup(projectID int64, groupID, authToken string) error {\n\turl := fmt.Sprintf(\n\t\t\"https://airbrake.io/api/v4/projects/%d/groups/%s/resolved?key=%s\",\n\t\tprojectID, groupID, authToken)\n\treq, err := http.NewRequest(\"PUT\", url, http.NoBody)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\treturn nil\n}\n\nfunc muteGroup(projectID int64, groupID, authToken string) error {\n\turl := fmt.Sprintf(\n\t\t\"https://airbrake.io/api/v4/projects/%d/groups/%s/muted?key=%s\",\n\t\tprojectID, groupID, authToken)\n\treq, err := http.NewRequest(\"PUT\", url, http.NoBody)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\treturn nil\n}\n\nfunc unmuteGroup(projectID int64, groupID, authToken string) error {\n\turl := fmt.Sprintf(\n\t\t\"https://airbrake.io/api/v4/projects/%d/groups/%s/unmuted?key=%s\",\n\t\tprojectID, groupID, authToken)\n\treq, err := http.NewRequest(\"PUT\", url, http.NoBody)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\treturn nil\n}\n"
  },
  {
    "path": "modules/airbrake/group_info_table.go",
    "content": "package airbrake\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype groupInfoTable struct {\n\tgroup       *Group\n\tpropertyMap map[string]string\n\n\tcolWidth0   int\n\tcolWidth1   int\n\ttableHeight int\n}\n\nfunc newGroupInfoTable(g *Group) *groupInfoTable {\n\tpropTable := &groupInfoTable{\n\t\tgroup: g,\n\n\t\tcolWidth0:   20,\n\t\tcolWidth1:   51,\n\t\ttableHeight: 15,\n\t}\n\n\tpropTable.propertyMap = propTable.buildPropertyMap()\n\n\treturn propTable\n}\n\nfunc (propTable *groupInfoTable) buildPropertyMap() map[string]string {\n\tpropMap := map[string]string{}\n\n\tg := propTable.group\n\tif g == nil {\n\t\treturn propMap\n\t}\n\tpropMap[\"1. First Seen\"] = g.CreatedAt\n\tpropMap[\"2. Last Seen\"] = g.LastNoticeAt\n\tpropMap[\"3. Occurrences\"] = strconv.Itoa(int(g.NoticeCount))\n\tpropMap[\"4. Environment\"] = g.Context.Environment\n\tpropMap[\"5. Severity\"] = g.Context.Severity\n\tpropMap[\"6. Muted\"] = fmt.Sprintf(\"%v\", g.Muted)\n\tpropMap[\"7. File\"] = g.File()\n\n\treturn propMap\n}\n\nfunc (propTable *groupInfoTable) render() string {\n\ttbl := view.NewInfoTable(\n\t\t[]string{\"Property\", \"Value\"},\n\t\tpropTable.propertyMap,\n\t\tpropTable.colWidth0,\n\t\tpropTable.colWidth1,\n\t\tpropTable.tableHeight,\n\t)\n\n\treturn tbl.Render()\n}\n"
  },
  {
    "path": "modules/airbrake/keyboard.go",
    "content": "package airbrake\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"o\", widget.openGroup, \"Open group in browser\")\n\twidget.SetKeyboardChar(\"s\", widget.resolveGroup, \"Resolve group\")\n\twidget.SetKeyboardChar(\"m\", widget.muteGroup, \"Mute group\")\n\twidget.SetKeyboardChar(\"u\", widget.unmuteGroup, \"Unmute group\")\n\twidget.SetKeyboardChar(\"t\", widget.toggleDisplayText, \"Toggle between title and compare views\")\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.viewGroup, \"View group\")\n}\n"
  },
  {
    "path": "modules/airbrake/result_table.go",
    "content": "package airbrake\n\nimport (\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype resultTable struct {\n\tpropertyMap map[string]string\n\n\tcolWidth0   int\n\tcolWidth1   int\n\ttableHeight int\n}\n\nfunc newResultTable(result, message string) *resultTable {\n\tpropTable := &resultTable{\n\t\tcolWidth0:   20,\n\t\tcolWidth1:   51,\n\t\ttableHeight: 15,\n\t}\n\tpropTable.propertyMap = map[string]string{result: message}\n\n\treturn propTable\n}\n\nfunc (propTable *resultTable) render() string {\n\ttbl := view.NewInfoTable(\n\t\t[]string{\"Result\", \"Message\"},\n\t\tpropTable.propertyMap,\n\t\tpropTable.colWidth0,\n\t\tpropTable.colWidth1,\n\t\tpropTable.tableHeight,\n\t)\n\n\treturn tbl.Render() + utils.CenterText(\"Esc to close\", 80)\n}\n"
  },
  {
    "path": "modules/airbrake/settings.go",
    "content": "package airbrake\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Airbrake\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tprojectID int    `help:\"The id of your Airbrake project.\"`\n\tauthToken string `help:\"The token that allows accessing Airbrake API\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle,\n\t\t\tdefaultFocusable, ymlConfig, globalConfig),\n\t\tprojectID: ymlConfig.UInt(\"projectID\", getProjectID()),\n\t\tauthToken: ymlConfig.UString(\"authToken\", os.Getenv(\"AIRBRAKE_USER_KEY\")),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.authToken).Load()\n\n\treturn &settings\n}\n\nfunc getProjectID() int {\n\tprojectID, err := strconv.ParseInt(os.Getenv(\"AIRBRAKE_PROJECT_ID\"), 10, 32)\n\tif err != nil {\n\t\treturn 0\n\t}\n\n\treturn int(projectID)\n}\n"
  },
  {
    "path": "modules/airbrake/util.go",
    "content": "package airbrake\n\nfunc reverseString(s string) string {\n\tr := []rune(s)\n\tfor i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {\n\t\tr[i], r[j] = r[j], r[i]\n\t}\n\treturn string(r)\n}\n"
  },
  {
    "path": "modules/airbrake/widget.go",
    "content": "package airbrake\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst module = \"Airbrake\"\n\nvar emojis = map[string]string{\n\t\"bug\":             \"🐛\",\n\t\"bell with slash\": \"🔕\",\n}\n\ntype ShowType int\n\nconst (\n\tSHOW_TITLE ShowType = iota\n\tSHOW_COMPARE\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tsettings *Settings\n\tapp      *tview.Application\n\tpages    *tview.Pages\n\n\tgroups  []Group\n\tproject *Project\n\n\tshowType ShowType\n\terr      error\n}\n\ntype GroupJSON struct {\n\tGroups []Group `json:\"groups\"`\n}\n\ntype Group struct {\n\tID           string `json:\"id\"`\n\tProjectID    int64  `json:\"projectId\"`\n\tErrors       []Error\n\tNoticeCount  int64  `json:\"noticeCount\"`\n\tCreatedAt    string `json:\"createdAt\"`\n\tLastNoticeAt string `json:\"lastNoticeAt\"`\n\tContext      GroupContext\n\tMuted        bool  `json:\"muted\"`\n\tCommentCount int64 `json:\"commentCount\"`\n}\n\ntype GroupContext struct {\n\tEnvironment string `json:\"environment\"`\n\tSeverity    string `json:\"severity\"`\n}\n\nfunc (g *Group) Link() string {\n\treturn fmt.Sprintf(\"https://airbrake.io/projects/%d/groups/%s\", g.ProjectID, g.ID)\n}\n\nfunc (g *Group) Title() string {\n\treturn fmt.Sprintf(\"%s: %s\", g.Type(), g.Message())\n}\n\nfunc (g *Group) Type() string {\n\treturn g.Errors[0].Type\n}\n\nfunc (g *Group) Message() string {\n\terr := g.Errors[0]\n\treturn strings.ReplaceAll(err.Message, \"\\n\", \". \")\n}\n\nfunc (g *Group) File() string {\n\ts := fmt.Sprintf(\"%s:%d\", g.Errors[0].Backtrace[0].File,\n\t\tg.Errors[0].Backtrace[0].Line)\n\treturn reverseString(utils.Truncate(reverseString(s), 51, true))\n}\n\ntype Error struct {\n\tType      string       `json:\"type\"`\n\tMessage   string       `json:\"message\"`\n\tBacktrace []StackFrame `json:\"backtrace\"`\n}\n\ntype StackFrame struct {\n\tFile     string `json:\"file\"`\n\tFunction string `json:\"function\"`\n\tLine     int64  `json:\"line\"`\n}\n\ntype ProjectJSON struct {\n\tProject Project `json:\"project\"`\n}\n\ntype Project struct {\n\tName string `json:\"name\"`\n}\n\nfunc rotateShowType(showtype ShowType) ShowType {\n\treturnValue := SHOW_TITLE\n\tswitch showtype {\n\tcase SHOW_TITLE:\n\t\treturnValue = SHOW_COMPARE\n\tcase SHOW_COMPARE:\n\t\treturnValue = SHOW_TITLE\n\t}\n\treturn returnValue\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tapp:      tviewApp,\n\t\tsettings: settings,\n\t\tpages:    pages,\n\t\tshowType: SHOW_TITLE,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\tgroups, err := groups(\n\t\twidget.settings.projectID,\n\t\twidget.settings.authToken)\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.groups = nil\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\twidget.err = nil\n\t\twidget.groups = groups\n\t\twidget.SetItemCount(len(groups))\n\t}\n\n\tproject, err := project(\n\t\twidget.settings.projectID,\n\t\twidget.settings.authToken)\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.project = nil\n\t} else {\n\t\twidget.err = nil\n\t\twidget.project = project\n\t}\n\n\twidget.Render()\n}\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tif widget.err != nil {\n\t\treturn module, widget.err.Error(), true\n\t}\n\n\tproject := widget.project\n\tif project != nil && project.Name == \"\" {\n\t\treturn module, \"No project found\", true\n\t}\n\n\ttitle := fmt.Sprintf(\"%s %s - %s's recent errors\", emojis[\"bug\"],\n\t\tmodule, project.Name)\n\n\tresult := widget.groups\n\tif result == nil || len(widget.groups) == 0 {\n\t\treturn title, \"All your errors are resolved!\", false\n\t}\n\n\tvar str string\n\tfor idx, g := range widget.groups {\n\t\trowColor := widget.RowColor(idx)\n\t\tvar row string\n\n\t\tif widget.showType == SHOW_TITLE {\n\t\t\tvar buf bytes.Buffer\n\t\t\tif g.Muted {\n\t\t\t\tbuf.WriteString(emojis[\"bell with slash\"])\n\t\t\t} else {\n\t\t\t\tbuf.WriteString(\"  \")\n\t\t\t}\n\t\t\tbuf.WriteString(\" \" + g.Title())\n\t\t\trow = fmt.Sprintf(\"[%s]%2d. %s[white]\", rowColor, idx+1, buf.String())\n\t\t} else {\n\t\t\trow = fmt.Sprintf(\n\t\t\t\t\"[%s]%2d. %-31s %-11s  %-10s  count: %-9d  comments: %-2d[white]\",\n\t\t\t\trowColor, idx+1, utils.Truncate(g.Type(), 30, true),\n\t\t\t\tg.Context.Environment, g.Context.Severity,\n\t\t\t\tg.NoticeCount, g.CommentCount)\n\t\t}\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(g.Type()))\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) openGroup() {\n\tsel := widget.GetSelected()\n\n\tif sel >= 0 && widget.groups != nil && sel < len(widget.groups) {\n\t\tgroup := widget.groups[sel]\n\t\tutils.OpenFile(group.Link())\n\t}\n}\n\nfunc (widget *Widget) viewGroup() {\n\tsel := widget.GetSelected()\n\n\tif sel >= 0 && widget.groups != nil && sel < len(widget.groups) {\n\t\tgroup := widget.groups[sel]\n\n\t\tcloseFunc := func() {\n\t\t\twidget.pages.RemovePage(\"group info\")\n\t\t\twidget.app.SetFocus(widget.View)\n\t\t}\n\n\t\ttable := newGroupInfoTable(&group).render()\n\t\ttable += utils.CenterText(\"Esc to close\", 80)\n\n\t\tmodal := view.NewBillboardModal(table, closeFunc)\n\t\tmodal.SetTitle(fmt.Sprintf(\" %s \", group.Title()))\n\n\t\twidget.pages.AddPage(\"group info\", modal, false, true)\n\t\twidget.app.SetFocus(modal)\n\n\t\twidget.app.QueueUpdateDraw(func() {\n\t\t\twidget.app.Draw()\n\t\t})\n\t}\n}\n\nfunc (widget *Widget) resolveGroup() {\n\tsel := widget.GetSelected()\n\n\tif sel >= 0 && widget.groups != nil && sel < len(widget.groups) {\n\t\tgroup := widget.groups[sel]\n\n\t\tcloseFunc := func() {\n\t\t\twidget.pages.RemovePage(\"resolve\")\n\t\t\twidget.app.SetFocus(widget.View)\n\t\t}\n\n\t\tvar tbl *resultTable\n\t\terr := resolveGroup(group.ProjectID, group.ID, widget.settings.authToken)\n\t\tif err == nil {\n\t\t\ttbl = newResultTable(\"Success\", \"Error Resolved\")\n\t\t\twidget.Refresh()\n\t\t} else {\n\t\t\ttbl = newResultTable(\"Error\", err.Error())\n\t\t}\n\n\t\tmodal := view.NewBillboardModal(tbl.render(), closeFunc)\n\t\tmodal.SetTitle(fmt.Sprintf(\" %s \", group.Title()))\n\n\t\twidget.pages.AddPage(\"resolve\", modal, false, true)\n\t\twidget.app.SetFocus(modal)\n\n\t\twidget.app.QueueUpdateDraw(func() {\n\t\t\twidget.app.Draw()\n\t\t})\n\t}\n}\n\nfunc (widget *Widget) muteGroup() {\n\tif widget.showType != SHOW_TITLE {\n\t\treturn\n\t}\n\n\tsel := widget.GetSelected()\n\n\tif sel >= 0 && widget.groups != nil && sel < len(widget.groups) {\n\t\tgroup := widget.groups[sel]\n\t\tif !group.Muted {\n\t\t\twidget.err = muteGroup(group.ProjectID, group.ID, widget.settings.authToken)\n\t\t\twidget.Refresh()\n\t\t}\n\t}\n}\n\nfunc (widget *Widget) unmuteGroup() {\n\tif widget.showType != SHOW_TITLE {\n\t\treturn\n\t}\n\n\tsel := widget.GetSelected()\n\n\tif sel >= 0 && widget.groups != nil && sel < len(widget.groups) {\n\t\tgroup := widget.groups[sel]\n\t\tif group.Muted {\n\t\t\twidget.err = unmuteGroup(group.ProjectID, group.ID, widget.settings.authToken)\n\t\t\twidget.Refresh()\n\t\t}\n\t}\n}\n\nfunc (widget *Widget) toggleDisplayText() {\n\twidget.showType = rotateShowType(widget.showType)\n\twidget.Render()\n}\n"
  },
  {
    "path": "modules/asana/client.go",
    "content": "package asana\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\tasana \"bitbucket.org/mikehouston/asana-go\"\n)\n\nfunc fetchTasksFromProject(token, projectId, mode string) ([]*TaskItem, error) {\n\ttaskItems := []*TaskItem{}\n\tuidToName := make(map[string]string)\n\n\tclient := asana.NewClientWithAccessToken(token)\n\n\tuid, err := getCurrentUserId(client, mode)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tq := &asana.TaskQuery{\n\t\tProject: projectId,\n\t}\n\n\tfetchedTasks, _, err := getTasksFromAsana(client, q)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error fetching tasks: %s\", err)\n\t}\n\n\tprocessFetchedTasks(client, &fetchedTasks, &taskItems, &uidToName, mode, projectId, uid)\n\n\treturn taskItems, nil\n}\n\nfunc fetchTasksFromProjectSections(token, projectId string, sections []string, mode string) ([]*TaskItem, error) {\n\ttaskItems := []*TaskItem{}\n\tuidToName := make(map[string]string)\n\n\tclient := asana.NewClientWithAccessToken(token)\n\n\tuid, err := getCurrentUserId(client, mode)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tp := &asana.Project{\n\t\tID: projectId,\n\t}\n\n\tfor _, section := range sections {\n\n\t\tsectionId, err := findSection(client, p, section)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error fetching tasks: %s\", err)\n\t\t}\n\n\t\tq := &asana.TaskQuery{\n\t\t\tSection: sectionId,\n\t\t}\n\n\t\tfetchedTasks, _, err := getTasksFromAsana(client, q)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error fetching tasks: %s\", err)\n\t\t}\n\n\t\tif len(fetchedTasks) > 0 {\n\t\t\ttaskItem := &TaskItem{\n\t\t\t\tname:     section,\n\t\t\t\ttaskType: TASK_SECTION,\n\t\t\t}\n\n\t\t\ttaskItems = append(taskItems, taskItem)\n\t\t}\n\n\t\tprocessFetchedTasks(client, &fetchedTasks, &taskItems, &uidToName, mode, projectId, uid)\n\n\t}\n\n\treturn taskItems, nil\n}\n\nfunc fetchTasksFromWorkspace(token, workspaceId, mode string) ([]*TaskItem, error) {\n\ttaskItems := []*TaskItem{}\n\tuidToName := make(map[string]string)\n\n\tclient := asana.NewClientWithAccessToken(token)\n\n\tuid, err := getCurrentUserId(client, mode)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tq := &asana.TaskQuery{\n\t\tWorkspace: workspaceId,\n\t\tAssignee:  \"me\",\n\t}\n\n\tfetchedTasks, _, err := getTasksFromAsana(client, q)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error fetching tasks: %s\", err)\n\t}\n\n\tprocessFetchedTasks(client, &fetchedTasks, &taskItems, &uidToName, mode, workspaceId, uid)\n\n\treturn taskItems, nil\n\n}\n\nfunc toggleTaskCompletionById(token, taskId string) error {\n\tclient := asana.NewClientWithAccessToken(token)\n\n\tt := &asana.Task{\n\t\tID: taskId,\n\t}\n\n\terr := t.Fetch(client)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error fetching task: %s\", err)\n\t}\n\n\tupdateReq := &asana.UpdateTaskRequest{}\n\n\tif *t.Completed {\n\t\tf := false\n\t\tupdateReq.Completed = &f\n\t} else {\n\t\tt := true\n\t\tupdateReq.Completed = &t\n\t}\n\n\terr = t.Update(client, updateReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error updating task: %s\", err)\n\t}\n\n\treturn nil\n}\n\nfunc processFetchedTasks(client *asana.Client, fetchedTasks *[]*asana.Task, taskItems *[]*TaskItem, uidToName *map[string]string, mode, projectId, uid string) {\n\n\tfor _, task := range *fetchedTasks {\n\t\tswitch {\n\t\tcase strings.HasSuffix(mode, \"_all\"):\n\t\t\tif task.Assignee != nil {\n\t\t\t\t// Check if we have already looked up this user\n\t\t\t\tif assigneeName, ok := (*uidToName)[task.Assignee.ID]; ok {\n\t\t\t\t\ttask.Assignee.Name = assigneeName\n\t\t\t\t} else {\n\t\t\t\t\t// We haven't looked up this user before, perform the lookup now\n\t\t\t\t\tassigneeName, err := getOtherUserEmail(client, task.Assignee.ID)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\ttask.Assignee.Name = \"Error\"\n\t\t\t\t\t}\n\t\t\t\t\t(*uidToName)[task.Assignee.ID] = assigneeName\n\t\t\t\t\ttask.Assignee.Name = assigneeName\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttask.Assignee = &asana.User{\n\t\t\t\t\tName: \"Unassigned\",\n\t\t\t\t}\n\t\t\t}\n\t\t\ttaskItem := buildTaskItem(task, projectId)\n\t\t\t(*taskItems) = append((*taskItems), taskItem)\n\t\tcase !strings.HasSuffix(mode, \"_all\") && task.Assignee != nil && task.Assignee.ID == uid:\n\t\t\ttaskItem := buildTaskItem(task, projectId)\n\t\t\t(*taskItems) = append((*taskItems), taskItem)\n\t\t}\n\t}\n}\n\nfunc buildTaskItem(task *asana.Task, projectId string) *TaskItem {\n\tdueOnString := \"\"\n\tif task.DueOn != nil {\n\t\tdueOn := time.Time(*task.DueOn)\n\t\tcurrentYear, _, _ := time.Now().Date()\n\t\tif currentYear != dueOn.Year() {\n\t\t\tdueOnString = dueOn.Format(\"Jan 2 2006\")\n\t\t} else {\n\t\t\tdueOnString = dueOn.Format(\"Jan 2\")\n\t\t}\n\t}\n\n\tassignString := \"\"\n\tif task.Assignee != nil {\n\t\tassignString = task.Assignee.Name\n\t}\n\n\ttaskItem := &TaskItem{\n\t\tname:        task.Name,\n\t\tid:          task.ID,\n\t\tnumSubtasks: task.NumSubtasks,\n\t\tdueOn:       dueOnString,\n\t\turl:         fmt.Sprintf(\"https://app.asana.com/0/%s/%s/f\", projectId, task.ID),\n\t\ttaskType:    TASK_TYPE,\n\t\tcompleted:   *task.Completed,\n\t\tassignee:    assignString,\n\t}\n\n\treturn taskItem\n\n}\n\nfunc getOtherUserEmail(client *asana.Client, uid string) (string, error) {\n\tif uid == \"\" {\n\t\treturn \"\", fmt.Errorf(\"missing uid\")\n\t}\n\n\tu := &asana.User{\n\t\tID: uid,\n\t}\n\n\terr := u.Fetch(client, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error fetching user: %s\", err)\n\t}\n\n\treturn u.Email, nil\n}\n\nfunc getCurrentUserId(client *asana.Client, mode string) (string, error) {\n\tif strings.HasSuffix(mode, \"_all\") {\n\t\treturn \"\", nil\n\t}\n\tu, err := client.CurrentUser()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error getting current user: %s\", err)\n\t}\n\n\treturn u.ID, nil\n}\n\nfunc findSection(client *asana.Client, project *asana.Project, sectionName string) (string, error) {\n\tsectionId := \"\"\n\n\tsections, _, err := project.Sections(client, &asana.Options{\n\t\tLimit: 100,\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error getting sections: %s\", err)\n\t}\n\n\tfor _, section := range sections {\n\t\tif section.Name == sectionName {\n\t\t\tsectionId = section.ID\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif sectionId == \"\" {\n\t\treturn \"\", fmt.Errorf(\"we didn't find the section %s\", sectionName)\n\t}\n\n\treturn sectionId, nil\n}\n\nfunc getTasksFromAsana(client *asana.Client, q *asana.TaskQuery) ([]*asana.Task, bool, error) {\n\tmoreTasks := false\n\n\ttasks, np, err := client.QueryTasks(q, &asana.Options{\n\t\tLimit: 100,\n\t\tFields: []string{\n\t\t\t\"assignee\",\n\t\t\t\"name\",\n\t\t\t\"num_subtasks\",\n\t\t\t\"due_on\",\n\t\t\t\"completed\",\n\t\t},\n\t})\n\n\tif err != nil {\n\t\treturn nil, false, fmt.Errorf(\"error querying tasks: %s\", err)\n\t}\n\n\tif np != nil {\n\t\tmoreTasks = true\n\t}\n\n\treturn tasks, moreTasks, nil\n}\n"
  },
  {
    "path": "modules/asana/keyboard.go",
    "content": "package asana\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next task\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous task\")\n\twidget.SetKeyboardChar(\"q\", widget.Unselect, \"Unselect task\")\n\twidget.SetKeyboardChar(\"o\", widget.openTask, \"Open task in browser\")\n\twidget.SetKeyboardChar(\"x\", widget.toggleTaskCompletion, \"Toggles the task's completion state\")\n\twidget.SetKeyboardChar(\"?\", widget.ShowHelp, \"Shows help\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next task\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous task\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Unselect task\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openTask, \"Open task in browser\")\n}\n"
  },
  {
    "path": "modules/asana/settings.go",
    "content": "package asana\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Asana\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tprojectId string `help:\"The Asana Project ID. If the mode is 'project' or 'project_sections' this is required to known which Asana Project to pull your tasks from\" values:\"A valid Asana Project ID string\" optional:\"true\"`\n\n\tworkspaceId string `help:\"The Asana Workspace ID. If mode is 'workspace' this is required\" values:\"A valid Asana Workspace ID string\" optional:\"true\"`\n\n\tsections []string `help:\"The Asana Section Labels to fetch from the Project. Required if the mode is 'project_sections'\" values:\"An array of Asana Section Label strings\" optional:\"true\"`\n\n\tallUsers bool `help:\"Fetch tasks for all users, defaults to false\" values:\"bool\" optional:\"true\"`\n\n\tmode string `help:\"What mode to query Asana, 'project', 'project_sections', 'workspace'\" values:\"A string with either 'project', 'project_sections' or 'workspace'\"`\n\n\thideComplete bool `help:\"Hide completed tasks, defaults to false\" values:\"bool\" optional:\"true\"`\n\n\tapiKey string `help:\"Your Asana Personal Access Token. Leave this blank to use the WTF_ASANA_TOKEN environment variable.\" values:\"Your Asana Personal Access Token as a string\" optional:\"true\"`\n\n\ttoken string\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig,\n\t\t\tglobalConfig),\n\t\tprojectId:    ymlConfig.UString(\"projectId\", \"\"),\n\t\tapiKey:       ymlConfig.UString(\"apiKey\", \"\"),\n\t\tworkspaceId:  ymlConfig.UString(\"workspaceId\", \"\"),\n\t\tsections:     utils.ToStrs(ymlConfig.UList(\"sections\")),\n\t\tallUsers:     ymlConfig.UBool(\"allUsers\", false),\n\t\tmode:         ymlConfig.UString(\"mode\", \"\"),\n\t\thideComplete: ymlConfig.UBool(\"hideComplete\", false),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/asana/widget.go",
    "content": "package asana\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype TaskType int\n\nconst (\n\tTASK_TYPE TaskType = iota\n\tTASK_SECTION\n\tTASK_BREAK\n)\n\ntype TaskItem struct {\n\tname        string\n\tnumSubtasks int32\n\tdueOn       string\n\tid          string\n\turl         string\n\ttaskType    TaskType\n\tcompleted   bool\n\tassignee    string\n}\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\ttasks []*TaskItem\n\n\tmu       sync.Mutex\n\terr      error\n\tsettings *Settings\n\ttviewApp *tview.Application\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\ttviewApp: tviewApp,\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\twidget.tasks = nil\n\twidget.err = nil\n\twidget.SetItemCount(0)\n\n\twidget.mu.Lock()\n\tdefer widget.mu.Unlock()\n\ttasks, err := widget.Fetch(\n\t\twidget.settings.workspaceId,\n\t\twidget.settings.projectId,\n\t\twidget.settings.mode,\n\t\twidget.settings.sections,\n\t\twidget.settings.allUsers,\n\t)\n\tif err != nil {\n\t\twidget.err = err\n\t} else {\n\t\twidget.tasks = tasks\n\t\twidget.SetItemCount(len(tasks))\n\t}\n\n\twidget.Render()\n}\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) Fetch(workspaceId, projectId, mode string, sections []string, allUsers bool) ([]*TaskItem, error) {\n\n\tavailableModes := map[string]interface{}{\n\t\t\"project\":          nil,\n\t\t\"project_sections\": nil,\n\t\t\"workspace\":        nil,\n\t}\n\n\tif _, ok := availableModes[mode]; !ok {\n\t\treturn nil, fmt.Errorf(\"missing mode, or mode is invalid - please set to project, project_sections or workspace\")\n\t}\n\n\tif widget.settings.apiKey != \"\" {\n\t\twidget.settings.token = widget.settings.apiKey\n\t} else {\n\t\twidget.settings.token = os.Getenv(\"WTF_ASANA_TOKEN\")\n\t}\n\n\tif widget.settings.token == \"\" {\n\t\treturn nil, fmt.Errorf(\"missing environment variable token or apikey config\")\n\t}\n\n\tsubMode := mode\n\tif allUsers && mode != \"workspace\" {\n\t\tsubMode += \"_all\"\n\t}\n\n\tif projectId == \"\" && strings.HasPrefix(subMode, \"project\") {\n\t\treturn nil, fmt.Errorf(\"missing project id\")\n\t}\n\n\tif workspaceId == \"\" && subMode == \"workspace\" {\n\t\treturn nil, fmt.Errorf(\"missing workspace id\")\n\t}\n\n\tvar tasks []*TaskItem\n\tvar err error\n\n\t//lint:ignore QF1002 An untagged switch makes sense here.\n\tswitch {\n\tcase strings.HasPrefix(subMode, \"project_sections\"):\n\t\ttasks, err = fetchTasksFromProjectSections(widget.settings.token, projectId, sections, subMode)\n\tcase strings.HasPrefix(subMode, \"project\"):\n\t\ttasks, err = fetchTasksFromProject(widget.settings.token, projectId, subMode)\n\tcase subMode == \"workspace\":\n\t\ttasks, err = fetchTasksFromWorkspace(widget.settings.token, workspaceId, subMode)\n\tdefault:\n\t\terr = fmt.Errorf(\"no mode found\")\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn tasks, nil\n\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\n\ttitle := widget.CommonSettings().Title\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tdata := widget.tasks\n\tif len(data) == 0 {\n\t\treturn title, \"No data\", false\n\t}\n\n\tvar str string\n\n\tfor idx, taskItem := range data {\n\t\tswitch taskItem.taskType {\n\t\tcase TASK_TYPE:\n\t\t\tif widget.settings.hideComplete && taskItem.completed {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\trowColor := widget.RowColor(idx)\n\n\t\t\tcompleted := \"[ []\"\n\t\t\tif taskItem.completed {\n\t\t\t\tcompleted = \"[x[]\"\n\t\t\t}\n\n\t\t\trow := \"\"\n\n\t\t\tif widget.settings.allUsers && taskItem.assignee != \"\" {\n\t\t\t\trow = fmt.Sprintf(\n\t\t\t\t\t\"[%s]  %s %s: %s\",\n\t\t\t\t\trowColor,\n\t\t\t\t\tcompleted,\n\t\t\t\t\ttaskItem.assignee,\n\t\t\t\t\ttaskItem.name,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\trow = fmt.Sprintf(\n\t\t\t\t\t\"[%s]  %s %s\",\n\t\t\t\t\trowColor,\n\t\t\t\t\tcompleted,\n\t\t\t\t\ttaskItem.name,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tif taskItem.numSubtasks > 0 {\n\t\t\t\trow += fmt.Sprintf(\" (%d)\", taskItem.numSubtasks)\n\t\t\t}\n\n\t\t\tif taskItem.dueOn != \"\" {\n\t\t\t\trow += fmt.Sprintf(\" due: %s\", taskItem.dueOn)\n\t\t\t}\n\n\t\t\trow += \" [white]\"\n\n\t\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(taskItem.name))\n\n\t\tcase TASK_SECTION:\n\t\t\tif idx > 1 {\n\t\t\t\trow := \"[white] \"\n\n\t\t\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(taskItem.name))\n\t\t\t}\n\t\t\trow := fmt.Sprintf(\n\t\t\t\t\"[white] %s [white]\",\n\t\t\t\ttaskItem.name,\n\t\t\t)\n\n\t\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(taskItem.name))\n\n\t\t\trow = \"[white] \"\n\n\t\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(taskItem.name))\n\n\t\t}\n\n\t}\n\n\treturn title, str, false\n\n}\n\nfunc (widget *Widget) openTask() {\n\tsel := widget.GetSelected()\n\n\tif sel >= 0 && widget.tasks != nil && sel < len(widget.tasks) {\n\t\ttask := widget.tasks[sel]\n\t\tif task.taskType == TASK_TYPE && task.url != \"\" {\n\t\t\tutils.OpenFile(task.url)\n\t\t}\n\t}\n}\n\nfunc (widget *Widget) toggleTaskCompletion() {\n\tsel := widget.GetSelected()\n\n\tif sel >= 0 && widget.tasks != nil && sel < len(widget.tasks) {\n\t\ttask := widget.tasks[sel]\n\t\tif task.taskType == TASK_TYPE {\n\t\t\twidget.mu.Lock()\n\n\t\t\terr := toggleTaskCompletionById(widget.settings.token, task.id)\n\t\t\tif err != nil {\n\t\t\t\twidget.err = err\n\t\t\t}\n\n\t\t\twidget.mu.Unlock()\n\t\t\twidget.Refresh()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "modules/azuredevops/client.go",
    "content": "package azuredevops\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tazrBuild \"github.com/microsoft/azure-devops-go-api/azuredevops/build\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc (widget *Widget) getBuildStats() string {\n\tprojName := widget.settings.projectName\n\tstatusFilter := azrBuild.BuildStatusValues.All\n\ttop := widget.settings.maxRows\n\tbuilds, err := widget.cli.GetBuilds(widget.ctx, azrBuild.GetBuildsArgs{Project: &projName, StatusFilter: &statusFilter, Top: &top})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not get builds\").Error()\n\t}\n\n\tresult := \"\"\n\tfor _, build := range builds.Value {\n\t\tnum := *build.BuildNumber\n\t\tbranch := *build.SourceBranch\n\t\treason := *build.Reason\n\t\ttriggers := *build.TriggerInfo\n\t\tif reason == azrBuild.BuildReasonValues.PullRequest {\n\t\t\tbranch = triggers[\"pr.sourceBranch\"]\n\t\t}\n\t\tbranch = strings.TrimPrefix(branch, \"refs/heads/\")\n\t\tstatus := *build.Status\n\t\tstatusDisplay := \"[white:grey]unknown\"\n\n\t\tswitch status {\n\t\tcase azrBuild.BuildStatusValues.InProgress:\n\t\t\tstatusDisplay = \"[white:blue]in progress\"\n\t\tcase azrBuild.BuildStatusValues.Cancelling:\n\t\t\tstatusDisplay = \"[white:orange]in cancelling\"\n\t\tcase azrBuild.BuildStatusValues.Postponed, azrBuild.BuildStatusValues.NotStarted:\n\t\t\tstatusDisplay = \"[white:blue]waiting\"\n\t\tcase azrBuild.BuildStatusValues.Completed:\n\n\t\t\tbuildResult := *build.Result\n\n\t\t\tswitch buildResult {\n\t\t\tcase azrBuild.BuildResultValues.Succeeded:\n\t\t\t\tstatusDisplay = \"[white:green]succeeded\"\n\t\t\tcase azrBuild.BuildResultValues.Failed:\n\t\t\t\tstatusDisplay = \"[white:red]failed\"\n\t\t\tcase azrBuild.BuildResultValues.Canceled:\n\t\t\t\tstatusDisplay = \"[white:darkgrey]cancelled\"\n\t\t\tcase azrBuild.BuildResultValues.PartiallySucceeded:\n\t\t\t\tstatusDisplay = \"[white:magenta]partially\"\n\t\t\t}\n\t\t}\n\n\t\tresult += fmt.Sprintf(\"%s[-:-:-] #%s %s (%s) \\n\", statusDisplay, num, branch, reason)\n\t}\n\n\tif result == \"\" {\n\t\tresult = \"no builds found\"\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "modules/azuredevops/example-conf.yml",
    "content": "wtf:\n  colors:\n    # background: black\n    # foreground: blue\n    border:\n      focusable: darkslateblue\n      focused: orange\n      normal: gray\n    checked: yellow\n    highlight: \n      fore: black\n      back: gray\n    rows:\n      even: yellow\n      odd: white\n  grid:\n    # How _wide_ the columns are, in terminal characters. In this case we have\n    # four columns, each of which are 35 characters wide.\n    # columns: [50, ]\n    # How _high_ the rows are, in terminal lines. In this case we have four rows\n    # that support ten line of text and one of four.\n    # rows: [50]\n  refreshInterval: 1\n  openFileUtil: \"open\"\n  mods:\n    azuredevops:\n      type: azuredevops\n      title: \"💻\"\n      enabled: true\n      position:\n        top: 0\n        left: 0\n        height: 3\n        width: 3\n      refreshInterval: 1\n      labelColor: lightblue # title label color (optional / default: white)\n      apiToken: \"mysecret api token\" # api key (required)\n      orgUrl: \"https://dev.azure.com/myawesomecompany/\" # url to your azure devops project (required)\n      prjectName: \"the awesome project\"  # name of your project (required)\n      maxRows: 3 #max rows to show (optional / default 3)\n\n  "
  },
  {
    "path": "modules/azuredevops/settings.go",
    "content": "package azuredevops\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocus = false\n\tdefaultTitle = \"azuredevops\"\n)\n\n// Settings defines the configuration options for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tapiToken    string `help:\"Your Azure DevOps Access Token.\"`\n\tlabelColor  string\n\tmaxRows     int\n\torgURL      string `help:\"Your Azure DevOps organization URL.\"`\n\tprojectName string\n}\n\n// NewSettingsFromYAML creates and returns an instance of Settings with configuration options populated\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocus, ymlConfig, globalConfig),\n\n\t\tapiToken:    ymlConfig.UString(\"apiToken\", os.Getenv(\"WTF_AZURE_DEVOPS_API_TOKEN\")),\n\t\tlabelColor:  ymlConfig.UString(\"labelColor\", \"white\"),\n\t\tmaxRows:     ymlConfig.UInt(\"maxRows\", 3),\n\t\torgURL:      ymlConfig.UString(\"orgURL\", os.Getenv(\"WTF_AZURE_DEVOPS_ORG_URL\")),\n\t\tprojectName: ymlConfig.UString(\"projectName\", os.Getenv(\"WTF_AZURE_DEVOPS_PROJECT_NAME\")),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiToken).\n\t\tService(settings.orgURL).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/azuredevops/widget.go",
    "content": "package azuredevops\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\tazr \"github.com/microsoft/azure-devops-go-api/azuredevops\"\n\tazrBuild \"github.com/microsoft/azure-devops-go-api/azuredevops/build\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\tcli           azrBuild.Client\n\tsettings      *Settings\n\tdisplayBuffer string\n\tctx           context.Context\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\t\tsettings:   settings,\n\t}\n\n\twidget.View.SetScrollable(true)\n\tconnection := azr.NewPatConnection(settings.orgURL, settings.apiToken)\n\tctx := context.Background()\n\n\tcli, err := azrBuild.NewClient(ctx, connection)\n\tif err != nil {\n\t\twidget.displayBuffer = errors.Wrap(err, \"could not create client 2\").Error()\n\t} else {\n\t\twidget.cli = cli\n\t\twidget.ctx = ctx\n\t}\n\n\twidget.refreshDisplayBuffer()\n\n\treturn &widget\n}\n\nfunc (widget *Widget) Refresh() {\n\twidget.refreshDisplayBuffer()\n\twidget.Redraw(widget.display)\n}\n\nfunc (widget *Widget) display() (string, string, bool) {\n\treturn widget.CommonSettings().Title, widget.displayBuffer, true\n}\n\nfunc (widget *Widget) refreshDisplayBuffer() {\n\tif widget.cli == nil {\n\t\treturn\n\t}\n\n\twidget.displayBuffer = \"\"\n\n\twidget.displayBuffer += fmt.Sprintf(\"[%s::bul] build status - %s\\n\",\n\t\twidget.settings.labelColor,\n\t\twidget.settings.projectName)\n\n\twidget.displayBuffer += widget.getBuildStats()\n}\n"
  },
  {
    "path": "modules/azurelogs/config.go",
    "content": "package azurelogs\n\nimport (\n\t_ \"embed\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// QueryFile represents the structure of a query configuration file\ntype QueryFile struct {\n\tTitle          string   `yaml:\"title\"`                 // Display title for the query\n\tSubscriptionID string   `yaml:\"azure_subscription_id\"` // Azure subscription ID\n\tWorkspaceID    string   `yaml:\"azure_workspace_id\"`    // Log Analytics workspace ID\n\tColumns        []string `yaml:\"columns\"`               // Expected column names\n\tQuery          string   `yaml:\"query\"`                 // KQL query string\n}\n\n// readQueryFile reads and parses a query configuration file\nfunc readQueryFile(sess *Session, queryPath string) error {\n\tfile, err := os.OpenFile(queryPath, os.O_RDONLY, 0o600)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfilename := file.Name()\n\tif len(filename) > 5 && filename[len(filename)-5:] == \".yaml\" {\n\t\tvar configFile QueryFile\n\t\tconfigFile, err = readQueryFileContent(queryPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsess.QueryFile = configFile\n\t} else {\n\t\treturn fmt.Errorf(\"invalid query file format: %s, expected .yaml\", filename)\n\t}\n\n\treturn nil\n}\n\n// readQueryFileContent reads a single config file and returns a QueryFile struct\nfunc readQueryFileContent(filePath string) (QueryFile, error) {\n\tvar configFile QueryFile\n\tdata, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn configFile, fmt.Errorf(\"failed to read query config file %s: %w\", filePath, err)\n\t}\n\n\terr = yaml.Unmarshal(data, &configFile)\n\tif err != nil {\n\t\treturn configFile, fmt.Errorf(\"failed to parse YAML in config file %s: %w\", filePath, err)\n\t}\n\n\treturn configFile, nil\n}\n"
  },
  {
    "path": "modules/azurelogs/config_test.go",
    "content": "package azurelogs\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestQueryFile_Structure(t *testing.T) {\n\t// Test QueryFile structure and YAML tags\n\tqf := QueryFile{\n\t\tTitle:          \"Test Azure Query\",\n\t\tSubscriptionID: \"subscription-123\",\n\t\tWorkspaceID:    \"workspace-456\",\n\t\tColumns:        []string{\"TimeGenerated\", \"Level\", \"Message\"},\n\t\tQuery:          \"AzureActivity | where Level == 'Error' | limit 100\",\n\t}\n\n\tassert.Equal(t, \"Test Azure Query\", qf.Title)\n\tassert.Equal(t, \"subscription-123\", qf.SubscriptionID)\n\tassert.Equal(t, \"workspace-456\", qf.WorkspaceID)\n\tassert.Len(t, qf.Columns, 3)\n\tassert.Equal(t, \"TimeGenerated\", qf.Columns[0])\n\tassert.Equal(t, \"Level\", qf.Columns[1])\n\tassert.Equal(t, \"Message\", qf.Columns[2])\n\tassert.Contains(t, qf.Query, \"AzureActivity\")\n}\n\nfunc TestReadQueryFileContent_ValidYAML(t *testing.T) {\n\t// Create a temporary YAML file for testing\n\tyamlContent := `title: \"Test Query\"\nazure_subscription_id: \"test-sub-123\"\nazure_workspace_id: \"test-workspace-456\"\ncolumns:\n  - \"TimeGenerated\"\n  - \"Level\"\n  - \"Message\"\nquery: \"AzureActivity | limit 10\"`\n\n\ttmpFile, err := os.CreateTemp(\"\", \"test-query-*.yaml\")\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpFile.Name())\n\n\t_, err = tmpFile.WriteString(yamlContent)\n\trequire.NoError(t, err)\n\trequire.NoError(t, tmpFile.Close())\n\n\t// Test reading the file\n\tqueryFile, err := readQueryFileContent(tmpFile.Name())\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"Test Query\", queryFile.Title)\n\tassert.Equal(t, \"test-sub-123\", queryFile.SubscriptionID)\n\tassert.Equal(t, \"test-workspace-456\", queryFile.WorkspaceID)\n\tassert.Len(t, queryFile.Columns, 3)\n\tassert.Equal(t, \"TimeGenerated\", queryFile.Columns[0])\n\tassert.Equal(t, \"Level\", queryFile.Columns[1])\n\tassert.Equal(t, \"Message\", queryFile.Columns[2])\n\tassert.Equal(t, \"AzureActivity | limit 10\", queryFile.Query)\n}\n\nfunc TestReadQueryFileContent_InvalidYAML(t *testing.T) {\n\t// Create a temporary file with invalid YAML\n\tinvalidYamlContent := `title: \"Test Query\"\nazure_subscription_id: \"test-sub-123\"\ninvalid_yaml: [unclosed bracket`\n\n\ttmpFile, err := os.CreateTemp(\"\", \"test-invalid-*.yaml\")\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpFile.Name())\n\n\t_, err = tmpFile.WriteString(invalidYamlContent)\n\trequire.NoError(t, err)\n\trequire.NoError(t, tmpFile.Close())\n\n\t// Test reading the invalid file\n\t_, err = readQueryFileContent(tmpFile.Name())\n\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"failed to parse YAML\")\n}\n\nfunc TestReadQueryFileContent_NonexistentFile(t *testing.T) {\n\t// Test reading a file that doesn't exist\n\t_, err := readQueryFileContent(\"/nonexistent/file.yaml\")\n\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"failed to read query config file\")\n}\n\nfunc TestReadQueryFile_ValidYAMLFile(t *testing.T) {\n\t// Create a temporary YAML file for testing\n\tyamlContent := `title: \"Integration Test Query\"\nazure_subscription_id: \"integration-sub-123\"\nazure_workspace_id: \"integration-workspace-456\"\ncolumns:\n  - \"Computer\"\n  - \"TimeGenerated\"\n  - \"SourceSystem\"\nquery: \"Heartbeat | limit 5\"`\n\n\ttmpFile, err := os.CreateTemp(\"\", \"test-integration-*.yaml\")\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpFile.Name())\n\n\t_, err = tmpFile.WriteString(yamlContent)\n\trequire.NoError(t, err)\n\trequire.NoError(t, tmpFile.Close())\n\n\t// Test reading into session\n\tsess := &Session{}\n\terr = readQueryFile(sess, tmpFile.Name())\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"Integration Test Query\", sess.QueryFile.Title)\n\tassert.Equal(t, \"integration-sub-123\", sess.QueryFile.SubscriptionID)\n\tassert.Equal(t, \"integration-workspace-456\", sess.QueryFile.WorkspaceID)\n\tassert.Len(t, sess.QueryFile.Columns, 3)\n\tassert.Equal(t, \"Computer\", sess.QueryFile.Columns[0])\n\tassert.Equal(t, \"Heartbeat | limit 5\", sess.QueryFile.Query)\n}\n\nfunc TestReadQueryFile_NonYAMLFile(t *testing.T) {\n\t// Create a temporary file with non-YAML extension\n\ttmpFile, err := os.CreateTemp(\"\", \"test-non-yaml-*.txt\")\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpFile.Name())\n\n\t_, err = tmpFile.WriteString(\"some content\")\n\trequire.NoError(t, err)\n\trequire.NoError(t, tmpFile.Close())\n\n\t// Test reading non-YAML file\n\tsess := &Session{}\n\terr = readQueryFile(sess, tmpFile.Name())\n\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"invalid query file format\")\n\tassert.Contains(t, err.Error(), \"expected .yaml\")\n}\n\nfunc TestReadQueryFile_EmptyYAMLFile(t *testing.T) {\n\t// Create an empty YAML file\n\ttmpFile, err := os.CreateTemp(\"\", \"test-empty-*.yaml\")\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpFile.Name())\n\n\trequire.NoError(t, tmpFile.Close())\n\n\t// Test reading empty file\n\tsess := &Session{}\n\terr = readQueryFile(sess, tmpFile.Name())\n\n\t// Should succeed but with empty values\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"\", sess.QueryFile.Title)\n\tassert.Equal(t, \"\", sess.QueryFile.SubscriptionID)\n\tassert.Equal(t, \"\", sess.QueryFile.WorkspaceID)\n\tassert.Empty(t, sess.QueryFile.Columns)\n\tassert.Equal(t, \"\", sess.QueryFile.Query)\n}\n\nfunc TestReadQueryFile_PartialYAMLFile(t *testing.T) {\n\t// Create a YAML file with only some fields\n\tyamlContent := `title: \"Partial Query\"\nazure_subscription_id: \"partial-sub-123\"\n# Missing workspace_id, columns, and query`\n\n\ttmpFile, err := os.CreateTemp(\"\", \"test-partial-*.yaml\")\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpFile.Name())\n\n\t_, err = tmpFile.WriteString(yamlContent)\n\trequire.NoError(t, err)\n\trequire.NoError(t, tmpFile.Close())\n\n\t// Test reading partial file\n\tsess := &Session{}\n\terr = readQueryFile(sess, tmpFile.Name())\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"Partial Query\", sess.QueryFile.Title)\n\tassert.Equal(t, \"partial-sub-123\", sess.QueryFile.SubscriptionID)\n\tassert.Equal(t, \"\", sess.QueryFile.WorkspaceID) // Should be empty\n\tassert.Empty(t, sess.QueryFile.Columns)         // Should be empty\n\tassert.Equal(t, \"\", sess.QueryFile.Query)       // Should be empty\n}\n\nfunc TestQueryFile_YAMLTags(t *testing.T) {\n\t// This is a structural test to ensure YAML tags are properly defined\n\t// We test this by creating a QueryFile and checking field mapping\n\tyamlContent := `title: \"YAML Tag Test\"\nazure_subscription_id: \"yaml-sub-123\"\nazure_workspace_id: \"yaml-workspace-456\"\ncolumns:\n  - \"TestColumn1\"\n  - \"TestColumn2\"\nquery: \"TestQuery | limit 1\"`\n\n\ttmpFile, err := os.CreateTemp(\"\", \"test-yaml-tags-*.yaml\")\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpFile.Name())\n\n\t_, err = tmpFile.WriteString(yamlContent)\n\trequire.NoError(t, err)\n\trequire.NoError(t, tmpFile.Close())\n\n\tqueryFile, err := readQueryFileContent(tmpFile.Name())\n\n\tassert.NoError(t, err)\n\n\t// Verify that YAML tags correctly map to struct fields\n\tassert.Equal(t, \"YAML Tag Test\", queryFile.Title)                          // yaml:\"title\"\n\tassert.Equal(t, \"yaml-sub-123\", queryFile.SubscriptionID)                  // yaml:\"azure_subscription_id\"\n\tassert.Equal(t, \"yaml-workspace-456\", queryFile.WorkspaceID)               // yaml:\"azure_workspace_id\"\n\tassert.Equal(t, []string{\"TestColumn1\", \"TestColumn2\"}, queryFile.Columns) // yaml:\"columns\"\n\tassert.Equal(t, \"TestQuery | limit 1\", queryFile.Query)                    // yaml:\"query\"\n}\n"
  },
  {
    "path": "modules/azurelogs/query.go",
    "content": "package azurelogs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/to\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/monitor/azquery\"\n)\n\n// LogQueryClients holds the Azure Logs clients for different subscriptions\n// This is a global variable to avoid creating a new client for each query\nvar LogQueryClients map[string]*azquery.LogsClient\n\n// clientsMutex protects concurrent access to LogQueryClients\nvar clientsMutex sync.RWMutex\n\n// TableRow represents a single row of data from Azure Log Analytics\ntype TableRow []string\n\n// TableResp represents the response from an Azure Log Analytics query\ntype TableResp struct {\n\tHeader []string   // Column headers\n\tRows   []TableRow // Data rows\n}\n\n// RunQuery executes an Azure Log Analytics query and returns the formatted results\nfunc RunQuery(sess *Session) (*TableResp, error) {\n\tqf := sess.QueryFile\n\tvar err error\n\tvar tableResp TableResp\n\ttableResp.Header = qf.Columns\n\n\tif qf.WorkspaceID == \"\" {\n\t\treturn nil, fmt.Errorf(\"azure workspace ID is required but not configured\")\n\t}\n\n\tif qf.SubscriptionID == \"\" {\n\t\treturn nil, fmt.Errorf(\"azure subscription ID is required but not configured\")\n\t}\n\n\t// Use read lock first to check if client exists\n\tclientsMutex.RLock()\n\tclient := LogQueryClients[qf.SubscriptionID]\n\tclientsMapExists := LogQueryClients != nil\n\tclientsMutex.RUnlock()\n\n\t// If map doesn't exist or client doesn't exist, we need write access\n\tif !clientsMapExists || client == nil {\n\t\tclientsMutex.Lock()\n\t\t// Double-check after acquiring write lock (double-checked locking pattern)\n\t\tif LogQueryClients == nil {\n\t\t\tLogQueryClients = make(map[string]*azquery.LogsClient)\n\t\t}\n\n\t\tif LogQueryClients[qf.SubscriptionID] == nil {\n\t\t\tLogQueryClients[qf.SubscriptionID], err = CreateLogsClient(sess, qf.SubscriptionID)\n\t\t\tif err != nil {\n\t\t\t\tclientsMutex.Unlock()\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create Azure Logs client for subscription %s: %w\", qf.SubscriptionID, err)\n\t\t\t}\n\t\t}\n\t\tclient = LogQueryClients[qf.SubscriptionID]\n\t\tclientsMutex.Unlock()\n\t}\n\n\tres, err := client.QueryWorkspace(\n\t\tcontext.Background(),\n\t\tqf.WorkspaceID,\n\t\tazquery.Body{\n\t\t\tQuery: to.Ptr(qf.Query),\n\t\t},\n\t\tnil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute query on workspace %s: %w\", qf.WorkspaceID, err)\n\t}\n\n\tif res.Error != nil {\n\t\treturn nil, res.Error\n\t}\n\n\tswitch len(res.Tables) {\n\tcase 0:\n\t\treturn nil, fmt.Errorf(\"query returned no data tables: %s\", qf.Query)\n\tcase 1:\n\t\tif len(res.Tables[0].Columns) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"query returned table with no columns: %s\", qf.Query)\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"query returned %d tables, expected 1: %s\", len(res.Tables), qf.Query)\n\t}\n\n\t// Process each row of data\n\tfor _, row := range res.Tables[0].Rows {\n\t\tvar r TableRow\n\n\t\tfor _, field := range row {\n\t\t\tif field == nil {\n\t\t\t\tr = append(r, \"\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Convert all data types to string representation\n\t\t\tswitch v := field.(type) {\n\t\t\tcase string:\n\t\t\t\tr = append(r, v)\n\t\t\tcase float64:\n\t\t\t\tr = append(r, fmt.Sprintf(\"%.0f\", v))\n\t\t\tdefault:\n\t\t\t\tr = append(r, fmt.Sprintf(\"%v\", v))\n\t\t\t}\n\t\t}\n\t\ttableResp.Rows = append(tableResp.Rows, r)\n\t}\n\n\treturn &tableResp, nil\n}\n"
  },
  {
    "path": "modules/azurelogs/query_concurrent_test.go",
    "content": "package azurelogs\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/monitor/azquery\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLogQueryClients_ConcurrentAccess(t *testing.T) {\n\t// Save original state\n\toriginalClients := LogQueryClients\n\tdefer func() { LogQueryClients = originalClients }()\n\n\t// Reset to nil to test initialization\n\tLogQueryClients = nil\n\n\tconst numGoroutines = 10\n\tconst subscriptionID = \"test-subscription\"\n\n\tvar wg sync.WaitGroup\n\tresults := make([]bool, numGoroutines)\n\n\t// Launch multiple goroutines that try to access LogQueryClients simultaneously\n\tfor i := 0; i < numGoroutines; i++ {\n\t\twg.Add(1)\n\t\tgo func(index int) {\n\t\t\tdefer wg.Done()\n\n\t\t\t// Use read lock to check if client exists\n\t\t\tclientsMutex.RLock()\n\t\t\tclient := LogQueryClients[subscriptionID]\n\t\t\tclientsMapExists := LogQueryClients != nil\n\t\t\tclientsMutex.RUnlock()\n\n\t\t\t// Record if we found the map initialized\n\t\t\tresults[index] = clientsMapExists\n\n\t\t\t// If map doesn't exist, try to initialize it\n\t\t\tif !clientsMapExists || client == nil {\n\t\t\t\tclientsMutex.Lock()\n\t\t\t\t// Double-check after acquiring write lock\n\t\t\t\tif LogQueryClients == nil {\n\t\t\t\t\tLogQueryClients = make(map[string]*azquery.LogsClient)\n\t\t\t\t}\n\t\t\t\tclientsMutex.Unlock()\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\t// Verify that LogQueryClients was properly initialized\n\tassert.NotNil(t, LogQueryClients)\n\tassert.IsType(t, map[string]*azquery.LogsClient{}, LogQueryClients)\n\n\t// At least one goroutine should have seen the map as not existing initially\n\tanyFoundNil := false\n\tfor _, result := range results {\n\t\tif !result {\n\t\t\tanyFoundNil = true\n\t\t\tbreak\n\t\t}\n\t}\n\tassert.True(t, anyFoundNil, \"Expected at least one goroutine to see LogQueryClients as nil initially\")\n}\n\nfunc TestLogQueryClients_ConcurrentReadWrite(t *testing.T) {\n\t// Save original state\n\toriginalClients := LogQueryClients\n\tdefer func() { LogQueryClients = originalClients }()\n\n\t// Initialize with a clean map\n\tLogQueryClients = make(map[string]*azquery.LogsClient)\n\n\tconst numReaders = 5\n\tconst numWriters = 3\n\tconst subscriptionPrefix = \"subscription-\"\n\n\tvar wg sync.WaitGroup\n\n\t// Launch reader goroutines\n\tfor i := 0; i < numReaders; i++ {\n\t\twg.Add(1)\n\t\tgo func(readerID int) {\n\t\t\tdefer wg.Done()\n\n\t\t\tfor j := 0; j < 10; j++ {\n\t\t\t\t// Read from the map safely\n\t\t\t\tclientsMutex.RLock()\n\t\t\t\t_ = LogQueryClients[\"test-subscription\"]\n\t\t\t\tmapSize := len(LogQueryClients)\n\t\t\t\tclientsMutex.RUnlock()\n\n\t\t\t\t// Verify map size is reasonable (between 0 and numWriters)\n\t\t\t\tassert.GreaterOrEqual(t, mapSize, 0)\n\t\t\t\tassert.LessOrEqual(t, mapSize, numWriters)\n\t\t\t}\n\t\t}(i)\n\t}\n\n\t// Launch writer goroutines\n\tfor i := 0; i < numWriters; i++ {\n\t\twg.Add(1)\n\t\tgo func(writerID int) {\n\t\t\tdefer wg.Done()\n\n\t\t\tsubscriptionID := subscriptionPrefix + string(rune('A'+writerID))\n\n\t\t\t// Write to the map safely\n\t\t\tclientsMutex.Lock()\n\t\t\tLogQueryClients[subscriptionID] = nil // Simulate adding a client entry\n\t\t\tclientsMutex.Unlock()\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\t// Verify final state\n\tclientsMutex.RLock()\n\tfinalSize := len(LogQueryClients)\n\tclientsMutex.RUnlock()\n\n\tassert.Equal(t, numWriters, finalSize, \"Expected exactly %d entries after concurrent writes\", numWriters)\n}\n\nfunc TestLogQueryClients_RaceCondition(t *testing.T) {\n\t// This test verifies that concurrent access to LogQueryClients is thread-safe\n\t// Save original state\n\toriginalClients := LogQueryClients\n\tdefer func() { LogQueryClients = originalClients }()\n\n\tconst numGoroutines = 100\n\tconst numIterations = 10\n\n\tfor attempt := 0; attempt < 5; attempt++ { // Run multiple attempts to catch race conditions\n\t\t// Reset to trigger concurrent initialization\n\t\tLogQueryClients = nil\n\n\t\tvar wg sync.WaitGroup\n\n\t\t// Launch goroutines that all try to access/initialize LogQueryClients\n\t\tfor i := 0; i < numGoroutines; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func(goroutineID int) {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\tsubscriptionID := fmt.Sprintf(\"subscription-%d\", goroutineID%3) // Use 3 different subscriptions\n\n\t\t\t\tfor j := 0; j < numIterations; j++ {\n\t\t\t\t\t// Exactly match the logic from RunQuery function\n\t\t\t\t\tclientsMutex.RLock()\n\t\t\t\t\tclient := LogQueryClients[subscriptionID]\n\t\t\t\t\tclientsMapExists := LogQueryClients != nil\n\t\t\t\t\tclientsMutex.RUnlock()\n\n\t\t\t\t\tif !clientsMapExists || client == nil {\n\t\t\t\t\t\tclientsMutex.Lock()\n\t\t\t\t\t\t// Double-check after acquiring write lock\n\t\t\t\t\t\tif LogQueryClients == nil {\n\t\t\t\t\t\t\tLogQueryClients = make(map[string]*azquery.LogsClient)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Double-check for this specific subscription\n\t\t\t\t\t\tif LogQueryClients[subscriptionID] == nil {\n\t\t\t\t\t\t\tLogQueryClients[subscriptionID] = nil // Simulate client creation\n\t\t\t\t\t\t}\n\t\t\t\t\t\tclientsMutex.Unlock()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}(i)\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Verify final state is consistent - no race conditions occurred\n\t\tclientsMutex.RLock()\n\t\tassert.NotNil(t, LogQueryClients, \"Attempt %d: LogQueryClients should be initialized\", attempt+1)\n\t\t// Should have exactly 3 subscriptions (based on goroutineID%3)\n\t\tassert.Equal(t, 3, len(LogQueryClients), \"Attempt %d: Expected 3 subscription entries\", attempt+1)\n\t\tclientsMutex.RUnlock()\n\t}\n}\n"
  },
  {
    "path": "modules/azurelogs/query_test.go",
    "content": "package azurelogs\n\nimport (\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/monitor/azquery\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// createMockSession creates a mock session for testing\nfunc createMockSession() *Session {\n\treturn &Session{\n\t\tQueryFile: QueryFile{\n\t\t\tWorkspaceID:    \"test-workspace-id\",\n\t\t\tSubscriptionID: \"test-subscription-id\",\n\t\t\tQuery:          \"test-query\",\n\t\t\tColumns:        []string{\"Column1\", \"Column2\", \"Column3\"},\n\t\t},\n\t}\n}\n\n// Tests for input validation that don't require Azure SDK mocking\nfunc TestRunQuery_MissingWorkspaceID(t *testing.T) {\n\tsess := createMockSession()\n\tsess.QueryFile.WorkspaceID = \"\"\n\n\t// Since we can't mock the Azure client easily, we expect this to fail\n\t// during client creation or earlier validation\n\tresult, err := RunQuery(sess)\n\n\tassert.Nil(t, result)\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"azure workspace ID is required\")\n}\n\nfunc TestRunQuery_MissingSubscriptionID(t *testing.T) {\n\tsess := createMockSession()\n\tsess.QueryFile.SubscriptionID = \"\"\n\n\tresult, err := RunQuery(sess)\n\n\tassert.Nil(t, result)\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"azure subscription ID is required\")\n}\n\nfunc TestTableResp_Structure(t *testing.T) {\n\t// Test TableResp structure creation and manipulation\n\ttableResp := &TableResp{\n\t\tHeader: []string{\"Col1\", \"Col2\", \"Col3\"},\n\t\tRows: []TableRow{\n\t\t\t{\"Value1\", \"Value2\", \"Value3\"},\n\t\t\t{\"Value4\", \"Value5\", \"Value6\"},\n\t\t},\n\t}\n\n\tassert.NotNil(t, tableResp)\n\tassert.Len(t, tableResp.Header, 3)\n\tassert.Len(t, tableResp.Rows, 2)\n\tassert.Equal(t, \"Col1\", tableResp.Header[0])\n\tassert.Equal(t, \"Value1\", tableResp.Rows[0][0])\n}\n\nfunc TestTableRow_Operations(t *testing.T) {\n\t// Test TableRow operations\n\trow := TableRow{\"data1\", \"data2\", \"data3\"}\n\n\tassert.Len(t, row, 3)\n\tassert.Equal(t, \"data1\", row[0])\n\tassert.Equal(t, \"data2\", row[1])\n\tassert.Equal(t, \"data3\", row[2])\n\n\t// Test appending to row\n\trow = append(row, \"data4\")\n\tassert.Len(t, row, 4)\n\tassert.Equal(t, \"data4\", row[3])\n}\n\nfunc TestLogQueryClients_GlobalVariable(t *testing.T) {\n\t// Test the global LogQueryClients variable behavior\n\toriginalClients := LogQueryClients\n\tdefer func() { LogQueryClients = originalClients }()\n\n\t// Test initialization\n\tLogQueryClients = nil\n\tassert.Nil(t, LogQueryClients)\n\n\t// Test map creation\n\tLogQueryClients = make(map[string]*azquery.LogsClient)\n\tassert.NotNil(t, LogQueryClients)\n\tassert.Len(t, LogQueryClients, 0)\n\n\t// Test that the map exists and can be used\n\tassert.IsType(t, map[string]*azquery.LogsClient{}, LogQueryClients)\n}\n"
  },
  {
    "path": "modules/azurelogs/session.go",
    "content": "package azurelogs\n\nimport (\n\t\"fmt\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azidentity\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/monitor/azquery\"\n\t\"os\"\n)\n\nconst (\n\tenvAzureClientID     = \"AZURE_CLIENT_ID\"\n\tenvAzureClientSecret = \"AZURE_CLIENT_SECRET\"\n\tenvAzureTenantID     = \"AZURE_TENANT_ID\"\n)\n\n// Init initializes a new Azure session with the specified query file\nfunc Init(queryPath *string) (*Session, error) {\n\tsess := &Session{}\n\tsess.Azure = &AZSession{}\n\n\t// Initialize Azure authentication using modern non-deprecated libraries\n\tif err := InitializeAzureAuthentication(sess); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize Azure authentication: %w\", err)\n\t}\n\n\terr := readQueryFile(sess, *queryPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read query file %s: %w\", *queryPath, err)\n\t}\n\n\treturn sess, nil\n}\n\n// Session holds the configuration and state for an Azure Log Analytics session\ntype Session struct {\n\tApp struct {\n\t\tSemVer string\n\t}\n\n\tAzure       *AZSession\n\tQueriesPath string\n\tQueryFile   QueryFile\n}\n\n// AZClientSecretCredential holds Azure service principal credentials\ntype AZClientSecretCredential struct {\n\tClientID     string\n\tClientSecret string\n\tTenantID     string\n}\n\n// AZSession holds Azure authentication and client information\ntype AZSession struct {\n\tCredential             azcore.TokenCredential\n\tClientSecretCredential AZClientSecretCredential\n}\n\n// InitializeAzureAuthentication sets up Azure authentication using modern SDK\nfunc InitializeAzureAuthentication(sess *Session) error {\n\tvar err error\n\n\tsess.Azure.ClientSecretCredential.ClientID = os.Getenv(envAzureClientID)\n\tsess.Azure.ClientSecretCredential.ClientSecret = os.Getenv(envAzureClientSecret)\n\tsess.Azure.ClientSecretCredential.TenantID = os.Getenv(envAzureTenantID)\n\n\t// Prefer client secret credential if all required environment variables are set\n\tif sess.Azure.ClientSecretCredential.ClientID != \"\" &&\n\t\tsess.Azure.ClientSecretCredential.ClientSecret != \"\" &&\n\t\tsess.Azure.ClientSecretCredential.TenantID != \"\" {\n\n\t\tsess.Azure.Credential, err = azidentity.NewClientSecretCredential(\n\t\t\tsess.Azure.ClientSecretCredential.TenantID,\n\t\t\tsess.Azure.ClientSecretCredential.ClientID,\n\t\t\tsess.Azure.ClientSecretCredential.ClientSecret,\n\t\t\t&azidentity.ClientSecretCredentialOptions{})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\n\tsess.Azure.Credential, err = azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// CreateLogsClient creates a cached Azure Log Analytics client for the specified subscription\nfunc CreateLogsClient(sess *Session, subscriptionID string) (*azquery.LogsClient, error) {\n\tif sess.Azure.Credential == nil {\n\t\treturn nil, fmt.Errorf(\"azure credentials not initialized for subscription %s: please set up authentication first\", subscriptionID)\n\t}\n\n\t// Create a new client for this subscription ID using modern Azure SDK\n\tclient, err := azquery.NewLogsClient(sess.Azure.Credential, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create Azure Logs client for subscription %s: %w\", subscriptionID, err)\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "modules/azurelogs/session_test.go",
    "content": "package azurelogs\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAZClientSecretCredential_Structure(t *testing.T) {\n\t// Test AZClientSecretCredential structure\n\tcred := AZClientSecretCredential{\n\t\tClientID:     \"test-client-id\",\n\t\tClientSecret: \"test-client-secret\",\n\t\tTenantID:     \"test-tenant-id\",\n\t}\n\n\tassert.Equal(t, \"test-client-id\", cred.ClientID)\n\tassert.Equal(t, \"test-client-secret\", cred.ClientSecret)\n\tassert.Equal(t, \"test-tenant-id\", cred.TenantID)\n}\n\nfunc TestAZSession_Structure(t *testing.T) {\n\t// Test AZSession structure\n\tazSession := &AZSession{\n\t\tClientSecretCredential: AZClientSecretCredential{\n\t\t\tClientID:     \"client-123\",\n\t\t\tClientSecret: \"secret-456\",\n\t\t\tTenantID:     \"tenant-789\",\n\t\t},\n\t}\n\n\tassert.Equal(t, \"client-123\", azSession.ClientSecretCredential.ClientID)\n\tassert.Equal(t, \"secret-456\", azSession.ClientSecretCredential.ClientSecret)\n\tassert.Equal(t, \"tenant-789\", azSession.ClientSecretCredential.TenantID)\n}\n\nfunc TestSession_Structure(t *testing.T) {\n\t// Test Session structure\n\tsess := &Session{\n\t\tQueriesPath: \"/path/to/queries\",\n\t\tQueryFile: QueryFile{\n\t\t\tTitle:          \"Test Query\",\n\t\t\tSubscriptionID: \"sub-123\",\n\t\t\tWorkspaceID:    \"workspace-456\",\n\t\t\tColumns:        []string{\"Col1\", \"Col2\"},\n\t\t\tQuery:          \"TestQuery | limit 10\",\n\t\t},\n\t}\n\n\tassert.Equal(t, \"/path/to/queries\", sess.QueriesPath)\n\tassert.Equal(t, \"Test Query\", sess.QueryFile.Title)\n\tassert.Equal(t, \"sub-123\", sess.QueryFile.SubscriptionID)\n\tassert.Equal(t, \"workspace-456\", sess.QueryFile.WorkspaceID)\n\tassert.Len(t, sess.QueryFile.Columns, 2)\n\tassert.Equal(t, \"TestQuery | limit 10\", sess.QueryFile.Query)\n}\n\nfunc TestInitializeAzureAuthentication_EnvironmentVariables(t *testing.T) {\n\t// Save original environment variables\n\toriginalClientID := os.Getenv(envAzureClientID)\n\toriginalClientSecret := os.Getenv(envAzureClientSecret)\n\toriginalTenantID := os.Getenv(envAzureTenantID)\n\n\t// Clean up after test\n\tdefer func() {\n\t\t_ = os.Setenv(envAzureClientID, originalClientID)\n\t\t_ = os.Setenv(envAzureClientSecret, originalClientSecret)\n\t\t_ = os.Setenv(envAzureTenantID, originalTenantID)\n\t}()\n\n\t// Test with all environment variables set\n\tt.Run(\"with all env vars set\", func(t *testing.T) {\n\t\t_ = os.Setenv(envAzureClientID, \"test-client-id\")\n\t\t_ = os.Setenv(envAzureClientSecret, \"test-client-secret\")\n\t\t_ = os.Setenv(envAzureTenantID, \"test-tenant-id\")\n\n\t\tsess := &Session{Azure: &AZSession{}}\n\t\terr := InitializeAzureAuthentication(sess)\n\n\t\t// We expect this to succeed in setting up the credential structure\n\t\t// even if the actual Azure authentication fails\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"test-client-id\", sess.Azure.ClientSecretCredential.ClientID)\n\t\tassert.Equal(t, \"test-client-secret\", sess.Azure.ClientSecretCredential.ClientSecret)\n\t\tassert.Equal(t, \"test-tenant-id\", sess.Azure.ClientSecretCredential.TenantID)\n\t\tassert.NotNil(t, sess.Azure.Credential)\n\t})\n\n\t// Test with missing environment variables (should fall back to default credential)\n\tt.Run(\"with missing env vars\", func(t *testing.T) {\n\t\t_ = os.Unsetenv(envAzureClientID)\n\t\t_ = os.Unsetenv(envAzureClientSecret)\n\t\t_ = os.Unsetenv(envAzureTenantID)\n\n\t\tsess := &Session{Azure: &AZSession{}}\n\t\terr := InitializeAzureAuthentication(sess)\n\n\t\t// Should fall back to DefaultAzureCredential\n\t\t// This may fail in test environment, but we're testing the fallback logic\n\t\tif err != nil {\n\t\t\t// In test environment, DefaultAzureCredential might fail\n\t\t\t// This is expected behavior\n\t\t\tassert.Contains(t, err.Error(), \"DefaultAzureCredential\")\n\t\t} else {\n\t\t\tassert.NotNil(t, sess.Azure.Credential)\n\t\t}\n\t})\n\n\t// Test with partial environment variables (should fall back to default)\n\tt.Run(\"with partial env vars\", func(t *testing.T) {\n\t\t_ = os.Setenv(envAzureClientID, \"test-client-id\")\n\t\t_ = os.Unsetenv(envAzureClientSecret)\n\t\t_ = os.Unsetenv(envAzureTenantID)\n\n\t\tsess := &Session{Azure: &AZSession{}}\n\t\terr := InitializeAzureAuthentication(sess)\n\n\t\t// Should fall back to DefaultAzureCredential since not all vars are set\n\t\tif err != nil {\n\t\t\tassert.Contains(t, err.Error(), \"DefaultAzureCredential\")\n\t\t} else {\n\t\t\tassert.NotNil(t, sess.Azure.Credential)\n\t\t}\n\t})\n}\n\nfunc TestCreateLogsClient_NilCredentials(t *testing.T) {\n\t// Test CreateLogsClient with nil credentials\n\tsess := &Session{\n\t\tAzure: &AZSession{\n\t\t\tCredential: nil,\n\t\t},\n\t}\n\n\tclient, err := CreateLogsClient(sess, \"test-subscription\")\n\n\tassert.Nil(t, client)\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"azure credentials not initialized\")\n\tassert.Contains(t, err.Error(), \"test-subscription\")\n}\n\nfunc TestInit_InvalidQueryPath(t *testing.T) {\n\t// Test Init with invalid query path\n\tinvalidPath := \"/nonexistent/path/to/query.yml\"\n\n\tsess, err := Init(&invalidPath)\n\n\tassert.Nil(t, sess)\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"failed to read query file\")\n}\n\nfunc TestInit_NilQueryPath(t *testing.T) {\n\t// Test Init with nil query path (should panic or handle gracefully)\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\t// Expected behavior - accessing *nil should panic\n\t\t\t// This is the expected behavior, so the test passes\n\t\t\tt.Log(\"Init correctly panicked when given nil query path\")\n\t\t}\n\t}()\n\n\tsess, err := Init(nil)\n\n\t// If we get here, the function handled nil gracefully\n\tassert.Nil(t, sess)\n\tassert.Error(t, err)\n}\n\nfunc TestEnvironmentConstants(t *testing.T) {\n\t// Test that environment variable constants are correctly defined\n\tassert.Equal(t, \"AZURE_CLIENT_ID\", envAzureClientID)\n\tassert.Equal(t, \"AZURE_CLIENT_SECRET\", envAzureClientSecret)\n\tassert.Equal(t, \"AZURE_TENANT_ID\", envAzureTenantID)\n}\n"
  },
  {
    "path": "modules/azurelogs/settings.go",
    "content": "package azurelogs\n\nimport (\n\t\"github.com/olebedev/config\"\n\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Azure Logs\"\n)\n\n// Settings defines the configuration for the Azure Logs widget\ntype Settings struct {\n\t*cfg.Common\n\n\t// Queryfile is the path to the YAML file containing the Azure query configuration\n\tQueryfile string `help:\"Path to YAML file containing Azure Log Analytics query configuration\"`\n}\n\n// NewSettingsFromYAML creates a new Settings instance from YAML configuration\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tQueryfile: ymlConfig.UString(\"queryFile\", \"\"),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/azurelogs/settings_test.go",
    "content": "package azurelogs\n\nimport (\n\t\"testing\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nfunc TestSettings_Structure(t *testing.T) {\n\t// Test Settings structure\n\tsettings := &Settings{\n\t\tCommon: &cfg.Common{\n\t\t\tTitle: \"Test Azure Logs\",\n\t\t},\n\t\tQueryfile: \"/path/to/query.yml\",\n\t}\n\n\tassert.NotNil(t, settings.Common)\n\tassert.Equal(t, \"Test Azure Logs\", settings.Title)\n\tassert.Equal(t, \"/path/to/query.yml\", settings.Queryfile)\n}\n\nfunc TestNewSettingsFromYAML(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tconfigData    map[string]interface{}\n\t\texpectedTitle string\n\t\texpectedQuery string\n\t}{\n\t\t{\n\t\t\tname: \"with custom query file\",\n\t\t\tconfigData: map[string]interface{}{\n\t\t\t\t\"queryFile\": \"/custom/path/query.yml\",\n\t\t\t\t\"title\":     \"Custom Azure Logs\",\n\t\t\t},\n\t\t\texpectedTitle: \"Custom Azure Logs\",\n\t\t\texpectedQuery: \"/custom/path/query.yml\",\n\t\t},\n\t\t{\n\t\t\tname:       \"with default values\",\n\t\t\tconfigData: map[string]interface{}{\n\t\t\t\t// No queryFile specified, should use default empty string\n\t\t\t},\n\t\t\texpectedTitle: defaultTitle, // Should use default title\n\t\t\texpectedQuery: \"\",           // Should use default empty string\n\t\t},\n\t\t{\n\t\t\tname: \"with empty query file\",\n\t\t\tconfigData: map[string]interface{}{\n\t\t\t\t\"queryFile\": \"\",\n\t\t\t\t\"title\":     \"Empty Query Azure Logs\",\n\t\t\t},\n\t\t\texpectedTitle: \"Empty Query Azure Logs\",\n\t\t\texpectedQuery: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create YAML config from test data\n\t\t\tymlConfig, err := config.ParseYaml(yamlFromMap(tt.configData))\n\t\t\tassert.NoError(t, err)\n\n\t\t\t// Create global config (can be minimal for this test)\n\t\t\tglobalConfig, err := config.ParseYaml(\"global: {}\")\n\t\t\tassert.NoError(t, err)\n\n\t\t\tsettings := NewSettingsFromYAML(\"test-widget\", ymlConfig, globalConfig)\n\n\t\t\tassert.NotNil(t, settings)\n\t\t\tassert.NotNil(t, settings.Common)\n\t\t\tassert.Equal(t, tt.expectedTitle, settings.Title)\n\t\t\tassert.Equal(t, tt.expectedQuery, settings.Queryfile)\n\t\t})\n\t}\n}\n\nfunc TestDefaultConstants(t *testing.T) {\n\t// Test that default constants are correctly defined\n\tassert.True(t, defaultFocusable)\n\tassert.Equal(t, \"Azure Logs\", defaultTitle)\n}\n\nfunc TestSettings_QueryfileField(t *testing.T) {\n\t// Test that Queryfile field can be set and retrieved\n\tsettings := &Settings{}\n\n\t// Test setting various query file paths\n\ttestPaths := []string{\n\t\t\"/absolute/path/query.yml\",\n\t\t\"relative/path/query.yml\",\n\t\t\"./current/dir/query.yml\",\n\t\t\"../parent/dir/query.yml\",\n\t\t\"\",\n\t}\n\n\tfor _, path := range testPaths {\n\t\tsettings.Queryfile = path\n\t\tassert.Equal(t, path, settings.Queryfile)\n\t}\n}\n\nfunc TestNewSettingsFromYAML_Integration(t *testing.T) {\n\t// Test with a more complete YAML configuration\n\tconfigData := map[string]interface{}{\n\t\t\"queryFile\": \"/etc/wtf/azure-query.yml\",\n\t\t\"title\":     \"Production Azure Logs\",\n\t\t\"enabled\":   true,\n\t\t\"position\": map[string]interface{}{\n\t\t\t\"top\":    0,\n\t\t\t\"left\":   0,\n\t\t\t\"width\":  2,\n\t\t\t\"height\": 1,\n\t\t},\n\t\t\"refreshInterval\": \"5m\",\n\t}\n\n\tymlConfig, err := config.ParseYaml(yamlFromMap(configData))\n\tassert.NoError(t, err)\n\n\tglobalConfig, err := config.ParseYaml(`\nwtf:\n  term: \"xterm-256color\"\n  grid:\n    columns: [40, 40, 40]\n    rows: [13, 13, 4]\n`)\n\tassert.NoError(t, err)\n\n\tsettings := NewSettingsFromYAML(\"azure-logs\", ymlConfig, globalConfig)\n\n\tassert.NotNil(t, settings)\n\tassert.Equal(t, \"Production Azure Logs\", settings.Title)\n\tassert.Equal(t, \"/etc/wtf/azure-query.yml\", settings.Queryfile)\n\tassert.NotNil(t, settings.Common)\n}\n\n// Helper function to convert map to YAML string for testing\nfunc yamlFromMap(data map[string]interface{}) string {\n\tif len(data) == 0 {\n\t\treturn \"{}\"\n\t}\n\n\tyaml := \"\"\n\tfor key, value := range data {\n\t\tswitch v := value.(type) {\n\t\tcase string:\n\t\t\tyaml += key + \": \\\"\" + v + \"\\\"\\n\"\n\t\tcase bool:\n\t\t\tif v {\n\t\t\t\tyaml += key + \": true\\n\"\n\t\t\t} else {\n\t\t\t\tyaml += key + \": false\\n\"\n\t\t\t}\n\t\tcase map[string]interface{}:\n\t\t\tyaml += key + \":\\n\"\n\t\t\tfor subKey, subValue := range v {\n\t\t\t\tyaml += \"  \" + subKey + \": \" + interfaceToString(subValue) + \"\\n\"\n\t\t\t}\n\t\tdefault:\n\t\t\tyaml += key + \": \" + interfaceToString(v) + \"\\n\"\n\t\t}\n\t}\n\treturn yaml\n}\n\n// Helper function to convert interface{} to string for YAML\nfunc interfaceToString(v interface{}) string {\n\tswitch val := v.(type) {\n\tcase string:\n\t\treturn \"\\\"\" + val + \"\\\"\"\n\tcase int:\n\t\treturn string(rune(val + '0'))\n\tcase bool:\n\t\tif val {\n\t\t\treturn \"true\"\n\t\t}\n\t\treturn \"false\"\n\tdefault:\n\t\treturn \"null\"\n\t}\n}\n"
  },
  {
    "path": "modules/azurelogs/widget.go",
    "content": "package azurelogs\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/to\"\n\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst (\n\tdefaultTableWidth  = 120\n\tminColumnWidth     = 8\n\tmaxColumnWidth     = 30\n\tmaxDisplayRows     = 50\n\ttruncateMarker     = \"...\"\n\tsampleRowsForWidth = 15\n)\n\ntype Widget struct {\n\tview.TextWidget\n\tsettings   *Settings\n\tloading    bool\n\tlastError  error\n\tdataLoaded bool\n\ttableData  *TableResp\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, _ *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\t\tsettings:   settings,\n\t}\n\n\twidget.settings.RefreshInterval = 60 * time.Second\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\t// Reset state to allow fresh data fetch\n\twidget.loading = false\n\twidget.lastError = nil\n\twidget.dataLoaded = false\n\twidget.tableData = nil\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Helper Functions -------------------- */\n\nfunc (widget *Widget) fetchDataAsync() {\n\tsess, err := Init(to.Ptr(widget.settings.Queryfile))\n\tif err != nil {\n\t\twidget.setError(fmt.Errorf(\"failed to initialize Azure session: %w\", err))\n\t\treturn\n\t}\n\n\t// Execute Azure query directly\n\ttableResp, err := RunQuery(sess)\n\tif err != nil {\n\t\twidget.setError(fmt.Errorf(\"failed to execute Azure query: %w\", err))\n\t\treturn\n\t}\n\n\t// Check if we have valid data structure\n\tif tableResp == nil || len(tableResp.Header) == 0 {\n\t\twidget.setError(fmt.Errorf(\"no table structure returned from query\"))\n\t\treturn\n\t}\n\n\t// Store the data and mark as loaded\n\twidget.tableData = tableResp\n\twidget.dataLoaded = true\n\twidget.loading = false\n\twidget.Redraw(widget.content)\n}\n\n// setError is a helper function to set error state and trigger redraw\nfunc (widget *Widget) setError(err error) {\n\twidget.lastError = err\n\twidget.loading = false\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) renderTable(title string) (string, string, bool) {\n\tif widget.tableData == nil {\n\t\treturn title, \"[red]Error: No table data available[white]\", true\n\t}\n\n\t// Calculate column widths and format table - headers are always shown when available\n\tcolWidths := calculateAdaptiveColumnWidths(widget.tableData, defaultTableWidth)\n\n\tvar sb strings.Builder\n\t// Always show headers when we have table structure\n\twidget.formatTableHeaders(&sb, widget.tableData.Header, colWidths)\n\twidget.formatTableSeparator(&sb, widget.tableData.Header, colWidths)\n\n\t// Show data rows if available, otherwise show informative message\n\tif len(widget.tableData.Rows) == 0 {\n\t\tsb.WriteString(\"[dim](No data rows returned)[white]\\n\")\n\t} else {\n\t\twidget.formatTableRows(&sb, widget.tableData.Rows, widget.tableData.Header, colWidths)\n\t}\n\n\treturn title, sb.String(), false\n}\n\n// formatTableHeaders writes the table header row to the string builder\nfunc (widget *Widget) formatTableHeaders(sb *strings.Builder, headers []string, colWidths []int) {\n\tfor i, header := range headers {\n\t\tif i > 0 {\n\t\t\tsb.WriteString(\" ¦\")\n\t\t}\n\t\theaderText := header\n\t\tif i < len(colWidths) && len(headerText) > colWidths[i] {\n\t\t\theaderText = headerText[:colWidths[i]-len(truncateMarker)] + truncateMarker\n\t\t}\n\t\t_, _ = fmt.Fprintf(sb, \"[lightblue]%-*s[white]\", colWidths[i], headerText)\n\t}\n\tsb.WriteString(\"\\n\")\n}\n\n// formatTableSeparator writes the table separator row to the string builder\nfunc (widget *Widget) formatTableSeparator(sb *strings.Builder, headers []string, colWidths []int) {\n\tfor i := range headers {\n\t\tif i > 0 {\n\t\t\tsb.WriteString(\"---\")\n\t\t}\n\t\tsb.WriteString(strings.Repeat(\"-\", colWidths[i]))\n\t}\n\tsb.WriteString(\"\\n\")\n}\n\n// formatTableRows writes the table data rows to the string builder\nfunc (widget *Widget) formatTableRows(sb *strings.Builder, rows []TableRow, headers []string, colWidths []int) {\n\tmaxRows := maxDisplayRows\n\trowCount := len(rows)\n\tif rowCount > maxRows {\n\t\trowCount = maxRows\n\t}\n\n\tfor rowIdx := 0; rowIdx < rowCount; rowIdx++ {\n\t\trow := rows[rowIdx]\n\t\tfor colIdx, cell := range row {\n\t\t\tif colIdx >= len(headers) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif colIdx > 0 {\n\t\t\t\tsb.WriteString(\" ¦\")\n\t\t\t}\n\n\t\t\tcellText := strings.TrimSpace(cell)\n\t\t\tif colIdx < len(colWidths) && len(cellText) > colWidths[colIdx] {\n\t\t\t\tcellText = cellText[:colWidths[colIdx]-len(truncateMarker)] + truncateMarker\n\t\t\t}\n\n\t\t\t_, _ = fmt.Fprintf(sb, \"%-*s\", colWidths[colIdx], cellText)\n\t\t}\n\t\tsb.WriteString(\"\\n\")\n\t}\n\n\tif len(rows) > maxRows {\n\t\t_, _ = fmt.Fprintf(sb, \"\\n[gray]... (%d more rows truncated for display)[white]\\n\", len(rows)-maxRows)\n\t}\n}\n\n// calculateAdaptiveColumnWidths computes optimal column widths based on content and available space\nfunc calculateAdaptiveColumnWidths(tr *TableResp, availableWidth int) []int {\n\tif len(tr.Header) == 0 {\n\t\treturn []int{}\n\t}\n\n\t// Calculate content-based widths\n\twidths := make([]int, len(tr.Header))\n\n\t// Start with header widths\n\tfor i, header := range tr.Header {\n\t\twidths[i] = len(header)\n\t}\n\n\t// Check data rows to find maximum content width per column (if any rows exist)\n\tif len(tr.Rows) > 0 {\n\t\tmaxRows := sampleRowsForWidth // Sample first N rows for width calculation\n\t\trowCount := len(tr.Rows)\n\t\tif rowCount > maxRows {\n\t\t\trowCount = maxRows\n\t\t}\n\n\t\tfor rowIdx := 0; rowIdx < rowCount; rowIdx++ {\n\t\t\trow := tr.Rows[rowIdx]\n\t\t\tfor colIdx, cell := range row {\n\t\t\t\tif colIdx >= len(widths) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcellLength := len(strings.TrimSpace(cell))\n\t\t\t\tif cellLength > widths[colIdx] {\n\t\t\t\t\twidths[colIdx] = cellLength\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Apply minimum and maximum constraints\n\ttotalWidth := 0\n\tfor i := range widths {\n\t\tif widths[i] < minColumnWidth {\n\t\t\twidths[i] = minColumnWidth\n\t\t}\n\t\tif widths[i] > maxColumnWidth {\n\t\t\twidths[i] = maxColumnWidth\n\t\t}\n\t\ttotalWidth += widths[i]\n\t}\n\n\t// Add space for separators: (n-1) * 2 chars for \" ¦\"\n\tseparatorSpace := (len(widths) - 1) * 2\n\ttotalUsed := totalWidth + separatorSpace\n\n\t// If we exceed available width, proportionally reduce columns\n\tif totalUsed > availableWidth {\n\t\tscaleFactor := float64(availableWidth-separatorSpace) / float64(totalWidth)\n\t\tfor i := range widths {\n\t\t\twidths[i] = int(float64(widths[i]) * scaleFactor)\n\t\t\tif widths[i] < minColumnWidth {\n\t\t\t\twidths[i] = minColumnWidth\n\t\t\t}\n\t\t}\n\t}\n\n\treturn widths\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\n\t// Check if query file is configured\n\tif widget.settings.Queryfile == \"\" {\n\t\treturn title, \"[red]Error: queryFile must be configured in widget settings[white]\\n\\n\", false\n\t}\n\n\t// If we have a previous error, show it immediately\n\tif widget.lastError != nil {\n\t\treturn title, fmt.Sprintf(\"[red]Error: %v[white]\\n\\n[dim]Press 'r' to retry[white]\", widget.lastError), true\n\t}\n\n\t// If data is already loaded, show it\n\tif widget.dataLoaded {\n\t\treturn widget.renderTable(title)\n\t}\n\n\t// Show loading text while fetching data\n\tif !widget.loading {\n\t\twidget.loading = true\n\n\t\t// Start async data fetch\n\t\tgo widget.fetchDataAsync()\n\t\treturn title, \"[yellow]Loading Azure Logs data...[white]\\n\\n[dim]• Initializing Azure session\\n• Executing query on workspace\\n• Processing results[white]\", false\n\t}\n\n\t// Still loading, show loading text\n\treturn title, \"[yellow]Loading Azure Logs data...[white]\\n\\n[dim]• Initializing Azure session\\n• Executing query on workspace\\n• Processing results[white]\", false\n}\n"
  },
  {
    "path": "modules/azurelogs/widget_test.go",
    "content": "package azurelogs\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nfunc TestNewWidget(t *testing.T) {\n\tapp := tview.NewApplication()\n\tredrawChan := make(chan bool, 1)\n\n\tsettings := &Settings{\n\t\tCommon: &cfg.Common{\n\t\t\tTitle: \"Test Azure Logs\",\n\t\t},\n\t\tQueryfile: \"/path/to/query.yml\",\n\t}\n\n\twidget := NewWidget(app, redrawChan, nil, settings)\n\n\tassert.NotNil(t, widget)\n\tassert.Equal(t, settings, widget.settings)\n\tassert.Equal(t, 60*time.Second, widget.settings.RefreshInterval)\n\tassert.False(t, widget.loading)\n\tassert.False(t, widget.dataLoaded)\n\tassert.Nil(t, widget.lastError)\n\tassert.Nil(t, widget.tableData)\n}\n\n// TestWidget_Refresh removed as it tests core WTF framework functionality (Disabled() method)\n// rather than Azure-specific logic\n\nfunc TestWidget_SetError(t *testing.T) {\n\twidget := createTestWidget()\n\twidget.loading = true\n\n\ttestError := assert.AnError\n\twidget.setError(testError)\n\n\tassert.Equal(t, testError, widget.lastError)\n\tassert.False(t, widget.loading)\n}\n\nfunc TestWidget_RenderTable(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\ttableData      *TableResp\n\t\texpectedTitle  string\n\t\texpectedError  bool\n\t\texpectedOutput string\n\t}{\n\t\t{\n\t\t\tname:           \"nil table data\",\n\t\t\ttableData:      nil,\n\t\t\texpectedTitle:  \"Test Title\",\n\t\t\texpectedError:  true,\n\t\t\texpectedOutput: \"[red]Error: No table data available[white]\",\n\t\t},\n\t\t{\n\t\t\tname: \"table with headers but no data\",\n\t\t\ttableData: &TableResp{\n\t\t\t\tHeader: []string{\"Column1\", \"Column2\"},\n\t\t\t\tRows:   []TableRow{},\n\t\t\t},\n\t\t\texpectedTitle:  \"Test Title\",\n\t\t\texpectedError:  false,\n\t\t\texpectedOutput: \"[lightblue]Column1 [white] ¦[lightblue]Column2 [white]\", // Just check the header part\n\t\t},\n\t\t{\n\t\t\tname: \"table with headers and data\",\n\t\t\ttableData: &TableResp{\n\t\t\t\tHeader: []string{\"Col1\", \"Col2\"},\n\t\t\t\tRows: []TableRow{\n\t\t\t\t\t{\"Value1\", \"Value2\"},\n\t\t\t\t\t{\"Value3\", \"Value4\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTitle: \"Test Title\",\n\t\t\texpectedError: false,\n\t\t\texpectedOutput: func() string {\n\t\t\t\t// This will contain the formatted table with headers, separator, and data\n\t\t\t\treturn \"[lightblue]Col1    [white] ¦[lightblue]Col2    [white]\\n--------¦--------\\nValue1   ¦Value2  \\nValue3   ¦Value4  \\n\"\n\t\t\t}(),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\twidget := createTestWidget()\n\t\t\twidget.tableData = tt.tableData\n\n\t\t\ttitle, content, hasError := widget.renderTable(\"Test Title\")\n\n\t\t\tassert.Equal(t, tt.expectedTitle, title)\n\t\t\tassert.Equal(t, tt.expectedError, hasError)\n\t\t\tassert.Contains(t, content, strings.Split(tt.expectedOutput, \"\\n\")[0]) // Check first line\n\t\t})\n\t}\n}\n\nfunc TestWidget_FormatTableHeaders(t *testing.T) {\n\twidget := createTestWidget()\n\tvar sb strings.Builder\n\n\theaders := []string{\"Header1\", \"Header2\", \"Header3\"}\n\tcolWidths := []int{10, 15, 8}\n\n\twidget.formatTableHeaders(&sb, headers, colWidths)\n\n\tresult := sb.String()\n\tassert.Contains(t, result, \"[lightblue]Header1   [white]\")\n\tassert.Contains(t, result, \"¦\")\n\tassert.Contains(t, result, \"[lightblue]Header2        [white]\")\n\tassert.Contains(t, result, \"[lightblue]Header3 [white]\")\n\tassert.True(t, strings.HasSuffix(result, \"\\n\"))\n}\n\nfunc TestWidget_FormatTableSeparator(t *testing.T) {\n\twidget := createTestWidget()\n\tvar sb strings.Builder\n\n\theaders := []string{\"A\", \"B\", \"C\"}\n\tcolWidths := []int{5, 8, 6}\n\n\twidget.formatTableSeparator(&sb, headers, colWidths)\n\n\tresult := sb.String()\n\t// Expected: 5 dashes + \"---\" + 8 dashes + \"---\" + 6 dashes + \"\\n\"\n\tassert.Equal(t, \"-----\"+\"---\"+\"--------\"+\"---\"+\"------\\n\", result)\n}\n\nfunc TestWidget_FormatTableRows(t *testing.T) {\n\twidget := createTestWidget()\n\tvar sb strings.Builder\n\n\theaders := []string{\"Col1\", \"Col2\"}\n\tcolWidths := []int{8, 8}\n\trows := []TableRow{\n\t\t{\"Data1\", \"Data2\"},\n\t\t{\"LongData\", \"Short\"},\n\t}\n\n\twidget.formatTableRows(&sb, rows, headers, colWidths)\n\n\tresult := sb.String()\n\tlines := strings.Split(strings.TrimSpace(result), \"\\n\")\n\tassert.Len(t, lines, 2)\n\tassert.Contains(t, lines[0], \"Data1\")\n\tassert.Contains(t, lines[0], \"Data2\")\n\tassert.Contains(t, lines[1], \"LongData\")\n\tassert.Contains(t, lines[1], \"Short\")\n}\n\nfunc TestWidget_FormatTableRows_WithTruncation(t *testing.T) {\n\twidget := createTestWidget()\n\tvar sb strings.Builder\n\n\theaders := []string{\"Col1\", \"Col2\"}\n\tcolWidths := []int{8, 8}\n\n\t// Create more rows than maxDisplayRows to test truncation\n\trows := make([]TableRow, maxDisplayRows+10)\n\tfor i := range rows {\n\t\trows[i] = TableRow{\"data1\", \"data2\"}\n\t}\n\n\twidget.formatTableRows(&sb, rows, headers, colWidths)\n\n\tresult := sb.String()\n\tassert.Contains(t, result, \"more rows truncated\")\n}\n\nfunc TestWidget_Content(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tqueryfile        string\n\t\tlastError        error\n\t\tdataLoaded       bool\n\t\tloading          bool\n\t\texpectedTitle    string\n\t\texpectedContains string\n\t}{\n\t\t{\n\t\t\tname:             \"no query file configured\",\n\t\t\tqueryfile:        \"\",\n\t\t\texpectedTitle:    \"Test Azure Logs\",\n\t\t\texpectedContains: \"[red]Error: queryFile must be configured\",\n\t\t},\n\t\t{\n\t\t\tname:             \"has error\",\n\t\t\tqueryfile:        \"/path/to/query.yml\",\n\t\t\tlastError:        assert.AnError,\n\t\t\texpectedTitle:    \"Test Azure Logs\",\n\t\t\texpectedContains: \"[red]Error:\",\n\t\t},\n\t\t{\n\t\t\tname:             \"data loaded\",\n\t\t\tqueryfile:        \"/path/to/query.yml\",\n\t\t\tdataLoaded:       true,\n\t\t\texpectedTitle:    \"Test Azure Logs\",\n\t\t\texpectedContains: \"[red]Error: No table data available\", // Since tableData is nil\n\t\t},\n\t\t{\n\t\t\tname:             \"loading state\",\n\t\t\tqueryfile:        \"/path/to/query.yml\",\n\t\t\tloading:          false, // Will trigger loading\n\t\t\texpectedTitle:    \"Test Azure Logs\",\n\t\t\texpectedContains: \"[yellow]Loading Azure Logs data\",\n\t\t},\n\t\t{\n\t\t\tname:             \"still loading\",\n\t\t\tqueryfile:        \"/path/to/query.yml\",\n\t\t\tloading:          true,\n\t\t\texpectedTitle:    \"Test Azure Logs\",\n\t\t\texpectedContains: \"[yellow]Loading Azure Logs data\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\twidget := createTestWidget()\n\t\t\twidget.settings.Queryfile = tt.queryfile\n\t\t\twidget.lastError = tt.lastError\n\t\t\twidget.dataLoaded = tt.dataLoaded\n\t\t\twidget.loading = tt.loading\n\n\t\t\ttitle, content, _ := widget.content()\n\n\t\t\tassert.Equal(t, tt.expectedTitle, title)\n\t\t\tassert.Contains(t, content, tt.expectedContains)\n\t\t})\n\t}\n}\n\nfunc TestCalculateAdaptiveColumnWidths(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\ttableResp      *TableResp\n\t\tavailableWidth int\n\t\texpected       []int\n\t}{\n\t\t{\n\t\t\tname: \"empty headers\",\n\t\t\ttableResp: &TableResp{\n\t\t\t\tHeader: []string{},\n\t\t\t\tRows:   []TableRow{},\n\t\t\t},\n\t\t\tavailableWidth: 100,\n\t\t\texpected:       []int{},\n\t\t},\n\t\t{\n\t\t\tname: \"headers only\",\n\t\t\ttableResp: &TableResp{\n\t\t\t\tHeader: []string{\"Short\", \"VeryLongHeaderName\"},\n\t\t\t\tRows:   []TableRow{},\n\t\t\t},\n\t\t\tavailableWidth: 100,\n\t\t\texpected:       []int{minColumnWidth, 18}, // \"VeryLongHeaderName\" is 18 chars\n\t\t},\n\t\t{\n\t\t\tname: \"headers with data\",\n\t\t\ttableResp: &TableResp{\n\t\t\t\tHeader: []string{\"Col1\", \"Col2\"},\n\t\t\t\tRows: []TableRow{\n\t\t\t\t\t{\"ShortData\", \"VeryLongDataValue\"},\n\t\t\t\t\t{\"X\", \"Y\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tavailableWidth: 100,\n\t\t\texpected:       []int{9, 17}, // Max of header/data lengths\n\t\t},\n\t\t{\n\t\t\tname: \"width constraints\",\n\t\t\ttableResp: &TableResp{\n\t\t\t\tHeader: []string{\"VeryVeryVeryLongColumnNameThatExceedsMaxWidth\"},\n\t\t\t\tRows:   []TableRow{},\n\t\t\t},\n\t\t\tavailableWidth: 100,\n\t\t\texpected:       []int{maxColumnWidth}, // Capped at maxColumnWidth\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := calculateAdaptiveColumnWidths(tt.tableResp, tt.availableWidth)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestCalculateAdaptiveColumnWidths_Scaling(t *testing.T) {\n\t// Test case where columns need to be scaled down\n\ttableResp := &TableResp{\n\t\tHeader: []string{\"LongHeader1\", \"LongHeader2\", \"LongHeader3\"},\n\t\tRows:   []TableRow{},\n\t}\n\n\t// Very small available width to force scaling\n\tresult := calculateAdaptiveColumnWidths(tableResp, 20)\n\n\t// All columns should be scaled down to minimum width\n\tfor _, width := range result {\n\t\tassert.GreaterOrEqual(t, width, minColumnWidth)\n\t}\n\n\t// Total width + separators should not exceed available width significantly\n\ttotalWidth := 0\n\tfor _, width := range result {\n\t\ttotalWidth += width\n\t}\n\tseparatorSpace := (len(result) - 1) * 2\n\tassert.LessOrEqual(t, totalWidth+separatorSpace, 30) // Allow some margin for scaling\n}\n\n// Helper function to create a test widget\nfunc createTestWidget() *Widget {\n\tapp := tview.NewApplication()\n\tredrawChan := make(chan bool, 1)\n\n\tsettings := &Settings{\n\t\tCommon: &cfg.Common{\n\t\t\tTitle:   \"Test Azure Logs\",\n\t\t\tEnabled: true, // Enable by default for tests\n\t\t},\n\t\tQueryfile: \"/path/to/query.yml\",\n\t}\n\n\treturn NewWidget(app, redrawChan, nil, settings)\n}\n"
  },
  {
    "path": "modules/bamboohr/calendar.go",
    "content": "package bamboohr\n\ntype Calendar struct {\n\tItems []Item `xml:\"item\"`\n}\n\n/* -------------------- Public Functions -------------------- */\n\nfunc (calendar *Calendar) Holidays() []Item {\n\treturn calendar.filteredItems(\"holiday\")\n}\n\nfunc (calendar *Calendar) ItemsByType(itemType string) []Item {\n\tif itemType == \"timeOff\" {\n\t\treturn calendar.TimeOffs()\n\t}\n\n\treturn calendar.Holidays()\n}\n\nfunc (calendar *Calendar) TimeOffs() []Item {\n\treturn calendar.filteredItems(\"timeOff\")\n}\n\n/* -------------------- Private Functions -------------------- */\n\nfunc (calendar *Calendar) filteredItems(itemType string) []Item {\n\titems := []Item{}\n\n\tfor _, item := range calendar.Items {\n\t\tif item.Type == itemType {\n\t\t\titems = append(items, item)\n\t\t}\n\t}\n\n\treturn items\n}\n"
  },
  {
    "path": "modules/bamboohr/client.go",
    "content": "package bamboohr\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n)\n\n// A Client represents the data required to connect to the BambooHR API\ntype Client struct {\n\tapiBase   string\n\tapiKey    string\n\tsubdomain string\n}\n\n// NewClient creates and returns a new BambooHR client\nfunc NewClient(url string, apiKey string, subdomain string) *Client {\n\tclient := Client{\n\t\tapiBase:   url,\n\t\tapiKey:    apiKey,\n\t\tsubdomain: subdomain,\n\t}\n\n\treturn &client\n}\n\n/* -------------------- Public Functions -------------------- */\n\n// Away returns a string representation of the people who are out of the office during the defined period\nfunc (client *Client) Away(itemType, startDate, endDate string) []Item {\n\tcalendar, err := client.getWhoIsAway(startDate, endDate)\n\tif err != nil {\n\t\treturn []Item{}\n\t}\n\n\titems := calendar.ItemsByType(itemType)\n\n\treturn items\n}\n\n/* -------------------- Private Functions -------------------- */\n\n// getWhoIsAway is the private interface for retrieving structural data about who will be out of the office\n// This method does the actual communication with BambooHR and returns the raw Go\n// data structures used by the public interface\nfunc (client *Client) getWhoIsAway(startDate, endDate string) (cal Calendar, err error) {\n\tapiURL := fmt.Sprintf(\n\t\t\"%s/%s/v1/time_off/whos_out?start=%s&end=%s\",\n\t\tclient.apiBase,\n\t\tclient.subdomain,\n\t\tstartDate,\n\t\tendDate,\n\t)\n\n\tdata, err := Request(client.apiKey, apiURL)\n\tif err != nil {\n\t\treturn cal, err\n\t}\n\terr = xml.Unmarshal(data, &cal)\n\n\treturn\n}\n"
  },
  {
    "path": "modules/bamboohr/employee.go",
    "content": "package bamboohr\n\n/*\n* Note: this currently implements the minimum number of fields to fulfill the Away functionality.\n* Undoubtedly there are more fields than this to an employee\n */\ntype Employee struct {\n\tID   int    `xml:\"id,attr\"`\n\tName string `xml:\",chardata\"`\n}\n"
  },
  {
    "path": "modules/bamboohr/item.go",
    "content": "package bamboohr\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\ntype Item struct {\n\tEmployee Employee `xml:\"employee\"`\n\tEnd      string   `xml:\"end\"`\n\tHoliday  string   `xml:\"holiday\"`\n\tStart    string   `xml:\"start\"`\n\tType     string   `xml:\"type,attr\"`\n}\n\nfunc (item *Item) String() string {\n\treturn fmt.Sprintf(\"Item: %s, %s, %s, %s\", item.Type, item.Employee.Name, item.Start, item.End)\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (item *Item) IsOneDay() bool {\n\treturn item.Start == item.End\n}\n\nfunc (item *Item) Name() string {\n\tif (item.Employee != Employee{}) {\n\t\treturn item.Employee.Name\n\t}\n\n\treturn item.Holiday\n}\n\nfunc (item *Item) PrettyStart() string {\n\treturn wtf.PrettyDate(item.Start)\n}\n\nfunc (item *Item) PrettyEnd() string {\n\treturn wtf.PrettyDate(item.End)\n}\n"
  },
  {
    "path": "modules/bamboohr/request.go",
    "content": "package bamboohr\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n)\n\nfunc Request(apiKey string, apiURL string) ([]byte, error) {\n\treq, err := http.NewRequest(\"GET\", apiURL, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.SetBasicAuth(apiKey, \"x\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tdata, err := ParseBody(resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data, err\n}\n\nfunc ParseBody(resp *http.Response) ([]byte, error) {\n\tvar buffer bytes.Buffer\n\t_, err := buffer.ReadFrom(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn buffer.Bytes(), nil\n}\n"
  },
  {
    "path": "modules/bamboohr/settings.go",
    "content": "package bamboohr\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"BambooHR\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey    string `help:\"Your BambooHR API token.\"`\n\tsubdomain string `help:\"Your BambooHR API subdomain name.\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:    ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_BAMBOO_HR_TOKEN\"))),\n\t\tsubdomain: ymlConfig.UString(\"subdomain\", os.Getenv(\"WTF_BAMBOO_HR_SUBDOMAIN\")),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/bamboohr/widget.go",
    "content": "package bamboohr\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\nconst apiURI = \"https://api.bamboohr.com/api/gateway.php\"\n\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n\titems    []Item\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tclient := NewClient(\n\t\tapiURI,\n\t\twidget.settings.apiKey,\n\t\twidget.settings.subdomain,\n\t)\n\n\twidget.items = client.Away(\n\t\t\"timeOff\",\n\t\ttime.Now().Local().Format(wtf.DateFormat),\n\t\ttime.Now().Local().Format(wtf.DateFormat),\n\t)\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tstr := \"\"\n\tif len(widget.items) == 0 {\n\t\tstr = fmt.Sprintf(\"\\n\\n\\n\\n\\n\\n\\n\\n%s\", utils.CenterText(\"[grey]no one[white]\", 50))\n\t} else {\n\t\tfor _, item := range widget.items {\n\t\t\tstr += widget.format(item)\n\t\t}\n\t}\n\n\treturn widget.CommonSettings().Title, str, false\n}\n\nfunc (widget *Widget) format(item Item) string {\n\tvar str string\n\n\tif item.IsOneDay() {\n\t\tstr = fmt.Sprintf(\" [green]%s[white]\\n %s\\n\\n\", item.Name(), item.PrettyEnd())\n\t} else {\n\t\tstr = fmt.Sprintf(\" [green]%s[white]\\n %s - %s\\n\\n\", item.Name(), item.PrettyStart(), item.PrettyEnd())\n\t}\n\n\treturn str\n}\n"
  },
  {
    "path": "modules/bargraph/settings.go",
    "content": "package bargraph\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Bargraph\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/bargraph/widget.go",
    "content": "package bargraph\n\n/**************\nThis is a demo bargraph that just populates some random date/val data\n*/\n\nimport (\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget define wtf widget to register widget later\ntype Widget struct {\n\tview.BarGraph\n\n\ttviewApp *tview.Application\n}\n\n// NewWidget Make new instance of widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tBarGraph: view.NewBarGraph(tviewApp, redrawChan, \"Sample Bar Graph\", settings.Common),\n\n\t\ttviewApp: tviewApp,\n\t}\n\n\twidget.View.SetWrap(true)\n\twidget.View.SetWordWrap(true)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// MakeGraph - Load the dead drop stats\nfunc MakeGraph(widget *Widget) {\n\n\t//this could come from config\n\tconst lineCount = 8\n\tvar stats [lineCount]view.Bar\n\n\tbarTime := time.Now()\n\tfor i := 0; i < lineCount; i++ {\n\t\tbarTime = barTime.Add(time.Minute)\n\n\t\tbar := view.Bar{\n\t\t\tLabel:      barTime.Format(\"15:04\"),\n\t\t\tPercent:    rand.Intn(100-5) + 5,\n\t\t\tLabelColor: \"red\",\n\t\t}\n\n\t\tstats[i] = bar\n\t}\n\n\twidget.BuildBars(stats[:])\n\n}\n\n// Refresh & update after interval time\nfunc (widget *Widget) Refresh() {\n\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\twidget.View.Clear()\n\tMakeGraph(widget)\n}\n"
  },
  {
    "path": "modules/buildkite/client.go",
    "content": "package buildkite\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\ntype Pipeline struct {\n\tSlug string `json:\"slug\"`\n}\n\ntype Build struct {\n\tState    string   `json:\"state\"`\n\tPipeline Pipeline `json:\"pipeline\"`\n\tBranch   string   `json:\"branch\"`\n\tWebUrl   string   `json:\"web_url\"`\n}\n\nfunc (widget *Widget) getBuilds() ([]Build, error) {\n\tbuilds := []Build{}\n\n\tfor _, pipeline := range widget.settings.pipelines {\n\t\tbuildsForPipeline, err := widget.recentBuilds(pipeline)\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmostRecent := mostRecentBuildForBranches(buildsForPipeline, pipeline.branches)\n\t\tbuilds = append(builds, mostRecent...)\n\t}\n\n\treturn builds, nil\n}\n\nfunc (widget *Widget) recentBuilds(pipeline PipelineSettings) ([]Build, error) {\n\turl := fmt.Sprintf(\n\t\t\"https://api.buildkite.com/v2/organizations/%s/pipelines/%s/builds%s\",\n\t\twidget.settings.orgSlug,\n\t\tpipeline.slug,\n\t\tbranchesQuery(pipeline.branches),\n\t)\n\n\treq, err := http.NewRequest(\"GET\", url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Add(\"Authorization\", fmt.Sprintf(\"Bearer %s\", widget.settings.apiKey))\n\n\thttpClient := &http.Client{Transport: &http.Transport{\n\t\tProxy: http.ProxyFromEnvironment,\n\t}}\n\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\n\tbuilds := []Build{}\n\terr = utils.ParseJSON(&builds, resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn builds, nil\n}\n\nfunc branchesQuery(branches []string) string {\n\tif len(branches) == 0 {\n\t\treturn \"\"\n\t}\n\n\tif len(branches) == 1 {\n\t\treturn fmt.Sprintf(\"?branch=%s\", branches[0])\n\t}\n\n\tqueryString := fmt.Sprintf(\"?branch[]=%s\", branches[0])\n\tfor _, branch := range branches[1:] {\n\t\tqueryString += fmt.Sprintf(\"&branch[]=%s\", branch)\n\t}\n\n\treturn queryString\n}\n\nfunc mostRecentBuildForBranches(builds []Build, branches []string) []Build {\n\trecentBuilds := []Build{}\n\n\thaveMostRecentBuildForBranch := map[string]bool{}\n\tfor _, branch := range branches {\n\t\thaveMostRecentBuildForBranch[branch] = false\n\t}\n\n\tfor _, build := range builds {\n\t\tif !haveMostRecentBuildForBranch[build.Branch] {\n\t\t\thaveMostRecentBuildForBranch[build.Branch] = true\n\t\t\trecentBuilds = append(recentBuilds, build)\n\t\t}\n\t}\n\n\treturn recentBuilds\n}\n"
  },
  {
    "path": "modules/buildkite/keyboard.go",
    "content": "package buildkite\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n}\n"
  },
  {
    "path": "modules/buildkite/pipelines_display_data.go",
    "content": "package buildkite\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n)\n\ntype pipelinesDisplayData struct {\n\tbuildsForPipeline map[string][]Build\n\torderedPipelines  []string\n}\n\nfunc (data *pipelinesDisplayData) Content() string {\n\tmaxPipelineLength := getLongestLength(data.orderedPipelines)\n\n\tstr := \"\"\n\tfor _, pipeline := range data.orderedPipelines {\n\t\tstr += fmt.Sprintf(\"[white]%s\", padRight(pipeline, maxPipelineLength))\n\t\tfor _, build := range data.buildsForPipeline[pipeline] {\n\t\t\tstr += fmt.Sprintf(\"  [%s]%s[white]\", buildColor(build.State), build.Branch)\n\t\t}\n\t\tstr += \"\\n\"\n\t}\n\n\treturn str\n}\n\nfunc newPipelinesDisplayData(builds []Build) pipelinesDisplayData {\n\tgrouped := make(map[string][]Build)\n\n\tfor _, build := range builds {\n\t\tif _, ok := grouped[build.Pipeline.Slug]; ok {\n\t\t\tgrouped[build.Pipeline.Slug] = append(grouped[build.Pipeline.Slug], build)\n\t\t} else {\n\t\t\tgrouped[build.Pipeline.Slug] = []Build{}\n\t\t\tgrouped[build.Pipeline.Slug] = append(grouped[build.Pipeline.Slug], build)\n\t\t}\n\t}\n\n\torderedPipelines := make([]string, len(grouped))\n\ti := 0\n\tfor pipeline := range grouped {\n\t\torderedPipelines[i] = pipeline\n\t\ti++\n\t}\n\tsort.Strings(orderedPipelines)\n\n\tname := func(b1, b2 *Build) bool {\n\t\treturn b1.Branch < b2.Branch\n\t}\n\tfor _, builds := range grouped {\n\t\tByBuild(name).Sort(builds)\n\t}\n\n\treturn pipelinesDisplayData{\n\t\tbuildsForPipeline: grouped,\n\t\torderedPipelines:  orderedPipelines,\n\t}\n}\n\ntype ByBuild func(b1, b2 *Build) bool\n\nfunc (by ByBuild) Sort(builds []Build) {\n\tsorter := &buildSorter{\n\t\tbuilds: builds,\n\t\tby:     by,\n\t}\n\tsort.Sort(sorter)\n}\n\ntype buildSorter struct {\n\tbuilds []Build\n\tby     func(b1, b2 *Build) bool\n}\n\nfunc (bs *buildSorter) Len() int {\n\treturn len(bs.builds)\n}\n\nfunc (bs *buildSorter) Swap(i, j int) {\n\tbs.builds[i], bs.builds[j] = bs.builds[j], bs.builds[i]\n}\n\nfunc (bs *buildSorter) Less(i, j int) bool {\n\treturn bs.by(&bs.builds[i], &bs.builds[j])\n}\n\nfunc getLongestLength(strs []string) int {\n\tlongest := 0\n\n\tfor _, str := range strs {\n\t\tif len(str) > longest {\n\t\t\tlongest = len(str)\n\t\t}\n\t}\n\n\treturn longest\n}\n\nfunc padRight(text string, length int) string {\n\tpadLength := length - len(text)\n\n\tif padLength <= 0 {\n\t\treturn text[:length]\n\t}\n\n\treturn text + strings.Repeat(\" \", padLength)\n}\n\nfunc buildColor(state string) string {\n\tswitch state {\n\tcase \"passed\":\n\t\treturn \"green\"\n\tcase \"failed\":\n\t\treturn \"red\"\n\tdefault:\n\t\treturn \"yellow\"\n\t}\n}\n"
  },
  {
    "path": "modules/buildkite/settings.go",
    "content": "package buildkite\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultTitle     = \"Buildkite\"\n\tdefaultFocusable = true\n)\n\n// PipelineSettings defines the configuration properties for a pipeline\ntype PipelineSettings struct {\n\tslug     string\n\tbranches []string\n}\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey    string             `help:\"Your Buildkite API Token\"`\n\torgSlug   string             `help:\"Organization Slug\"`\n\tpipelines []PipelineSettings `help:\"An array of pipelines to get data from\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:    ymlConfig.UString(\"apiKey\", os.Getenv(\"WTF_BUILDKITE_TOKEN\")),\n\t\torgSlug:   ymlConfig.UString(\"organizationSlug\"),\n\t\tpipelines: buildPipelineSettings(ymlConfig),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\treturn &settings\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc buildPipelineSettings(ymlConfig *config.Config) []PipelineSettings {\n\tpipelines := []PipelineSettings{}\n\n\tfor slug := range ymlConfig.UMap(\"pipelines\") {\n\t\tbranches := utils.ToStrs(ymlConfig.UList(\"pipelines.\" + slug + \".branches\"))\n\t\tif len(branches) == 0 {\n\t\t\tbranches = []string{\"master\"}\n\t\t}\n\n\t\tpipeline := PipelineSettings{\n\t\t\tslug:     slug,\n\t\t\tbranches: branches,\n\t\t}\n\n\t\tpipelines = append(pipelines, pipeline)\n\t}\n\n\treturn pipelines\n}\n"
  },
  {
    "path": "modules/buildkite/widget.go",
    "content": "package buildkite\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\tsettings *Settings\n\n\tbuilds []Build\n\terr    error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\t\tsettings:   settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\twidget.View.SetScrollable(true)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tbuilds, err := widget.getBuilds()\n\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.builds = nil\n\t} else {\n\t\twidget.builds = builds\n\t\twidget.err = nil\n\t}\n\n\t// The last call should always be to the display function\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := fmt.Sprintf(\"%s - [green]%s\", widget.CommonSettings().Title, widget.settings.orgSlug)\n\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tdisplayData := newPipelinesDisplayData(widget.builds)\n\n\treturn title, displayData.Content(), false\n}\n"
  },
  {
    "path": "modules/cds/favorites/display.go",
    "content": "package cdsfavorites\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/ovh/cds/sdk\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tif len(widget.View.GetHighlights()) > 0 {\n\t\twidget.View.ScrollToHighlight()\n\t} else {\n\t\twidget.View.ScrollToBeginning()\n\t}\n\n\twidget.Items = make([]int64, 0)\n\n\tworkflow := widget.currentCDSWorkflow()\n\tif workflow == nil {\n\t\treturn \"\", \" Workflow not selected \", false\n\t}\n\n\t_, _, width, _ := widget.View.GetRect()\n\tstr := widget.settings.PaginationMarker(len(widget.workflows), widget.Idx, width) + \"\\n\"\n\ttitle := fmt.Sprintf(\"%s - %s\", widget.CommonSettings().Title, widget.title(workflow))\n\n\tstr += widget.displayWorkflowRuns(workflow)\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) title(workflow *sdk.Workflow) string {\n\treturn fmt.Sprintf(\n\t\t\"[%s]%s/%s[white]\",\n\t\twidget.settings.Colors.Title,\n\t\tworkflow.ProjectKey, workflow.Name,\n\t)\n}\n\nfunc (widget *Widget) displayWorkflowRuns(workflow *sdk.Workflow) string {\n\truns, _ := widget.client.WorkflowRunList(workflow.ProjectKey, workflow.Name, 0, 16)\n\n\twidget.SetItemCount(len(runs))\n\n\tif len(runs) == 0 {\n\t\treturn \" [grey]none[white]\\n\"\n\t}\n\n\tcontent := \"\"\n\tfor idx, run := range runs {\n\t\tvar tags string\n\t\tfor _, tag := range run.Tags {\n\t\t\ttoadd := true\n\t\t\tfor _, v := range widget.settings.hideTags {\n\t\t\t\tif v == tag.Tag {\n\t\t\t\t\ttoadd = false\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif toadd {\n\t\t\t\ttags = fmt.Sprintf(\"%s%s:%s \", tags, tag.Tag, tag.Value)\n\t\t\t}\n\t\t}\n\t\tcontent += fmt.Sprintf(`[%s][\"%d\"]%d %-6s[\"\"][gray] %s`, getStatusColor(run.Status), idx, run.Number, run.Status, tags)\n\t\tcontent += \"\\n\"\n\t\twidget.Items = append(widget.Items, run.Number)\n\t}\n\n\treturn content\n}\n\nfunc getStatusColor(status string) string {\n\tswitch status {\n\tcase sdk.StatusSuccess:\n\t\treturn \"green\"\n\tcase sdk.StatusBuilding, sdk.StatusWaiting:\n\t\treturn \"blue\"\n\tcase sdk.StatusFail:\n\t\treturn \"red\"\n\tcase sdk.StatusStopped:\n\t\treturn \"red\"\n\tcase sdk.StatusSkipped:\n\t\treturn \"grey\"\n\tcase sdk.StatusDisabled:\n\t\treturn \"grey\"\n\t}\n\treturn \"red\"\n}\n"
  },
  {
    "path": "modules/cds/favorites/keyboard.go",
    "content": "package cdsfavorites\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next workflow\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous workflow\")\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardChar(\"o\", widget.openWorkflow, \"Open workflow in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next workflow\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous workflow\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openWorkflow, \"Open workflow in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/cds/favorites/settings.go",
    "content": "package cdsfavorites\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"CDS Favorites\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\ttoken    string `help:\"Your CDS API token.\"`\n\tapiURL   string `help:\"Your CDS API URL.\"`\n\tuiURL    string\n\thideTags []string `help:\"Hide some workflow tags.\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\ttoken:    ymlConfig.UString(\"token\", ymlConfig.UString(\"token\", os.Getenv(\"CDS_TOKEN\"))),\n\t\tapiURL:   ymlConfig.UString(\"apiURL\", os.Getenv(\"CDS_API_URL\")),\n\t\thideTags: utils.ToStrs(ymlConfig.UList(\"hideTags\")),\n\t}\n\n\tsettings.SetDocumentationPath(\"cds/favorites\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/cds/favorites/widget.go",
    "content": "package cdsfavorites\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/ovh/cds/sdk\"\n\t\"github.com/ovh/cds/sdk/cdsclient\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget define wtf widget to register widget later\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tworkflows []sdk.Workflow\n\n\tclient cdsclient.Interface\n\n\tsettings *Settings\n\tSelected int\n\tmaxItems int\n\tItems    []int64\n}\n\n// NewWidget creates a new instance of the widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"workflow\", \"workflows\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\twidget.View.SetRegions(true)\n\twidget.SetDisplayFunction(widget.display)\n\n\twidget.Unselect()\n\n\twidget.client = cdsclient.New(cdsclient.Config{\n\t\tHost:                               settings.apiURL,\n\t\tBuiltinConsumerAuthenticationToken: settings.token,\n\t})\n\n\tconfig, _ := widget.client.ConfigUser()\n\n\tif config.URLUI != \"\" {\n\t\twidget.settings.uiURL = config.URLUI\n\t}\n\n\twidget.workflows = widget.buildWorkflowsCollection()\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// SetItemCount sets the amount of workflows throughout the widgets display creation\nfunc (widget *Widget) SetItemCount(items int) {\n\twidget.maxItems = items\n}\n\n// GetItemCount returns the amount of workflows calculated so far as an int\nfunc (widget *Widget) GetItemCount() int {\n\treturn widget.maxItems\n}\n\n// GetSelected returns the index of the currently highlighted item as an int\nfunc (widget *Widget) GetSelected() int {\n\tif widget.Selected < 0 {\n\t\treturn 0\n\t}\n\treturn widget.Selected\n}\n\n// Next cycles the currently highlighted text down\nfunc (widget *Widget) Next() {\n\twidget.Selected++\n\tif widget.Selected >= widget.maxItems {\n\t\twidget.Selected = 0\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Prev cycles the currently highlighted text up\nfunc (widget *Widget) Prev() {\n\twidget.Selected--\n\tif widget.Selected < 0 {\n\t\twidget.Selected = widget.maxItems - 1\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Unselect stops highlighting the text and jumps the scroll position to the top\nfunc (widget *Widget) Unselect() {\n\twidget.Selected = -1\n\twidget.View.Highlight()\n\twidget.View.ScrollToBeginning()\n}\n\n// Refresh reloads the data\nfunc (widget *Widget) Refresh() {\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) buildWorkflowsCollection() []sdk.Workflow {\n\tworkflows := []sdk.Workflow{}\n\tdata, _ := widget.client.Navbar()\n\tfor _, v := range data {\n\t\tif v.Favorite && v.WorkflowName != \"\" {\n\t\t\tworkflows = append(workflows, sdk.Workflow{ProjectKey: v.Key, Name: v.WorkflowName})\n\t\t}\n\t}\n\treturn workflows\n}\n\nfunc (widget *Widget) currentCDSWorkflow() *sdk.Workflow {\n\tif len(widget.workflows) == 0 {\n\t\treturn nil\n\t}\n\n\tif widget.Idx < 0 || widget.Idx >= len(widget.workflows) {\n\t\twidget.Idx = 0\n\t}\n\n\tp := widget.workflows[widget.Idx]\n\treturn &p\n}\n\nfunc (widget *Widget) openWorkflow() {\n\tcurrentSelection := widget.View.GetHighlights()\n\tif widget.Selected >= 0 && currentSelection[0] != \"\" {\n\t\twf := widget.currentCDSWorkflow()\n\t\turl := fmt.Sprintf(\"%s/project/%s/workflow/%s/run/%d\",\n\t\t\twidget.settings.uiURL, wf.ProjectKey, wf.Name, widget.Items[widget.Selected])\n\t\tutils.OpenFile(url)\n\t}\n}\n"
  },
  {
    "path": "modules/cds/queue/display.go",
    "content": "package cdsqueue\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ovh/cds/sdk\"\n\t\"github.com/ovh/cds/sdk/cdsclient\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tif len(widget.View.GetHighlights()) > 0 {\n\t\twidget.View.ScrollToHighlight()\n\t} else {\n\t\twidget.View.ScrollToBeginning()\n\t}\n\n\twidget.Items = make([]sdk.WorkflowNodeJobRun, 0)\n\tfilter := widget.currentFilter()\n\t_, _, width, _ := widget.View.GetRect()\n\n\tstr := widget.settings.PaginationMarker(len(widget.filters), widget.Idx, width) + \"\\n\"\n\tstr += widget.displayQueue(filter)\n\n\ttitle := fmt.Sprintf(\"%s - %s\", widget.CommonSettings().Title, widget.title(filter))\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) title(filter string) string {\n\treturn fmt.Sprintf(\n\t\t\"[%s]%d - %s[white]\",\n\t\twidget.settings.Colors.Title,\n\t\twidget.maxItems,\n\t\tfilter,\n\t)\n}\n\nfunc (widget *Widget) displayQueue(filter string) string {\n\truns, _ := widget.client.QueueWorkflowNodeJobRun(cdsclient.Status(filter))\n\n\twidget.SetItemCount(len(runs))\n\n\tif len(runs) == 0 {\n\t\treturn \" [grey]none[white]\\n\"\n\t}\n\n\tvar content string\n\tfor idx, job := range runs {\n\t\tcontent += fmt.Sprintf(`[grey][\"%d\"]%s`,\n\t\t\tidx, widget.generateQueueJobLine(job.Parameters, job.Job, time.Since(job.Queued), job.BookedBy, job.Status))\n\n\t\twidget.Items = append(widget.Items, job)\n\t}\n\n\treturn content\n}\n\nfunc (widget *Widget) generateQueueJobLine(parameters []sdk.Parameter, executedJob sdk.ExecutedJob,\n\tduration time.Duration, bookedBy sdk.BookedBy, status string) string {\n\tprj := getVarsInPbj(\"cds.project\", parameters)\n\tworkflow := getVarsInPbj(\"cds.workflow\", parameters)\n\tnode := getVarsInPbj(\"cds.node\", parameters)\n\trun := getVarsInPbj(\"cds.run\", parameters)\n\ttriggeredBy := getVarsInPbj(\"cds.triggered_by.username\", parameters)\n\n\trow := make([]string, 6)\n\trow[0] = pad(sdk.Round(duration, time.Second).String(), 9)\n\trow[2] = pad(run, 7)\n\trow[3] = pad(prj+\"/\"+workflow+\"/\"+node, 40)\n\n\tswitch {\n\tcase status == sdk.StatusBuilding:\n\t\trow[1] = pad(fmt.Sprintf(\" %s.%s \", executedJob.WorkerName, executedJob.WorkerID), 27)\n\tcase bookedBy.ID != 0:\n\t\trow[1] = pad(fmt.Sprintf(\" %s.%d \", bookedBy.Name, bookedBy.ID), 27)\n\tdefault:\n\t\trow[1] = pad(\"\", 27)\n\t}\n\n\trow[4] = fmt.Sprintf(\"➤ %s\", pad(triggeredBy, 17))\n\n\tc := \"grey\"\n\tif status == sdk.StatusWaiting {\n\t\tif duration > 120*time.Second {\n\t\t\tc = \"red\"\n\t\t} else if duration > 50*time.Second {\n\t\t\tc = \"yellow\"\n\t\t}\n\t}\n\n\treturn fmt.Sprintf(\"[%s]%s [grey]%s %s %s %s\\n\", c, row[0], row[1], row[2], row[3], row[4])\n}\n\nfunc pad(t string, size int) string {\n\tif len(t) > size {\n\t\treturn t[0:size-3] + \"...\"\n\t}\n\treturn t + strings.Repeat(\" \", size-len(t))\n}\n\nfunc getVarsInPbj(key string, ps []sdk.Parameter) string {\n\tfor _, p := range ps {\n\t\tif p.Name == key {\n\t\t\treturn p.Value\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "modules/cds/queue/keyboard.go",
    "content": "package cdsqueue\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next workflow\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous workflow\")\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next filter\")\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous filter\")\n\twidget.SetKeyboardChar(\"o\", widget.openWorkflow, \"Open workflow in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next workflow\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous workflow\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next filter\")\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous filter\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openWorkflow, \"Open workflow in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/cds/queue/settings.go",
    "content": "package cdsqueue\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"CDS Queue\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\ttoken  string `help:\"Your CDS API token.\"`\n\tapiURL string `help:\"Your CDS API URL.\"`\n\tuiURL  string\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\ttoken:  ymlConfig.UString(\"token\", ymlConfig.UString(\"token\", os.Getenv(\"CDS_TOKEN\"))),\n\t\tapiURL: ymlConfig.UString(\"apiURL\", os.Getenv(\"CDS_API_URL\")),\n\t}\n\n\tsettings.SetDocumentationPath(\"cds/queue\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/cds/queue/widget.go",
    "content": "package cdsqueue\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/ovh/cds/sdk\"\n\t\"github.com/ovh/cds/sdk/cdsclient\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget define wtf widget to register widget later\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tfilters []string\n\n\tclient cdsclient.Interface\n\n\tsettings *Settings\n\tSelected int\n\tmaxItems int\n\tItems    []sdk.WorkflowNodeJobRun\n}\n\n// NewWidget creates a new instance of the widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"workflow\", \"workflows\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\twidget.View.SetRegions(true)\n\twidget.SetDisplayFunction(widget.display)\n\n\twidget.Unselect()\n\twidget.filters = []string{sdk.StatusWaiting, sdk.StatusBuilding}\n\n\twidget.client = cdsclient.New(cdsclient.Config{\n\t\tHost:                               settings.apiURL,\n\t\tBuiltinConsumerAuthenticationToken: settings.token,\n\t})\n\n\tconfig, _ := widget.client.ConfigUser()\n\n\tif config.URLUI != \"\" {\n\t\twidget.settings.uiURL = config.URLUI\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// SetItemCount sets the amount of workflows throughout the widgets display creation\nfunc (widget *Widget) SetItemCount(items int) {\n\twidget.maxItems = items\n}\n\n// GetItemCount returns the amount of workflows calculated so far as an int\nfunc (widget *Widget) GetItemCount() int {\n\treturn widget.maxItems\n}\n\n// GetSelected returns the index of the currently highlighted item as an int\nfunc (widget *Widget) GetSelected() int {\n\tif widget.Selected < 0 {\n\t\treturn 0\n\t}\n\treturn widget.Selected\n}\n\n// Next cycles the currently highlighted text down\nfunc (widget *Widget) Next() {\n\twidget.Selected++\n\tif widget.Selected >= widget.maxItems {\n\t\twidget.Selected = 0\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Prev cycles the currently highlighted text up\nfunc (widget *Widget) Prev() {\n\twidget.Selected--\n\tif widget.Selected < 0 {\n\t\twidget.Selected = widget.maxItems - 1\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Unselect stops highlighting the text and jumps the scroll position to the top\nfunc (widget *Widget) Unselect() {\n\twidget.Selected = -1\n\twidget.View.Highlight()\n\twidget.View.ScrollToBeginning()\n}\n\n// Refresh reloads the data\nfunc (widget *Widget) Refresh() {\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) currentFilter() string {\n\tif len(widget.filters) == 0 {\n\t\treturn sdk.StatusWaiting\n\t}\n\n\tif widget.Idx < 0 || widget.Idx >= len(widget.filters) {\n\t\twidget.Idx = 0\n\t\treturn sdk.StatusWaiting\n\t}\n\n\treturn widget.filters[widget.Idx]\n}\n\nfunc (widget *Widget) openWorkflow() {\n\tcurrentSelection := widget.View.GetHighlights()\n\tif widget.Selected >= 0 && currentSelection[0] != \"\" {\n\t\tjob := widget.Items[widget.Selected]\n\t\tprj := getVarsInPbj(\"cds.project\", job.Parameters)\n\t\tworkflow := getVarsInPbj(\"cds.workflow\", job.Parameters)\n\t\trunNumber := getVarsInPbj(\"cds.run.number\", job.Parameters)\n\t\turl := fmt.Sprintf(\"%s/project/%s/workflow/%s/run/%s\", widget.settings.uiURL, prj, workflow, runNumber)\n\t\tutils.OpenFile(url)\n\t}\n}\n"
  },
  {
    "path": "modules/cds/status/display.go",
    "content": "package cdsstatus\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/ovh/cds/sdk\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tif len(widget.View.GetHighlights()) > 0 {\n\t\twidget.View.ScrollToHighlight()\n\t} else {\n\t\twidget.View.ScrollToBeginning()\n\t}\n\n\twidget.Items = make([]sdk.MonitoringStatusLine, 0)\n\tstr := widget.displayStatus()\n\ttitle := widget.CommonSettings().Title\n\treturn title, str, false\n}\n\nfunc (widget *Widget) displayStatus() string {\n\tstatus, err := widget.client.MonStatus()\n\n\tif err != nil || len(status.Lines) == 0 {\n\t\treturn fmt.Sprintf(\" [red]Error: %v[white]\\n\", err.Error())\n\t}\n\n\twidget.SetItemCount(len(status.Lines))\n\n\tvar (\n\t\tglobal     []string\n\t\tglobalWarn []string\n\t\tglobalRed  []string\n\t\tok         []string\n\t\twarn       []string\n\t\tred        []string\n\t)\n\n\tfor _, line := range status.Lines {\n\t\tswitch {\n\t\tcase line.Status == sdk.MonitoringStatusWarn && strings.Contains(line.Component, \"Global\"):\n\t\t\tglobalWarn = append(globalWarn, line.String())\n\t\tcase line.Status != sdk.MonitoringStatusOK && strings.Contains(line.Component, \"Global\"):\n\t\t\tglobalRed = append(globalRed, line.String())\n\t\tcase strings.Contains(line.Component, \"Global\"):\n\t\t\tglobal = append(global, line.String())\n\t\tcase line.Status == sdk.MonitoringStatusWarn:\n\t\t\twarn = append(warn, line.String())\n\t\tcase line.Status == sdk.MonitoringStatusOK:\n\t\t\tok = append(ok, line.String())\n\t\tdefault:\n\t\t\tred = append(red, line.String())\n\t\t}\n\t}\n\tvar idx int\n\tvar content string\n\tfor _, v := range globalRed {\n\t\tcontent += fmt.Sprintf(\"[grey][\\\"%d\\\"][red]%s\\n\", idx, v)\n\t\tidx++\n\t}\n\tfor _, v := range globalWarn {\n\t\tcontent += fmt.Sprintf(\"[grey][\\\"%d\\\"][yellow]%s\\n\", idx, v)\n\t\tidx++\n\t}\n\tfor _, v := range global {\n\t\tcontent += fmt.Sprintf(\"[grey][\\\"%d\\\"][grey]%s\\n\", idx, v)\n\t\tidx++\n\t}\n\tfor _, v := range red {\n\t\tcontent += fmt.Sprintf(\"[grey][\\\"%d\\\"][red]%s\\n\", idx, v)\n\t\tidx++\n\t}\n\tfor _, v := range warn {\n\t\tcontent += fmt.Sprintf(\"[grey][\\\"%d\\\"][yellow]%s\\n\", idx, v)\n\t\tidx++\n\t}\n\tfor _, v := range ok {\n\t\tcontent += fmt.Sprintf(\"[grey][\\\"%d\\\"][grey]%s\\n\", idx, v)\n\t\tidx++\n\t}\n\treturn content\n}\n"
  },
  {
    "path": "modules/cds/status/keyboard.go",
    "content": "package cdsstatus\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next line\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous line\")\n\twidget.SetKeyboardChar(\"o\", widget.openWorkflow, \"Open status in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next line\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous line\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openWorkflow, \"Open status in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/cds/status/settings.go",
    "content": "package cdsstatus\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"CDS Status\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\ttoken  string `help:\"Your CDS API token.\"`\n\tapiURL string `help:\"Your CDS API URL.\"`\n\tuiURL  string\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\ttoken:  ymlConfig.UString(\"token\", ymlConfig.UString(\"token\", os.Getenv(\"CDS_TOKEN\"))),\n\t\tapiURL: ymlConfig.UString(\"apiURL\", os.Getenv(\"CDS_API_URL\")),\n\t}\n\n\tsettings.SetDocumentationPath(\"cds/status\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/cds/status/widget.go",
    "content": "package cdsstatus\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/ovh/cds/sdk\"\n\t\"github.com/ovh/cds/sdk/cdsclient\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget define wtf widget to register widget later\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tfilters []string\n\n\tclient cdsclient.Interface\n\n\tsettings *Settings\n\tSelected int\n\tmaxItems int\n\tItems    []sdk.MonitoringStatusLine\n}\n\n// NewWidget creates a new instance of the widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"workflow\", \"workflows\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\twidget.View.SetRegions(true)\n\twidget.SetDisplayFunction(widget.display)\n\n\twidget.Unselect()\n\twidget.filters = []string{sdk.StatusWaiting, sdk.StatusBuilding}\n\n\twidget.client = cdsclient.New(cdsclient.Config{\n\t\tHost:                               settings.apiURL,\n\t\tBuiltinConsumerAuthenticationToken: settings.token,\n\t})\n\n\tconfig, _ := widget.client.ConfigUser()\n\n\tif config.URLUI != \"\" {\n\t\twidget.settings.uiURL = config.URLUI\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// SetItemCount sets the amount of line throughout the widgets display creation\nfunc (widget *Widget) SetItemCount(items int) {\n\twidget.maxItems = items\n}\n\n// GetItemCount returns the amount of line calculated so far as an int\nfunc (widget *Widget) GetItemCount() int {\n\treturn widget.maxItems\n}\n\n// GetSelected returns the index of the currently highlighted item as an int\nfunc (widget *Widget) GetSelected() int {\n\tif widget.Selected < 0 {\n\t\treturn 0\n\t}\n\treturn widget.Selected\n}\n\n// Next cycles the currently highlighted text down\nfunc (widget *Widget) Next() {\n\twidget.Selected++\n\tif widget.Selected >= widget.maxItems {\n\t\twidget.Selected = 0\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Prev cycles the currently highlighted text up\nfunc (widget *Widget) Prev() {\n\twidget.Selected--\n\tif widget.Selected < 0 {\n\t\twidget.Selected = widget.maxItems - 1\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Unselect stops highlighting the text and jumps the scroll position to the top\nfunc (widget *Widget) Unselect() {\n\twidget.Selected = -1\n\twidget.View.Highlight()\n\twidget.View.ScrollToBeginning()\n}\n\n// Refresh reloads the data\nfunc (widget *Widget) Refresh() {\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) openWorkflow() {\n\tcurrentSelection := widget.View.GetHighlights()\n\tif widget.Selected >= 0 && currentSelection[0] != \"\" {\n\t\turl := fmt.Sprintf(\"%s/admin/services\", widget.settings.uiURL)\n\t\tutils.OpenFile(url)\n\t}\n}\n"
  },
  {
    "path": "modules/circleci/build.go",
    "content": "package circleci\n\ntype Build struct {\n\tAuthorEmail string `json:\"author_email\"`\n\tAuthorName  string `json:\"author_name\"`\n\tBranch      string `json:\"branch\"`\n\tBuildNum    int    `json:\"build_num\"`\n\tReponame    string `json:\"reponame\"`\n\tStatus      string `json:\"status\"`\n}\n"
  },
  {
    "path": "modules/circleci/client.go",
    "content": "package circleci\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\ntype Client struct {\n\tapiKey string\n}\n\nfunc NewClient(apiKey string) *Client {\n\tclient := Client{\n\t\tapiKey: apiKey,\n\t}\n\n\treturn &client\n}\n\nfunc (client *Client) BuildsFor() ([]*Build, error) {\n\tbuilds := []*Build{}\n\n\tresp, err := client.circleRequest(\"recent-builds\")\n\tif err != nil {\n\t\treturn builds, err\n\t}\n\n\terr = utils.ParseJSON(&builds, bytes.NewReader(resp))\n\tif err != nil {\n\t\treturn builds, err\n\t}\n\n\treturn builds, nil\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nvar (\n\tcircleAPIURL = &url.URL{Scheme: \"https\", Host: \"circleci.com\", Path: \"/api/v1/\"}\n)\n\nfunc (client *Client) circleRequest(path string) ([]byte, error) {\n\tparams := url.Values{}\n\tparams.Add(\"circle-token\", client.apiKey)\n\n\turl := circleAPIURL.ResolveReference(&url.URL{Path: path, RawQuery: params.Encode()})\n\n\treq, err := http.NewRequest(\"GET\", url.String(), http.NoBody)\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn body, nil\n}\n"
  },
  {
    "path": "modules/circleci/settings.go",
    "content": "package circleci\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"CircleCI\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey         string `help:\"Your CircleCI API token.\"`\n\tnumberOfBuilds int    `help:\"The number of build, 10 by default\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:         ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_CIRCLE_API_KEY\"))),\n\t\tnumberOfBuilds: ymlConfig.UInt(\"numberOfBuilds\", 10),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/circleci/widget.go",
    "content": "package circleci\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\t*Client\n\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\t\tClient:     NewClient(settings.apiKey),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tbuilds, err := widget.BuildsFor()\n\n\ttitle := fmt.Sprintf(\"%s - Builds\", widget.CommonSettings().Title)\n\tvar str string\n\twrap := false\n\tif err != nil {\n\t\twrap = true\n\t\tstr = err.Error()\n\t} else {\n\t\tfor idx, build := range builds {\n\t\t\tif idx > widget.settings.numberOfBuilds {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tstr += fmt.Sprintf(\n\t\t\t\t\"[%s] %s-%d (%s) [white]%s\\n\",\n\t\t\t\tbuildColor(build),\n\t\t\t\tbuild.Reponame,\n\t\t\t\tbuild.BuildNum,\n\t\t\t\tbuild.Branch,\n\t\t\t\tbuild.AuthorName,\n\t\t\t)\n\t\t}\n\t}\n\n\treturn title, str, wrap\n}\n\nfunc buildColor(build *Build) string {\n\tswitch build.Status {\n\tcase \"failed\":\n\t\treturn \"red\"\n\tcase \"running\":\n\t\treturn \"yellow\"\n\tcase \"success\":\n\t\treturn \"green\"\n\tcase \"fixed\":\n\t\treturn \"green\"\n\tdefault:\n\t\treturn \"white\"\n\t}\n}\n"
  },
  {
    "path": "modules/clocks/clock.go",
    "content": "package clocks\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\ntype Clock struct {\n\tLabel    string\n\tLocation *time.Location\n}\n\nfunc NewClock(label string, timeLoc *time.Location) Clock {\n\tclock := Clock{\n\t\tLabel:    label,\n\t\tLocation: timeLoc,\n\t}\n\n\treturn clock\n}\n\nfunc BuildClock(label string, location string) (clock Clock, err error) {\n\ttimeLoc, err := time.LoadLocation(sanitizeLocation(location))\n\tif err != nil {\n\t\treturn Clock{}, err\n\t}\n\treturn NewClock(label, timeLoc), nil\n}\n\nfunc (clock *Clock) Date(dateFormat string) string {\n\treturn clock.LocalTime().Format(dateFormat)\n}\n\nfunc (clock *Clock) LocalTime() time.Time {\n\treturn clock.ToLocal(time.Now())\n}\n\nfunc (clock *Clock) ToLocal(t time.Time) time.Time {\n\treturn t.In(clock.Location)\n}\n\nfunc (clock *Clock) Time(timeFormat string) string {\n\treturn clock.LocalTime().Format(timeFormat)\n}\n\nfunc sanitizeLocation(locStr string) string {\n\treturn strings.ReplaceAll(locStr, \" \", \"_\")\n}\n"
  },
  {
    "path": "modules/clocks/clock_collection.go",
    "content": "package clocks\n\nimport (\n\t\"sort\"\n\t\"time\"\n)\n\ntype ClockCollection struct {\n\tClocks []Clock\n}\n\nfunc (clocks *ClockCollection) Sorted(sortOrder string) []Clock {\n\n\tswitch sortOrder {\n\tcase \"natural\":\n\t\t// do nothing\n\tcase \"chronological\":\n\t\tclocks.SortedChronologically()\n\tcase \"reversechronological\":\n\t\tclocks.SortedReverseChronologically()\n\tdefault:\n\t\tclocks.SortedAlphabetically()\n\t}\n\n\treturn clocks.Clocks\n}\n\nfunc (clocks *ClockCollection) SortedAlphabetically() {\n\tsort.Slice(clocks.Clocks, func(i, j int) bool {\n\t\tclock := clocks.Clocks[i]\n\t\tother := clocks.Clocks[j]\n\n\t\treturn clock.Label < other.Label\n\t})\n}\n\nfunc (clocks *ClockCollection) SortedChronologically() {\n\tnow := time.Now()\n\tsort.Slice(clocks.Clocks, func(i, j int) bool {\n\t\tclock := clocks.Clocks[i]\n\t\tother := clocks.Clocks[j]\n\n\t\treturn clock.ToLocal(now).String() < other.ToLocal(now).String()\n\t})\n}\n\nfunc (clocks *ClockCollection) SortedReverseChronologically() {\n\tnow := time.Now()\n\tsort.Slice(clocks.Clocks, func(i, j int) bool {\n\t\tclock := clocks.Clocks[i]\n\t\tother := clocks.Clocks[j]\n\n\t\treturn clock.ToLocal(now).String() > other.ToLocal(now).String()\n\t})\n}\n"
  },
  {
    "path": "modules/clocks/display.go",
    "content": "package clocks\n\nimport \"fmt\"\n\nfunc (widget *Widget) display(clocks []Clock, dateFormat string, timeFormat string) {\n\tstr := \"\"\n\n\tlocationWidth := 12\n\tfor _, clock := range clocks {\n\t\tif len(clock.Label) > locationWidth {\n\t\t\tlocationWidth = len(clock.Label) + 2\n\t\t}\n\t}\n\n\tif len(clocks) == 0 {\n\t\tstr = fmt.Sprintf(\"\\n%s\", \" no timezone data available\")\n\t} else {\n\t\tfor idx, clock := range clocks {\n\t\t\tstr += fmt.Sprintf(\n\t\t\t\t\" [%s]%-*s %-10s %7s[white]\\n\",\n\t\t\t\twidget.CommonSettings().RowColor(idx),\n\t\t\t\tlocationWidth,\n\t\t\t\tclock.Label,\n\t\t\t\tclock.Time(timeFormat),\n\t\t\t\tclock.Date(dateFormat),\n\t\t\t)\n\t\t}\n\t}\n\n\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, str, false })\n}\n"
  },
  {
    "path": "modules/clocks/settings.go",
    "content": "package clocks\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Clocks\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tdateFormat string  `help:\"The format of the date string for all clocks.\" values:\"Any valid Go date layout which is handled by Time.Format. Defaults to Jan 2.\"`\n\ttimeFormat string  `help:\"The format of the time string for all clocks.\" values:\"Any valid Go time layout which is handled by Time.Format. Defaults to 15:04 MST.\"`\n\tlocations  []Clock `help:\"Defines the timezones for the world clocks that you want to display. key is a unique label that will be displayed in the UI. value is a timezone name.\" values:\"Any TZ database timezone.\"`\n\tsort       string  `help:\"Defines the display order of the clocks in the widget.\" values:\"'alphabetical', 'chronological', or 'natural. 'alphabetical' will sort in ascending order by key, 'chronological' will sort in ascending order by date/time, 'natural' will keep ordering as per the config.\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tdateFormat: ymlConfig.UString(\"dateFormat\", utils.SimpleDateFormat),\n\t\ttimeFormat: ymlConfig.UString(\"timeFormat\", utils.SimpleTimeFormat),\n\t\tlocations:  buildLocations(ymlConfig),\n\t\tsort:       ymlConfig.UString(\"sort\"),\n\t}\n\n\treturn &settings\n}\n\nfunc buildLocations(ymlConfig *config.Config) []Clock {\n\tclocks := []Clock{}\n\tlocations, err := ymlConfig.Map(\"locations\")\n\tif err == nil {\n\t\tfor k, v := range locations {\n\t\t\tname := k\n\t\t\tzone := v.(string)\n\t\t\tclock, err := BuildClock(name, zone)\n\t\t\tif err == nil {\n\t\t\t\tclocks = append(clocks, clock)\n\t\t\t}\n\t\t}\n\t\treturn clocks\n\t}\n\n\tlistLocations := ymlConfig.UList(\"locations\")\n\tfor _, location := range listLocations {\n\t\tif location, ok := location.(map[string]interface{}); ok {\n\t\t\tfor k, v := range location {\n\t\t\t\tname := k\n\t\t\t\tzone := v.(string)\n\t\t\t\tclock, err := BuildClock(name, zone)\n\t\t\t\tif err == nil {\n\t\t\t\t\tclocks = append(clocks, clock)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn clocks\n}\n"
  },
  {
    "path": "modules/clocks/widget.go",
    "content": "package clocks\n\nimport (\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tclockColl  ClockCollection\n\tdateFormat string\n\ttimeFormat string\n\tsettings   *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings:   settings,\n\t\tdateFormat: settings.dateFormat,\n\t\ttimeFormat: settings.timeFormat,\n\t}\n\n\twidget.clockColl = widget.buildClockCollection()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\tsortedClocks := widget.clockColl.Sorted(widget.settings.sort)\n\twidget.display(sortedClocks, widget.dateFormat, widget.timeFormat)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) buildClockCollection() ClockCollection {\n\tclockColl := ClockCollection{}\n\n\tclockColl.Clocks = widget.settings.locations\n\n\treturn clockColl\n}\n"
  },
  {
    "path": "modules/cmdrunner/settings.go",
    "content": "package cmdrunner\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"CmdRunner\"\n)\n\n// Settings for the cmdrunner widget\ntype Settings struct {\n\t*cfg.Common\n\n\targs              []string `help:\"The arguments to the command, with each item as an element in an array. Example: for curl -I cisco.com, the arguments array would be ['-I', 'cisco.com'].\"`\n\tcmd               string   `help:\"The terminal command to be run, withouth the arguments. Ie: ping, whoami, curl.\"`\n\ttail              bool     `help:\"Automatically scroll to the end of the command output.\"`\n\tpty               bool     `help:\"Run the command in a pseudo-terminal. Some apps will behave differently if they feel in a terminal. For example, some apps will produce colorized output in a terminal, and non-colorized output otherwise. Default false\" optional:\"true\"`\n\tptySuppressErrors bool     `help:\"Do not display pty errors. Some apps producing colorized output may result in trailing errors. This will attempt to hide them, use only when necessary. Default false\" optional:\"true\"`\n\tmaxLines          int      `help:\"Maximum number of lines kept in the buffer.\"`\n\tworkingDir        string   `help:\"Working directory for command to run in\" optional:\"true\"`\n\n\t// The dimensions of the module\n\twidth  int\n\theight int\n}\n\n// NewSettingsFromYAML loads the cmdrunner portion of the WTF config\nfunc NewSettingsFromYAML(name string, moduleConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, moduleConfig, globalConfig),\n\n\t\targs:              utils.ToStrs(moduleConfig.UList(\"args\")),\n\t\tworkingDir:        moduleConfig.UString(\"workingDir\", \".\"),\n\t\tcmd:               moduleConfig.UString(\"cmd\"),\n\t\tpty:               moduleConfig.UBool(\"pty\", false),\n\t\tptySuppressErrors: moduleConfig.UBool(\"ptySuppressErrors\", false),\n\t\ttail:              moduleConfig.UBool(\"tail\", false),\n\t\tmaxLines:          moduleConfig.UInt(\"maxLines\", 256),\n\t}\n\n\twidth, height, err := utils.CalculateDimensions(moduleConfig, globalConfig)\n\tif err == nil {\n\t\tsettings.width = width\n\t\tsettings.height = height\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/cmdrunner/widget.go",
    "content": "package cmdrunner\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\n\t\"github.com/creack/pty\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/logger\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget contains the data for this widget\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n\n\tm          sync.Mutex\n\tbuffer     *bytes.Buffer\n\trunChan    chan bool\n\tredrawChan chan bool\n}\n\n// NewWidget creates a new instance of the widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t\tbuffer:   &bytes.Buffer{},\n\t}\n\n\twidget.View.SetWrap(true)\n\twidget.View.SetScrollable(true)\n\n\twidget.runChan = make(chan bool)\n\twidget.redrawChan = make(chan bool)\n\tgo runCommandLoop(&widget)\n\tgo redrawLoop(&widget)\n\twidget.runChan <- true\n\n\treturn &widget\n}\n\n// Refresh signals the runCommandLoop to continue, or triggers a re-draw if the\n// command is still running.\nfunc (widget *Widget) Refresh() {\n\t// Try to run the command. If the command is still running, let it keep\n\t// running and do a refresh instead. Otherwise, the widget will redraw when\n\t// the command completes.\n\tselect {\n\tcase widget.runChan <- true:\n\tdefault:\n\t\twidget.redrawChan <- true\n\t}\n}\n\n// String returns the string representation of the widget\nfunc (widget *Widget) String() string {\n\targs := strings.Join(widget.settings.args, \" \")\n\n\tif args != \"\" {\n\t\treturn fmt.Sprintf(\"%s %s\", widget.settings.cmd, args)\n\t}\n\n\treturn widget.settings.cmd\n}\n\nfunc (widget *Widget) Write(p []byte) (n int, err error) {\n\twidget.m.Lock()\n\tdefer widget.m.Unlock()\n\n\t// Write the new data into the buffer\n\tn, err = widget.buffer.Write(p)\n\n\t// Remove lines that exceed maxLines\n\tlines := widget.countLines()\n\tif lines > widget.settings.maxLines {\n\t\terr = widget.drainLines(lines - widget.settings.maxLines)\n\t}\n\n\treturn n, err\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// countLines counts the lines of data in the buffer\nfunc (widget *Widget) countLines() int {\n\treturn bytes.Count(widget.buffer.Bytes(), []byte{'\\n'})\n}\n\n// drainLines removed the first n lines from the buffer\nfunc (widget *Widget) drainLines(n int) error {\n\tfor i := 0; i < n; i++ {\n\t\t_, err := widget.buffer.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (widget *Widget) environment() []string {\n\tenvs := os.Environ()\n\tenvs = append(\n\t\tenvs,\n\t\tfmt.Sprintf(\"WTF_WIDGET_WIDTH=%d\", widget.settings.width),\n\t\tfmt.Sprintf(\"WTF_WIDGET_HEIGHT=%d\", widget.settings.height),\n\t)\n\treturn envs\n}\n\nfunc runCommandLoop(widget *Widget) {\n\t// Run the command forever in a loop. Refresh() will put a value into the\n\t// channel to signal the loop to continue.\n\tfor {\n\t\t<-widget.runChan\n\t\twidget.resetBuffer()\n\t\tcmd := exec.Command(widget.settings.cmd, widget.settings.args...)\n\t\tcmd.Env = widget.environment()\n\t\tcmd.Dir = widget.settings.workingDir\n\t\tvar err error\n\t\tif widget.settings.pty {\n\t\t\terr = runCommandPty(widget, cmd)\n\t\t} else {\n\t\t\terr = runCommand(widget, cmd)\n\t\t}\n\t\tif err != nil {\n\t\t\twidget.handleError(err)\n\t\t}\n\t\twidget.redrawChan <- true\n\t}\n}\n\nfunc runCommand(widget *Widget, cmd *exec.Cmd) error {\n\tcmd.Stdout = widget\n\treturn cmd.Run()\n}\n\nfunc runCommandPty(widget *Widget, cmd *exec.Cmd) error {\n\tf, err := pty.Start(cmd)\n\t// The command has exited, print any error messages\n\tif err != nil {\n\t\tif widget.settings.ptySuppressErrors {\n\t\t\treturn cmd.Wait()\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Make sure to close the pty at the end.\n\tdefer func() { _ = f.Close() }() // Best effort.\n\n\t// Handle pty size.\n\tch := make(chan os.Signal, 1)\n\tsignal.Notify(ch, syscall.SIGWINCH)\n\tgo func() {\n\t\tfor range ch {\n\t\t\tif err := pty.InheritSize(os.Stdin, f); err != nil {\n\t\t\t\tlogger.Log(fmt.Sprintf(\"error resizing pty: %s\", err))\n\t\t\t}\n\t\t}\n\t}()\n\tch <- syscall.SIGWINCH                        // Initial resize.\n\tdefer func() { signal.Stop(ch); close(ch) }() // Cleanup signals when done.\n\n\t// Extract output\n\t_, err = io.Copy(widget.buffer, f)\n\tif err != nil {\n\t\tif widget.settings.ptySuppressErrors && errors.Is(err, syscall.EIO) {\n\t\t\treturn cmd.Wait()\n\t\t}\n\t\treturn err\n\t}\n\treturn cmd.Wait()\n}\n\nfunc (widget *Widget) handleError(err error) {\n\twidget.m.Lock()\n\tdefer widget.m.Unlock()\n\t_, writeErr := widget.buffer.WriteString(err.Error())\n\tif writeErr != nil {\n\t\treturn\n\t}\n}\n\nfunc redrawLoop(widget *Widget) {\n\tfor {\n\t\twidget.Redraw(widget.content)\n\t\tif widget.settings.tail {\n\t\t\twidget.View.ScrollToEnd()\n\t\t}\n\t\t<-widget.redrawChan\n\t}\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\twidget.m.Lock()\n\tresult := widget.buffer.String()\n\twidget.m.Unlock()\n\n\tansiTitle := tview.TranslateANSI(tview.Escape(widget.CommonSettings().Title))\n\tif ansiTitle == defaultTitle {\n\t\tansiTitle = tview.TranslateANSI(tview.Escape(widget.String()))\n\t}\n\tansiResult := tview.TranslateANSI(tview.Escape(result))\n\n\treturn ansiTitle, ansiResult, false\n}\n\nfunc (widget *Widget) resetBuffer() {\n\twidget.m.Lock()\n\tdefer widget.m.Unlock()\n\n\twidget.buffer.Reset()\n}\n"
  },
  {
    "path": "modules/cryptocurrency/bittrex/bittrex.go",
    "content": "package bittrex\n\ntype summaryList struct {\n\titems []*bCurrency\n}\n\n// Base Currency\ntype bCurrency struct {\n\tname        string\n\tdisplayName string\n\tmarkets     []*mCurrency\n}\n\n// Market Currency\ntype mCurrency struct {\n\tname string\n\tsummaryInfo\n}\n\ntype summaryInfo struct {\n\tLow            string\n\tHigh           string\n\tVolume         string\n\tLast           string\n\tOpenSellOrders string\n\tOpenBuyOrders  string\n}\n\ntype summaryResponse struct {\n\tSuccess bool   `json:\"success\"`\n\tMessage string `json:\"message\"`\n\tResult  []struct {\n\t\tMarketName     string  `json:\"MarketName\"`\n\t\tHigh           float64 `json:\"High\"`\n\t\tLow            float64 `json:\"Low\"`\n\t\tLast           float64 `json:\"Last\"`\n\t\tVolume         float64 `json:\"Volume\"`\n\t\tOpenSellOrders int     `json:\"OpenSellOrders\"`\n\t\tOpenBuyOrders  int     `json:\"OpenBuyOrders\"`\n\t} `json:\"result\"`\n}\n\nfunc (list *summaryList) addSummaryItem(name, displayName string, marketList []*mCurrency) {\n\tlist.items = append(list.items, &bCurrency{\n\t\tname:        name,\n\t\tdisplayName: displayName,\n\t\tmarkets:     marketList,\n\t})\n}\n"
  },
  {
    "path": "modules/cryptocurrency/bittrex/display.go",
    "content": "package bittrex\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"text/template\"\n)\n\nfunc (widget *Widget) display() {\n\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tif !ok {\n\t\treturn widget.CommonSettings().Title, errorText, true\n\t}\n\n\tlist := &widget.summaryList\n\tstr := \"\"\n\n\tfor _, baseCurrency := range list.items {\n\t\tstr += fmt.Sprintf(\n\t\t\t\" [%s]%s[%s] (%s)\\n\\n\",\n\t\t\twidget.settings.base.displayName,\n\t\t\tbaseCurrency.displayName,\n\t\t\twidget.settings.base.name,\n\t\t\tbaseCurrency.name,\n\t\t)\n\n\t\tresultTemplate := template.New(\"bittrex\")\n\n\t\tfor _, marketCurrency := range baseCurrency.markets {\n\t\t\twriter := new(bytes.Buffer)\n\n\t\t\tstrTemplate, _ := resultTemplate.Parse(\n\t\t\t\t\"  [{{.nameColor}}]{{.mName}}\\n\" +\n\t\t\t\t\tformatableText(\"High\", \"High\") +\n\t\t\t\t\tformatableText(\"Low\", \"Low\") +\n\t\t\t\t\tformatableText(\"Last\", \"Last\") +\n\t\t\t\t\tformatableText(\"Volume\", \"Volume\") +\n\t\t\t\t\t\"\\n\" +\n\t\t\t\t\tformatableText(\"Open Buy\", \"OpenBuyOrders\") +\n\t\t\t\t\tformatableText(\"Open Sell\", \"OpenSellOrders\"),\n\t\t\t)\n\n\t\t\terr := strTemplate.Execute(writer, map[string]string{\n\t\t\t\t\"nameColor\":      widget.settings.market.name,\n\t\t\t\t\"fieldColor\":     widget.settings.market.field,\n\t\t\t\t\"valueColor\":     widget.settings.market.value,\n\t\t\t\t\"mName\":          marketCurrency.name,\n\t\t\t\t\"High\":           marketCurrency.High,\n\t\t\t\t\"Low\":            marketCurrency.Low,\n\t\t\t\t\"Last\":           marketCurrency.Last,\n\t\t\t\t\"Volume\":         marketCurrency.Volume,\n\t\t\t\t\"OpenBuyOrders\":  marketCurrency.OpenBuyOrders,\n\t\t\t\t\"OpenSellOrders\": marketCurrency.OpenSellOrders,\n\t\t\t})\n\n\t\t\tif err != nil {\n\t\t\t\tstr = err.Error()\n\t\t\t} else {\n\t\t\t\tstr += writer.String() + \"\\n\"\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn widget.CommonSettings().Title, str, true\n\n}\n\nfunc formatableText(key, value string) string {\n\treturn fmt.Sprintf(\"[{{.fieldColor}}]%12s: [{{.valueColor}}]{{.%s}}\\n\", key, value)\n}\n"
  },
  {
    "path": "modules/cryptocurrency/bittrex/settings.go",
    "content": "package bittrex\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Bittrex\"\n)\n\ntype colors struct {\n\tbase struct {\n\t\tname        string\n\t\tdisplayName string\n\t}\n\tmarket struct {\n\t\tname  string\n\t\tfield string\n\t\tvalue string\n\t}\n}\n\ntype currency struct {\n\tdisplayName string\n\tmarket      []interface{}\n}\n\ntype summary struct {\n\tcurrencies map[string]*currency\n}\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcolors\n\tsummary\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\tsettings.base.name = ymlConfig.UString(\"colors.base.name\")\n\tsettings.base.displayName = ymlConfig.UString(\"colors.base.displayName\")\n\n\tsettings.market.name = ymlConfig.UString(\"colors.market.name\")\n\tsettings.market.field = ymlConfig.UString(\"colors.market.field\")\n\tsettings.market.value = ymlConfig.UString(\"colors.market.value\")\n\n\tsettings.currencies = make(map[string]*currency)\n\tfor key, val := range ymlConfig.UMap(\"summary\") {\n\t\tcoercedVal := val.(map[string]interface{})\n\n\t\tcurrency := &currency{\n\t\t\tdisplayName: coercedVal[\"displayName\"].(string),\n\t\t\tmarket:      coercedVal[\"market\"].([]interface{}),\n\t\t}\n\n\t\tsettings.currencies[key] = currency\n\t}\n\n\tsettings.SetDocumentationPath(\"cryptocurrencies/bittrex\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/cryptocurrency/bittrex/widget.go",
    "content": "package bittrex\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"net/http\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst (\n\tbaseURL = \"https://bittrex.com/api/v1.1/public/getmarketsummary\"\n)\n\nvar (\n\terrorText = \"\"\n\tok        = true\n)\n\n// Widget define wtf widget to register widget later\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n\tsummaryList\n}\n\n// NewWidget Make new instance of widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings:    settings,\n\t\tsummaryList: summaryList{},\n\t}\n\n\tok = true\n\terrorText = \"\"\n\n\twidget.setSummaryList()\n\n\treturn &widget\n}\n\nfunc (widget *Widget) setSummaryList() {\n\tfor symbol, currency := range widget.settings.currencies {\n\t\tmCurrencyList := widget.makeSummaryMarketList(currency.market)\n\t\twidget.addSummaryItem(symbol, currency.displayName, mCurrencyList)\n\t}\n}\n\nfunc (widget *Widget) makeSummaryMarketList(market []interface{}) []*mCurrency {\n\tmCurrencyList := []*mCurrency{}\n\n\tfor _, marketSymbol := range market {\n\t\tmCurrencyList = append(mCurrencyList, makeMarketCurrency(marketSymbol.(string)))\n\t}\n\n\treturn mCurrencyList\n}\n\nfunc makeMarketCurrency(name string) *mCurrency {\n\treturn &mCurrency{\n\t\tname: name,\n\t\tsummaryInfo: summaryInfo{\n\t\t\tHigh:           \"\",\n\t\t\tLow:            \"\",\n\t\t\tVolume:         \"\",\n\t\t\tLast:           \"\",\n\t\t\tOpenBuyOrders:  \"\",\n\t\t\tOpenSellOrders: \"\",\n\t\t},\n\t}\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh & update after interval time\nfunc (widget *Widget) Refresh() {\n\twidget.updateSummary()\n\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) updateSummary() {\n\t// In case if anything bad happened!\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tfmt.Println(\"recovered in updateSummary()\", r)\n\t\t}\n\t}()\n\n\tclient := &http.Client{\n\t\tTimeout: 5 * time.Second,\n\t}\n\n\tfor _, baseCurrency := range widget.items {\n\t\tfor _, mCurrency := range baseCurrency.markets {\n\t\t\trequest := makeRequest(baseCurrency.name, mCurrency.name)\n\t\t\tresponse, err := client.Do(request)\n\n\t\t\tok = true\n\t\t\terrorText = \"\"\n\n\t\t\tif err != nil {\n\t\t\t\tok = false\n\t\t\t\terrorText = \"Please Check Your Internet Connection!\"\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif response.StatusCode != http.StatusOK {\n\t\t\t\terrorText = response.Status\n\t\t\t\tok = false\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tdefer func() { _ = response.Body.Close() }()\n\t\t\tjsonResponse := summaryResponse{}\n\t\t\tdecoder := json.NewDecoder(response.Body)\n\t\t\terr = decoder.Decode(&jsonResponse)\n\t\t\tif err != nil {\n\t\t\t\terrorText = \"Could not parse JSON!\"\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif !jsonResponse.Success {\n\t\t\t\tok = false\n\t\t\t\terrorText = fmt.Sprintf(\"%s-%s: %s\", baseCurrency.name, mCurrency.name, jsonResponse.Message)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tok = true\n\t\t\terrorText = \"\"\n\n\t\t\tmCurrency.Last = fmt.Sprintf(\"%f\", jsonResponse.Result[0].Last)\n\t\t\tmCurrency.High = fmt.Sprintf(\"%f\", jsonResponse.Result[0].High)\n\t\t\tmCurrency.Low = fmt.Sprintf(\"%f\", jsonResponse.Result[0].Low)\n\t\t\tmCurrency.Volume = fmt.Sprintf(\"%f\", jsonResponse.Result[0].Volume)\n\t\t\tmCurrency.OpenBuyOrders = fmt.Sprintf(\"%d\", jsonResponse.Result[0].OpenBuyOrders)\n\t\t\tmCurrency.OpenSellOrders = fmt.Sprintf(\"%d\", jsonResponse.Result[0].OpenSellOrders)\n\t\t}\n\t}\n\n\twidget.display()\n}\n\nfunc makeRequest(baseName, marketName string) *http.Request {\n\turl := fmt.Sprintf(\"%s?market=%s-%s\", baseURL, baseName, marketName)\n\trequest, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\n\treturn request\n}\n"
  },
  {
    "path": "modules/cryptocurrency/blockfolio/settings.go",
    "content": "package blockfolio\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Blockfolio\"\n)\n\ntype colors struct {\n\tname  string\n\tgrows string\n\tdrop  string\n}\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcolors\n\n\tdeviceToken     string\n\tdisplayHoldings bool\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tdeviceToken:     ymlConfig.UString(\"device_token\"),\n\t\tdisplayHoldings: ymlConfig.UBool(\"displayHoldings\", true),\n\t}\n\n\tsettings.SetDocumentationPath(\"cryptocurrencies/blockfolio\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/cryptocurrency/blockfolio/widget.go",
    "content": "package blockfolio\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tdevice_token string\n\tsettings     *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tdevice_token: settings.deviceToken,\n\t\tsettings:     settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\nfunc (widget *Widget) content() (string, string, bool) {\n\tpositions, err := Fetch(widget.device_token)\n\ttitle := widget.CommonSettings().Title\n\tif err != nil {\n\t\treturn title, err.Error(), true\n\t}\n\n\tres := \"\"\n\ttotalFiat := float32(0.0)\n\n\tfor i := 0; i < len(positions.PositionList); i++ {\n\t\tcolorForChange := widget.settings.grows\n\n\t\tif positions.PositionList[i].TwentyFourHourPercentChangeFiat <= 0 {\n\t\t\tcolorForChange = widget.settings.drop\n\t\t}\n\n\t\ttotalFiat += positions.PositionList[i].HoldingValueFiat\n\n\t\tif widget.settings.displayHoldings {\n\t\t\tres += fmt.Sprintf(\n\t\t\t\t\"[%s]%-6s - %5.2f ([%s]%.3fk [%s]%.2f%s)\\n\",\n\t\t\t\twidget.settings.name,\n\t\t\t\tpositions.PositionList[i].Coin,\n\t\t\t\tpositions.PositionList[i].Quantity,\n\t\t\t\tcolorForChange,\n\t\t\t\tpositions.PositionList[i].HoldingValueFiat/1000,\n\t\t\t\tcolorForChange,\n\t\t\t\tpositions.PositionList[i].TwentyFourHourPercentChangeFiat,\n\t\t\t\t\"%\",\n\t\t\t)\n\t\t} else {\n\t\t\tres += fmt.Sprintf(\n\t\t\t\t\"[%s]%-6s - %5.2f ([%s]%.2f%s)\\n\",\n\t\t\t\twidget.settings.name,\n\t\t\t\tpositions.PositionList[i].Coin,\n\t\t\t\tpositions.PositionList[i].Quantity,\n\t\t\t\tcolorForChange,\n\t\t\t\tpositions.PositionList[i].TwentyFourHourPercentChangeFiat,\n\t\t\t\t\"%\",\n\t\t\t)\n\t\t}\n\t}\n\n\tif widget.settings.displayHoldings {\n\t\tres += fmt.Sprintf(\"\\n[%s]Total value: $%.3fk\", \"green\", totalFiat/1000)\n\t}\n\n\treturn title, res, true\n}\n\n// always the same\nconst magic = \"edtopjhgn2345piuty89whqejfiobh89-2q453\"\n\ntype Position struct {\n\tCoin                            string  `json:\"coin\"`\n\tLastPriceFiat                   float32 `json:\"lastPriceFiat\"`\n\tTwentyFourHourPercentChangeFiat float32 `json:\"twentyFourHourPercentChangeFiat\"`\n\tQuantity                        float32 `json:\"quantity\"`\n\tHoldingValueFiat                float32 `json:\"holdingValueFiat\"`\n}\n\ntype AllPositionsResponse struct {\n\tPositionList []Position `json:\"positionList\"`\n}\n\nfunc MakeApiRequest(token string, method string) ([]byte, error) {\n\tclient := &http.Client{}\n\turl := \"https://api-v0.blockfolio.com/rest/\" + method + \"/\" + token + \"?use_alias=true&fiat_currency=USD\"\n\treq, err := http.NewRequest(\"GET\", url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Add(\"magic\", magic)\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn body, err\n}\n\nfunc GetAllPositions(token string) (*AllPositionsResponse, error) {\n\tjsn, _ := MakeApiRequest(token, \"get_all_positions\")\n\tvar parsed AllPositionsResponse\n\n\terr := json.Unmarshal(jsn, &parsed)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to parse json %v\", err)\n\t\treturn nil, err\n\t}\n\treturn &parsed, err\n}\n\nfunc Fetch(token string) (*AllPositionsResponse, error) {\n\treturn GetAllPositions(token)\n}\n"
  },
  {
    "path": "modules/cryptocurrency/cryptolive/price/price.go",
    "content": "package price\n\ntype list struct {\n\titems []*fromCurrency\n}\n\ntype fromCurrency struct {\n\tname        string\n\tdisplayName string\n\tto          []*toCurrency\n}\n\ntype toCurrency struct {\n\tname  string\n\tprice float32\n}\n\ntype cResponse map[string]float32\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (l *list) addItem(name string, displayName string, to []*toCurrency) {\n\tl.items = append(l.items, &fromCurrency{\n\t\tname:        name,\n\t\tdisplayName: displayName,\n\t\tto:          to,\n\t})\n}\n"
  },
  {
    "path": "modules/cryptocurrency/cryptolive/price/settings.go",
    "content": "package price\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"CryptoLive\"\n)\n\ntype colors struct {\n\tfrom struct {\n\t\tname        string\n\t\tdisplayName string\n\t}\n\tto struct {\n\t\tname  string\n\t\tprice string\n\t}\n\ttop struct {\n\t\tfrom struct {\n\t\t\tname        string\n\t\t\tdisplayName string\n\t\t}\n\t\tto struct {\n\t\t\tname  string\n\t\t\tfield string\n\t\t\tvalue string\n\t\t}\n\t}\n}\n\ntype currency struct {\n\tdisplayName string\n\tto          []interface{}\n}\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcolors\n\tcurrencies map[string]*currency\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\tsettings.from.name = ymlConfig.UString(\"colors.from.name\")\n\tsettings.from.displayName = ymlConfig.UString(\"colors.from.displayName\")\n\n\tsettings.to.name = ymlConfig.UString(\"colors.to.name\")\n\tsettings.to.price = ymlConfig.UString(\"colors.to.price\")\n\n\tsettings.top.from.name = ymlConfig.UString(\"colors.top.from.name\")\n\tsettings.top.from.displayName = ymlConfig.UString(\"colors.top.from.displayName\")\n\n\tsettings.top.to.name = ymlConfig.UString(\"colors.top.to.name\")\n\tsettings.top.to.field = ymlConfig.UString(\"colors.top.to.field\")\n\tsettings.top.to.value = ymlConfig.UString(\"colors.top.to.value\")\n\n\tsettings.currencies = make(map[string]*currency)\n\n\tfor key, val := range ymlConfig.UMap(\"currencies\") {\n\t\tcoercedVal := val.(map[string]interface{})\n\n\t\tcurrency := &currency{\n\t\t\tdisplayName: coercedVal[\"displayName\"].(string),\n\t\t\tto:          coercedVal[\"to\"].([]interface{}),\n\t\t}\n\n\t\tsettings.currencies[key] = currency\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/cryptocurrency/cryptolive/price/widget.go",
    "content": "package price\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar baseURL = \"https://min-api.cryptocompare.com/data/price\"\nvar ok = true\n\n// Widget define wtf widget to register widget later\ntype Widget struct {\n\t*list\n\tsettings *Settings\n\n\tResult string\n\n\tRefreshInterval time.Duration\n}\n\n// NewWidget Make new instance of widget\nfunc NewWidget(settings *Settings) *Widget {\n\twidget := Widget{\n\t\tsettings: settings,\n\t}\n\n\twidget.setList()\n\n\treturn &widget\n}\n\nfunc (widget *Widget) setList() {\n\twidget.list = &list{}\n\n\tfor symbol, currency := range widget.settings.currencies {\n\t\ttoList := widget.getToList(symbol)\n\t\twidget.addItem(symbol, currency.displayName, toList)\n\t}\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh & update after interval time\nfunc (widget *Widget) Refresh(wg *sync.WaitGroup) {\n\tif len(widget.items) != 0 {\n\t\twidget.updateCurrencies()\n\t\tif !ok {\n\t\t\twidget.Result = \"Please check your internet connection\"\n\t\t} else {\n\t\t\twidget.display()\n\t\t}\n\t}\n\twg.Done()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) display() {\n\tstr := \"\"\n\n\tfor _, item := range widget.items {\n\t\tstr += fmt.Sprintf(\n\t\t\t\" [%s]%s[%s] (%s)\\n\",\n\t\t\twidget.settings.from.name,\n\t\t\titem.displayName,\n\t\t\twidget.settings.from.name,\n\t\t\titem.name,\n\t\t)\n\t\tfor _, toItem := range item.to {\n\t\t\tstr += fmt.Sprintf(\n\t\t\t\t\"\\t[%s]%s: [%s]%f\\n\",\n\t\t\t\twidget.settings.to.name,\n\t\t\t\ttoItem.name,\n\t\t\t\twidget.settings.to.price,\n\t\t\t\ttoItem.price,\n\t\t\t)\n\t\t}\n\t\tstr += \"\\n\"\n\t}\n\n\twidget.Result = fmt.Sprintf(\"\\n%s\", str)\n}\n\nfunc (widget *Widget) getToList(symbol string) []*toCurrency {\n\tvar toList []*toCurrency\n\n\tfor _, to := range widget.settings.currencies[symbol].to {\n\t\ttoList = append(toList, &toCurrency{\n\t\t\tname:  to.(string),\n\t\t\tprice: 0,\n\t\t})\n\t}\n\n\treturn toList\n}\n\nfunc (widget *Widget) updateCurrencies() {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tfmt.Println(\"recovered in updateSummary()\", r)\n\t\t}\n\t}()\n\tfor _, fromCurrency := range widget.items {\n\n\t\tvar (\n\t\t\tclient       http.Client\n\t\t\tjsonResponse cResponse\n\t\t)\n\n\t\tclient = http.Client{\n\t\t\tTimeout: 5 * time.Second,\n\t\t}\n\n\t\trequest := makeRequest(fromCurrency)\n\t\tresponse, err := client.Do(request)\n\n\t\tif err != nil {\n\t\t\tok = false\n\t\t} else {\n\t\t\tok = true\n\t\t}\n\n\t\tdefer func() { _ = response.Body.Close() }()\n\n\t\t_ = json.NewDecoder(response.Body).Decode(&jsonResponse)\n\n\t\tsetPrices(&jsonResponse, fromCurrency)\n\t}\n}\n\nfunc makeRequest(currency *fromCurrency) *http.Request {\n\ttsyms := \"\"\n\tfor _, to := range currency.to {\n\t\ttsyms += fmt.Sprintf(\"%s,\", to.name)\n\t}\n\n\turl := fmt.Sprintf(\"%s?fsym=%s&tsyms=%s\", baseURL, currency.name, tsyms)\n\trequest, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\n\treturn request\n}\n\nfunc setPrices(response *cResponse, currencry *fromCurrency) {\n\tfor idx, toCurrency := range currencry.to {\n\t\tcurrencry.to[idx].price = (*response)[toCurrency.name]\n\t}\n}\n"
  },
  {
    "path": "modules/cryptocurrency/cryptolive/settings.go",
    "content": "package cryptolive\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/modules/cryptocurrency/cryptolive/price\"\n\t\"github.com/wtfutil/wtf/modules/cryptocurrency/cryptolive/toplist\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"CryptolLive\"\n)\n\ntype colors struct {\n\tfrom struct {\n\t\tname        string\n\t\tdisplayName string\n\t}\n\tto struct {\n\t\tname  string\n\t\tprice string\n\t}\n\ttop struct {\n\t\tfrom struct {\n\t\t\tname        string\n\t\t\tdisplayName string\n\t\t}\n\t\tto struct {\n\t\t\tname  string\n\t\t\tfield string\n\t\t\tvalue string\n\t\t}\n\t}\n}\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcolors\n\n\tcurrencies map[string]interface{}\n\ttop        map[string]interface{}\n\n\tpriceSettings   *price.Settings\n\ttoplistSettings *toplist.Settings\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tcurrencies, _ := ymlConfig.Map(\"currencies\")\n\ttop, _ := ymlConfig.Map(\"top\")\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tcurrencies: currencies,\n\t\ttop:        top,\n\n\t\tpriceSettings:   price.NewSettingsFromYAML(name, ymlConfig, globalConfig),\n\t\ttoplistSettings: toplist.NewSettingsFromYAML(name, ymlConfig, globalConfig),\n\t}\n\n\tsettings.from.name = ymlConfig.UString(\"colors.from.name\")\n\tsettings.from.displayName = ymlConfig.UString(\"colors.from.displayName\")\n\n\tsettings.to.name = ymlConfig.UString(\"colors.to.name\")\n\tsettings.to.price = ymlConfig.UString(\"colors.to.price\")\n\n\tsettings.colors.top.from.name = ymlConfig.UString(\"colors.top.from.name\")\n\tsettings.colors.top.from.displayName = ymlConfig.UString(\"colors.top.from.displayName\")\n\n\tsettings.colors.top.to.name = ymlConfig.UString(\"colors.top.to.name\")\n\tsettings.colors.top.to.field = ymlConfig.UString(\"colors.top.to.field\")\n\tsettings.colors.top.to.value = ymlConfig.UString(\"colors.top.to.value\")\n\n\tsettings.SetDocumentationPath(\"cryptocurrencies/cryptolive\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/cryptocurrency/cryptolive/toplist/display.go",
    "content": "package toplist\n\nimport \"fmt\"\n\nfunc (widget *Widget) display() {\n\tstr := \"\"\n\tfor _, fromCurrency := range widget.list.items {\n\t\tstr += fmt.Sprintf(\n\t\t\t\"[%s]%s [%s](%s)\\n\",\n\t\t\twidget.settings.from.displayName,\n\t\t\tfromCurrency.displayName,\n\t\t\twidget.settings.from.name,\n\t\t\tfromCurrency.name,\n\t\t)\n\t\tstr += widget.makeToListText(fromCurrency.to)\n\t}\n\n\twidget.Result = str\n}\n\nfunc (widget *Widget) makeToListText(toList []*tCurrency) string {\n\tstr := \"\"\n\tfor _, toCurrency := range toList {\n\t\tstr += widget.makeToText(toCurrency)\n\t}\n\n\treturn str\n}\n\nfunc (widget *Widget) makeToText(toCurrency *tCurrency) string {\n\tstr := \"\"\n\tstr += fmt.Sprintf(\n\t\t\"  [%s]%s\\n\",\n\t\twidget.settings.to.name,\n\t\ttoCurrency.name,\n\t)\n\n\tfor _, info := range toCurrency.info {\n\t\tstr += widget.makeInfoText(info)\n\t\tstr += \"\\n\\n\"\n\t}\n\treturn str\n}\n\nfunc (widget *Widget) makeInfoText(info tInfo) string {\n\treturn fmt.Sprintf(\n\t\t\"    [%s]Exchange: [%s]%s\\n\",\n\t\twidget.settings.colors.top.to.field,\n\t\twidget.settings.colors.top.to.value,\n\t\tinfo.exchange,\n\t) +\n\t\tfmt.Sprintf(\n\t\t\t\"    [%s]Volume(24h): [%s]%f-[%s]%f\",\n\t\t\twidget.settings.colors.top.to.field,\n\t\t\twidget.settings.colors.top.to.value,\n\t\t\tinfo.volume24h,\n\t\t\twidget.settings.colors.top.to.value,\n\t\t\tinfo.volume24hTo,\n\t\t)\n}\n"
  },
  {
    "path": "modules/cryptocurrency/cryptolive/toplist/settings.go",
    "content": "package toplist\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"CryptoLive\"\n)\n\ntype colors struct {\n\tfrom struct {\n\t\tname        string\n\t\tdisplayName string\n\t}\n\tto struct {\n\t\tname  string\n\t\tprice string\n\t}\n\ttop struct {\n\t\tfrom struct {\n\t\t\tname        string\n\t\t\tdisplayName string\n\t\t}\n\t\tto struct {\n\t\t\tname  string\n\t\t\tfield string\n\t\t\tvalue string\n\t\t}\n\t}\n}\n\ntype currency struct {\n\tdisplayName string\n\tlimit       int\n\tto          []interface{}\n}\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcolors\n\ttop map[string]*currency\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\tsettings.from.name = ymlConfig.UString(\"colors.from.name\")\n\tsettings.from.displayName = ymlConfig.UString(\"colors.from.displayName\")\n\n\tsettings.to.name = ymlConfig.UString(\"colors.to.name\")\n\tsettings.to.price = ymlConfig.UString(\"colors.to.price\")\n\n\tsettings.colors.top.from.name = ymlConfig.UString(\"colors.top.from.name\")\n\tsettings.colors.top.from.displayName = ymlConfig.UString(\"colors.top.from.displayName\")\n\n\tsettings.colors.top.to.name = ymlConfig.UString(\"colors.top.to.name\")\n\tsettings.colors.top.to.field = ymlConfig.UString(\"colors.top.to.field\")\n\tsettings.colors.top.to.value = ymlConfig.UString(\"colors.top.to.value\")\n\n\tsettings.top = make(map[string]*currency)\n\n\tfor key, val := range ymlConfig.UMap(\"top\") {\n\t\tcoercedVal := val.(map[string]interface{})\n\n\t\tlimit, _ := coercedVal[\"limit\"].(int)\n\n\t\tcurrency := &currency{\n\t\t\tdisplayName: coercedVal[\"displayName\"].(string),\n\t\t\tlimit:       limit,\n\t\t\tto:          coercedVal[\"to\"].([]interface{}),\n\t\t}\n\n\t\tsettings.top[key] = currency\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/cryptocurrency/cryptolive/toplist/toplist.go",
    "content": "package toplist\n\ntype cList struct {\n\titems []*fCurrency\n}\n\ntype fCurrency struct {\n\tname, displayName string\n\tlimit             int\n\tto                []*tCurrency\n}\n\ntype tCurrency struct {\n\tname string\n\tinfo []tInfo\n}\n\ntype tInfo struct {\n\texchange               string\n\tvolume24h, volume24hTo float32\n}\n\ntype responseInterface struct {\n\tResponse string `json:\"Response\"`\n\tData     []struct {\n\t\tExchange    string  `json:\"exchange\"`\n\t\tFromSymbol  string  `json:\"fromSymbol\"`\n\t\tToSymbol    string  `json:\"toSymbol\"`\n\t\tVolume24h   float32 `json:\"volume24h\"`\n\t\tVolume24hTo float32 `json:\"volume24hTo\"`\n\t} `json:\"Data\"`\n}\n\nfunc (list *cList) addItem(name, displayName string, limit int, to []*tCurrency) {\n\tlist.items = append(list.items, &fCurrency{\n\t\tname:        name,\n\t\tdisplayName: displayName,\n\t\tlimit:       limit,\n\t\tto:          to,\n\t})\n}\n"
  },
  {
    "path": "modules/cryptocurrency/cryptolive/toplist/widget.go",
    "content": "package toplist\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar baseURL = \"https://min-api.cryptocompare.com/data/top/exchanges\"\n\n// Widget Toplist Widget\ntype Widget struct {\n\tResult string\n\n\tRefreshInterval time.Duration\n\n\tlist     *cList\n\tsettings *Settings\n}\n\n// NewWidget Make new toplist widget\nfunc NewWidget(settings *Settings) *Widget {\n\twidget := Widget{\n\t\tsettings: settings,\n\t}\n\n\twidget.list = &cList{}\n\twidget.setList()\n\n\treturn &widget\n}\n\nfunc (widget *Widget) setList() {\n\tfor symbol, currency := range widget.settings.top {\n\t\ttoList := widget.makeToList(symbol, currency.limit)\n\t\twidget.list.addItem(symbol, currency.displayName, currency.limit, toList)\n\t}\n}\n\nfunc (widget *Widget) makeToList(symbol string, limit int) (list []*tCurrency) {\n\tfor _, to := range widget.settings.top[symbol].to {\n\t\tlist = append(list, &tCurrency{\n\t\t\tname: to.(string),\n\t\t\tinfo: make([]tInfo, limit),\n\t\t})\n\t}\n\n\treturn\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh & update after interval time\nfunc (widget *Widget) Refresh(wg *sync.WaitGroup) {\n\tif len(widget.list.items) != 0 {\n\n\t\twidget.updateData()\n\n\t\twidget.display()\n\t}\n\twg.Done()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) updateData() {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tfmt.Println(\"recovered in updateSummary()\", r)\n\t\t}\n\t}()\n\n\tclient := &http.Client{\n\t\tTimeout: 5 * time.Second,\n\t}\n\n\tfor _, fromCurrency := range widget.list.items {\n\t\tfor _, toCurrency := range fromCurrency.to {\n\n\t\t\trequest := makeRequest(fromCurrency.name, toCurrency.name, fromCurrency.limit)\n\t\t\tresponse, _ := client.Do(request)\n\n\t\t\tvar jsonResponse responseInterface\n\n\t\t\terr := json.NewDecoder(response.Body).Decode(&jsonResponse)\n\n\t\t\tif err != nil {\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\n\t\t\tfor idx, info := range jsonResponse.Data {\n\t\t\t\ttoCurrency.info[idx] = tInfo{\n\t\t\t\t\texchange:    info.Exchange,\n\t\t\t\t\tvolume24h:   info.Volume24h,\n\t\t\t\t\tvolume24hTo: info.Volume24hTo,\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t}\n}\n\nfunc makeRequest(fsym, tsym string, limit int) *http.Request {\n\turl := fmt.Sprintf(\"%s?fsym=%s&tsym=%s&limit=%d\", baseURL, fsym, tsym, limit)\n\trequest, _ := http.NewRequest(\"GET\", url, http.NoBody)\n\treturn request\n}\n"
  },
  {
    "path": "modules/cryptocurrency/cryptolive/widget.go",
    "content": "package cryptolive\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/modules/cryptocurrency/cryptolive/price\"\n\t\"github.com/wtfutil/wtf/modules/cryptocurrency/cryptolive/toplist\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget define wtf widget to register widget later\ntype Widget struct {\n\tview.TextWidget\n\n\tpriceWidget   *price.Widget\n\ttoplistWidget *toplist.Widget\n\tsettings      *Settings\n}\n\n// NewWidget Make new instance of widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tpriceWidget:   price.NewWidget(settings.priceSettings),\n\t\ttoplistWidget: toplist.NewWidget(settings.toplistSettings),\n\t\tsettings:      settings,\n\t}\n\n\twidget.priceWidget.RefreshInterval = widget.RefreshInterval()\n\twidget.toplistWidget.RefreshInterval = widget.RefreshInterval()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh & update after interval time\nfunc (widget *Widget) Refresh() {\n\tvar wg sync.WaitGroup\n\n\twg.Add(2)\n\twidget.priceWidget.Refresh(&wg)\n\twidget.toplistWidget.Refresh(&wg)\n\twg.Wait()\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tstr := \"\"\n\tstr += widget.priceWidget.Result\n\tstr += widget.toplistWidget.Result\n\n\treturn widget.CommonSettings().Title, fmt.Sprintf(\"\\n%s\", str), false\n}\n"
  },
  {
    "path": "modules/cryptocurrency/mempool/settings.go",
    "content": "package mempool\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"mempool\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\tcommon *cfg.Common\n\n\t// Define your settings attributes here\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tcommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\t// Configure your settings attributes here. See http://github.com/olebedev/config for type details\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/cryptocurrency/mempool/widget.go",
    "content": "package mempool\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/logger\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget is the container for your module's data\ntype Widget struct {\n\tview.TextWidget\n\tsettings *Settings\n}\n\n// NewWidget creates and returns an instance of Widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.common),\n\n\t\tsettings: settings,\n\t}\n\treturn &widget\n}\n\ntype feeStruct struct {\n\tFastFee     int `json:\"fastestFee\"`\n\tHalfHourFee int `json:\"halfHourFee\"`\n\tHourFee     int `json:\"hourFee\"`\n\tEcoFee      int `json:\"economyFee\"`\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\t// The last call should always be to the display function\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() string {\n\treturn getBTCTxFees()\n}\n\nfunc getBTCTxFees() string {\n\turl := \"https://mempool.space/api/v1/fees/recommended\"\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\tlogger.Log(fmt.Sprintf(\"[mempool] Error: Failed to make request to mempool. Reason: %s\", err))\n\t\treturn \"[mempool] error callng mempool API\"\n\t}\n\tdefer resp.Body.Close()\n\n\tparsed := feeStruct{}\n\terr = utils.ParseJSON(&parsed, resp.Body)\n\tif err != nil {\n\t\tlogger.Log(fmt.Sprintf(\"[mempool] Error: Failed to decode JSON data from mempool. Reason: %s\", err))\n\t\treturn \"[mempool] error parsing JSON from mempool API\"\n\t}\n\n\tfinalStr := \"\"\n\tfinalStr += fmt.Sprintf(\"%-7s %2d sat/vB\\n\", \"Fast\", parsed.FastFee)\n\tfinalStr += fmt.Sprintf(\"%-7s %2d sat/vB\\n\", \"30 min\", parsed.HalfHourFee)\n\tfinalStr += fmt.Sprintf(\"%-7s %2d sat/vB\\n\", \"60 min\", parsed.HourFee)\n\tfinalStr += fmt.Sprintf(\"%-7s %2d sat/vB\\n\", \"Eco\", parsed.EcoFee)\n\n\treturn finalStr\n}\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(func() (string, string, bool) {\n\t\treturn widget.CommonSettings().Title, widget.content(), false\n\t})\n}\n"
  },
  {
    "path": "modules/datadog/client.go",
    "content": "package datadog\n\nimport (\n\t\"github.com/wtfutil/wtf/utils\"\n\tdatadog \"github.com/zorkian/go-datadog-api\"\n)\n\n// Monitors returns a list of Datadog monitors\nfunc (widget *Widget) Monitors() ([]datadog.Monitor, error) {\n\tclient := datadog.NewClient(\n\t\twidget.settings.apiKey,\n\t\twidget.settings.applicationKey,\n\t)\n\n\ttags := utils.ToStrs(widget.settings.tags)\n\n\tmonitors, err := client.GetMonitorsByTags(tags)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn monitors, nil\n}\n"
  },
  {
    "path": "modules/datadog/keyboard.go",
    "content": "package datadog\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openItem, \"Open item in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openItem, \"Open item in browser\")\n}\n"
  },
  {
    "path": "modules/datadog/settings.go",
    "content": "package datadog\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"DataDog\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey         string        `help:\"Your Datadog API key.\"`\n\tapplicationKey string        `help:\"Your Datadog Application key.\"`\n\ttags           []interface{} `help:\"Array of tags you want to query monitors by.\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:         ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_DATADOG_API_KEY\"))),\n\t\tapplicationKey: ymlConfig.UString(\"applicationKey\", os.Getenv(\"WTF_DATADOG_APPLICATION_KEY\")),\n\t\ttags:           ymlConfig.UList(\"monitors.tags\"),\n\t}\n\n\tcfg.ModuleSecret(name+\"-api\", globalConfig, &settings.apiKey).Load()\n\tcfg.ModuleSecret(name+\"-app\", globalConfig, &settings.applicationKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/datadog/widget.go",
    "content": "package datadog\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\tdatadog \"github.com/zorkian/go-datadog-api\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tmonitors []datadog.Monitor\n\tsettings *Settings\n\terr      error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\twidget.err = nil\n\tmonitors, monitorErr := widget.Monitors()\n\n\tif monitorErr != nil {\n\t\twidget.monitors = nil\n\t\twidget.err = monitorErr\n\t\twidget.SetItemCount(0)\n\t\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, monitorErr.Error(), true })\n\t\treturn\n\t}\n\ttriggeredMonitors := []datadog.Monitor{}\n\n\tfor _, monitor := range monitors {\n\t\tstate := *monitor.OverallState\n\t\tif state == \"Alert\" {\n\t\t\ttriggeredMonitors = append(triggeredMonitors, monitor)\n\t\t}\n\t}\n\twidget.monitors = triggeredMonitors\n\twidget.SetItemCount(len(widget.monitors))\n\n\twidget.Render()\n}\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttriggeredMonitors := widget.monitors\n\tvar str string\n\n\ttitle := widget.CommonSettings().Title\n\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif len(triggeredMonitors) > 0 {\n\t\tstr += fmt.Sprintf(\n\t\t\t\" %s\\n\",\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"[%s]Triggered Monitors[white]\",\n\t\t\t\twidget.settings.Colors.Subheading,\n\t\t\t),\n\t\t)\n\t\tfor idx, triggeredMonitor := range triggeredMonitors {\n\t\t\trow := fmt.Sprintf(`[%s][red] %s[%s]`,\n\t\t\t\twidget.RowColor(idx),\n\t\t\t\t*triggeredMonitor.Name,\n\t\t\t\twidget.RowColor(idx),\n\t\t\t)\n\t\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(*triggeredMonitor.Name))\n\t\t}\n\t} else {\n\t\tstr += fmt.Sprintf(\n\t\t\t\" %s\\n\",\n\t\t\t\"[green]No Triggered Monitors[white]\",\n\t\t)\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) openItem() {\n\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.monitors != nil && sel < len(widget.monitors) {\n\t\titem := &widget.monitors[sel]\n\t\tutils.OpenFile(fmt.Sprintf(\"https://app.datadoghq.com/monitors/%d?q=*\", *item.Id))\n\t}\n}\n"
  },
  {
    "path": "modules/devto/keyboard.go",
    "content": "package devto\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"d\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"a\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openStory, \"Open story in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openStory, \"Open story in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/devto/settings.go",
    "content": "package devto\n\nimport (\n\t\"github.com/olebedev/config\"\n\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"dev.to | News Feed\"\n)\n\n// Settings defines the configuration options for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tnumberOfArticles int    `help:\"Number of stories to show. Default is 10\" optional:\"true\"`\n\tcontentTag       string `help:\"List articles from a specific tag. Default is empty\" optional:\"true\"`\n\tcontentUsername  string `help:\"List articles from a specific user. Default is empty\" optional:\"true\"`\n\tcontentState     string `help:\"Order the feed by fresh/rising. Default is rising\" optional:\"true\"`\n}\n\n// NewSettingsFromYAML creates and returns an instance of Settings with configuration options populated\nfunc NewSettingsFromYAML(name string, yamlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, yamlConfig, globalConfig),\n\n\t\tnumberOfArticles: yamlConfig.UInt(\"numberOfArticles\", 10),\n\t\tcontentTag:       yamlConfig.UString(\"contentTag\", \"\"),\n\t\tcontentUsername:  yamlConfig.UString(\"contentUsername\", \"\"),\n\t\tcontentState:     yamlConfig.UString(\"contentState\", \"\"),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/devto/widget.go",
    "content": "package devto\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/VictorAvelar/devto-api-go/devto\"\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tarticles []devto.ListedArticle\n\tsettings *Settings\n\terr      error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.View.SetScrollable(true)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\tctx := context.Background()\n\twCfg, _ := devto.NewConfig(false, \"\")\n\n\tc, _ := devto.NewClient(ctx, wCfg, nil, devto.BaseURL)\n\n\toptions := devto.ArticleListOptions{\n\t\tTags:     widget.settings.contentTag,\n\t\tUsername: widget.settings.contentUsername,\n\t\tState:    widget.settings.contentState,\n\t}\n\n\tarticles, err := c.Articles.List(ctx, options)\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.articles = nil\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\tvar displayArticles []devto.ListedArticle\n\t\tvar l int\n\t\tif len(articles) < widget.settings.numberOfArticles {\n\t\t\tl = len(articles)\n\t\t} else {\n\t\t\tl = widget.settings.numberOfArticles - 1\n\t\t}\n\t\tfor i, art := range articles {\n\t\t\tif i > l {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdisplayArticles = append(displayArticles, art)\n\t\t}\n\t\twidget.articles = displayArticles\n\t\twidget.SetItemCount(len(displayArticles))\n\t}\n\n\twidget.Render()\n}\n\n// Render sets up the widget data for redrawing to the screen\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := fmt.Sprintf(\"%s - %s stories\", widget.CommonSettings().Title, widget.settings.contentTag)\n\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tarticles := widget.articles\n\tif len(articles) == 0 {\n\t\treturn title, \"No stories to display\", false\n\t}\n\n\tvar str string\n\tfor idx, article := range articles {\n\t\trow := fmt.Sprintf(\n\t\t\t`[%s]%2d. %s [lightblue](%s)[white]`,\n\t\t\twidget.RowColor(idx),\n\t\t\tidx+1,\n\t\t\tarticle.Title,\n\t\t\tarticle.User.Username,\n\t\t)\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(article.Title))\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) openStory() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.articles != nil && sel < len(widget.articles) {\n\t\tarticle := &widget.articles[sel]\n\t\tutils.OpenFile(article.URL.String())\n\t}\n}\n"
  },
  {
    "path": "modules/digitalclock/clocks.go",
    "content": "package digitalclock\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// AM defines the AM string format\nconst AM = \"A\"\n\n// PM defines the PM string format\nconst PM = \"P\"\nconst minRowsForBorder = 3\n\n// Converts integer to string along with makes sure the length of string is > 2\nfunc intStrConv(val int) string {\n\tvalStr := strconv.Itoa(val)\n\n\tif len(valStr) < 2 {\n\t\tvalStr = \"0\" + valStr\n\t}\n\treturn valStr\n}\n\n// Returns Hour + minute + AM/PM information based on the settings\nfunc getHourMinute(hourFormat string) string {\n\tstrHours := intStrConv(time.Now().Hour())\n\tAMPM := \" \"\n\n\tif hourFormat == \"12\" {\n\t\thour := time.Now().Hour()\n\t\tstrHours = intStrConv(hour % 12)\n\t\tif (hour % 12) == hour {\n\t\t\tAMPM = AM\n\t\t} else {\n\t\t\tAMPM = PM\n\t\t}\n\n\t}\n\n\tstrMinutes := intStrConv(time.Now().Minute())\n\tstrMinutes += AMPM\n\treturn strHours + getColon() + strMinutes\n}\n\n// Returns the : with blinking based on the seconds\nfunc getColon() string {\n\tif time.Now().Second()%2 == 0 {\n\t\treturn \":\"\n\t}\n\treturn \" \"\n}\n\nfunc getDate(dateFormat string, withDatePrefix bool) string {\n\tif withDatePrefix {\n\t\treturn fmt.Sprintf(\"Date: %s\", time.Now().Format(dateFormat))\n\t}\n\treturn time.Now().Format(dateFormat)\n}\n\nfunc getUTC() string {\n\treturn fmt.Sprintf(\"UTC: %s\", time.Now().UTC().Format(time.RFC3339))\n}\n\nfunc getEpoch() string {\n\treturn fmt.Sprintf(\"Epoch: %d\", time.Now().Unix())\n}\n\n// Renders the clock as string by accessing appropriate font from configured in settings\nfunc renderClock(widgetSettings Settings) (string, bool) {\n\tvar digFont ClockFont\n\tclockTime := getHourMinute(widgetSettings.hourFormat)\n\tdigFont = getFont(widgetSettings)\n\n\tchars := [][]string{}\n\tfor _, char := range clockTime {\n\t\tchars = append(chars, digFont.get(string(char)))\n\t}\n\n\tneedBorder := digFont.fontRows <= minRowsForBorder\n\treturn fontsJoin(chars, digFont.fontRows, widgetSettings.color), needBorder\n}\n"
  },
  {
    "path": "modules/digitalclock/display.go",
    "content": "package digitalclock\n\nimport \"strings\"\n\nfunc mergeLines(outString []string) string {\n\treturn strings.Join(outString, \"\\n\")\n}\n\nfunc renderWidget(widgetSettings Settings) string {\n\tvar outputStrings []string\n\n\tclockString, needBorder := renderClock(widgetSettings)\n\tif needBorder {\n\t\toutputStrings = append(outputStrings, mergeLines([]string{\"\", clockString, \"\"}))\n\t} else {\n\t\toutputStrings = append(outputStrings, clockString)\n\t}\n\n\tif widgetSettings.withDate {\n\t\toutputStrings = append(outputStrings, getDate(widgetSettings.dateFormat, widgetSettings.withDatePrefix))\n\t}\n\n\tif widgetSettings.withUTC {\n\t\toutputStrings = append(outputStrings, getUTC())\n\t}\n\n\tif widgetSettings.withEpoch {\n\t\toutputStrings = append(outputStrings, getEpoch())\n\t}\n\n\treturn mergeLines(outputStrings)\n}\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(func() (string, string, bool) {\n\t\ttitle := widget.CommonSettings().Title\n\t\tif widget.settings.dateTitle {\n\t\t\ttitle = getDate(widget.settings.dateFormat, false)\n\t\t}\n\t\treturn title, renderWidget(*widget.settings), false\n\t})\n}\n"
  },
  {
    "path": "modules/digitalclock/fonts.go",
    "content": "package digitalclock\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// ClockFontInterface to makes sure all fonts implement join and get methods\ntype ClockFontInterface interface {\n\tjoin() string\n\tget() string\n}\n\n// ClockFont struct to hold the font info\ntype ClockFont struct {\n\tfontRows int\n\tfonts    map[string][]string\n}\n\n// function to join fonts, since the fonts have multi rows\nfunc fontsJoin(fontCharArray [][]string, rows int, color string) string {\n\toutString := \"\"\n\n\tfor i := 0; i < rows; i++ {\n\t\toutString += fmt.Sprintf(\"[%s]\", color)\n\t\tfor _, charFont := range fontCharArray {\n\t\t\toutString += \" \" + fmt.Sprintf(\"[%s]%s\", color, charFont[i])\n\t\t}\n\t\toutString += \"\\n\"\n\t}\n\treturn strings.TrimSuffix(outString, \"\\n\")\n}\n\nfunc (font *ClockFont) get(char string) []string {\n\treturn font.fonts[char]\n}\n\nfunc getDigitalFont() ClockFont {\n\tfontsMap := map[string][]string{\n\t\t\"1\": {\"▄█ \", \" █ \", \"▄█▄\"},\n\t\t\"2\": {\"█▀█\", \" ▄▀\", \"█▄▄\"},\n\t\t\"3\": {\"█▀▀█\", \"  ▀▄\", \"█▄▄█\"},\n\t\t\"4\": {\" █▀█ \", \"█▄▄█▄\", \"   █ \"},\n\t\t\"5\": {\"█▀▀\", \"▀▀▄\", \"▄▄▀\"},\n\t\t\"6\": {\"▄▀▀▄\", \"█▄▄ \", \"▀▄▄▀\"},\n\t\t\"7\": {\"▀▀▀█\", \"  █ \", \" ▐▌ \"},\n\t\t\"8\": {\"▄▀▀▄\", \"▄▀▀▄\", \"▀▄▄▀\"},\n\t\t\"9\": {\"▄▀▀▄\", \"▀▄▄█\", \" ▄▄▀\"},\n\t\t\"0\": {\"█▀▀█\", \"█  █\", \"█▄▄█\"},\n\t\t\":\": {\"█\", \" \", \"█\"},\n\t\t\" \": {\" \", \" \", \" \"},\n\t\t\"A\": {\"\", \"\", \"AM\"},\n\t\t\"P\": {\"\", \"\", \"PM\"},\n\t}\n\n\tdigitalFont := ClockFont{fontRows: 3, fonts: fontsMap}\n\treturn digitalFont\n}\n\nfunc getBigFont() ClockFont {\n\tfontsMap := map[string][]string{\n\t\t\"1\": {\" ┏┓ \", \"┏┛┃ \", \"┗┓┃ \", \" ┃┃ \", \"┏┛┗┓\", \"┗━━┛\"},\n\t\t\"2\": {\"┏━━━┓\", \"┃┏━┓┃\", \"┗┛┏┛┃\", \"┏━┛┏┛\", \"┃ ┗━┓\", \"┗━━━┛\"},\n\t\t\"3\": {\"┏━━━┓\", \"┃┏━┓┃\", \"┗┛┏┛┃\", \"┏┓┗┓┃\", \"┃┗━┛┃\", \"┗━━━┛\"},\n\t\t\"4\": {\"┏┓ ┏┓\", \"┃┃ ┃┃\", \"┃┗━┛┃\", \"┗━━┓┃\", \"   ┃┃\", \"   ┗┛\"},\n\t\t\"5\": {\"┏━━━┓\", \"┃┏━━┛\", \"┃┗━━┓\", \"┗━━┓┃\", \"┏━━┛┃\", \"┗━━━┛\"},\n\t\t\"6\": {\"┏━━━┓\", \"┃┏━━┛\", \"┃┗━━┓\", \"┃┏━┓┃\", \"┃┗━┛┃\", \"┗━━━┛\"},\n\t\t\"7\": {\"┏━━━┓\", \"┃┏━┓┃\", \"┗┛┏┛┃\", \"  ┃┏┛\", \"  ┃┃ \", \"  ┗┛ \"},\n\t\t\"8\": {\"┏━━━┓\", \"┃┏━┓┃\", \"┃┗━┛┃\", \"┃┏━┓┃\", \"┃┗━┛┃\", \"┗━━━┛\"},\n\t\t\"9\": {\"┏━━━┓\", \"┃┏━┓┃\", \"┃┗━┛┃\", \"┗━━┓┃\", \"┏━━┛┃\", \"┗━━━┛\"},\n\t\t\"0\": {\"┏━━━┓\", \"┃┏━┓┃\", \"┃┃ ┃┃\", \"┃┃ ┃┃\", \"┃┗━┛┃\", \"┗━━━┛\"},\n\t\t\":\": {\"   \", \"┏━┓\", \"┗━┛\", \"┏━┓\", \"┗━┛\", \"   \"},\n\t\t\" \": {\"   \", \"   \", \"   \", \"   \", \"   \", \"   \"},\n\t\t\"A\": {\"\", \"\", \"\", \"\", \"\", \"AM\"},\n\t\t\"P\": {\"\", \"\", \"\", \"\", \"\", \"PM\"},\n\t}\n\n\tbigFont := ClockFont{fontRows: 6, fonts: fontsMap}\n\treturn bigFont\n}\n\nfunc getBoldFont() ClockFont {\n\tfontsMap := map[string][]string{\n\t\t\"1\": {\"██\", \"██\", \"██\", \"██\", \"██\"},\n\t\t\"2\": {\"██████\", \"    ██\", \"██████\", \"██    \", \"██████\"},\n\t\t\"3\": {\"██████\", \"    ██\", \"██████\", \"    ██\", \"██████\"},\n\t\t\"4\": {\"██  ██\", \"██  ██\", \"██████\", \"    ██\", \"    ██\"},\n\t\t\"5\": {\"██████\", \"██    \", \"██████\", \"    ██\", \"██████\"},\n\t\t\"6\": {\"██████\", \"██    \", \"██████\", \"██  ██\", \"██████\"},\n\t\t\"7\": {\"██████\", \"    ██\", \"    ██\", \"    ██\", \"    ██\"},\n\t\t\"8\": {\"██████\", \"██  ██\", \"██████\", \"██  ██\", \"██████\"},\n\t\t\"9\": {\"██████\", \"██  ██\", \"██████\", \"    ██\", \"██████\"},\n\t\t\"0\": {\"██████\", \"██  ██\", \"██  ██\", \"██  ██\", \"██████\"},\n\t\t\":\": {\"  \", \"██\", \"  \", \"██\", \"  \"},\n\t\t\" \": {\"  \", \"  \", \"  \", \"  \", \"  \"},\n\t\t\"A\": {\"\", \"\", \"\", \"\", \"AM\"},\n\t\t\"P\": {\"\", \"\", \"\", \"\", \"PM\"},\n\t}\n\n\tboldFont := ClockFont{fontRows: 5, fonts: fontsMap}\n\treturn boldFont\n}\n\n// getFont returns appropriate font map based on the font settings\nfunc getFont(widgetSettings Settings) ClockFont {\n\tswitch strings.ToLower(widgetSettings.font) {\n\tcase \"digitalfont\":\n\t\treturn getDigitalFont()\n\tcase \"boldfont\":\n\t\treturn getBoldFont()\n\tdefault:\n\t\treturn getBigFont()\n\t}\n}\n"
  },
  {
    "path": "modules/digitalclock/settings.go",
    "content": "package digitalclock\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Clocks\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tcolor          string `help:\"The color of the clock.\"`\n\tfont           string `help:\"The font of the clock.\" values:\"bigfont or digitalfont\"`\n\thourFormat     string `help:\"The format of the clock.\" values:\"12 or 24\"`\n\tdateFormat     string `help:\"The format of the date.\"`\n\tdateTitle      bool   `help:\"Whether or not to display date as widget title\"`\n\twithDate       bool   `help:\"Whether or not to display date information\"`\n\twithUTC        bool   `help:\"Whether or not to display UTC information\"`\n\twithEpoch      bool   `help:\"Whether or not to display Epoch information\"`\n\twithDatePrefix bool   `help:\"Whether or not to display Date: prefix\"`\n\tcenterAlign    bool   `help:\"Whether or not to use center align in widget\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tcolor:          ymlConfig.UString(\"color\"),\n\t\tfont:           ymlConfig.UString(\"font\"),\n\t\thourFormat:     ymlConfig.UString(\"hourFormat\", \"24\"),\n\t\tdateFormat:     ymlConfig.UString(\"dateFormat\", \"Monday January 02 2006\"),\n\t\tdateTitle:      ymlConfig.UBool(\"dateTitle\", false),\n\t\twithDate:       ymlConfig.UBool(\"withDate\", true),\n\t\twithUTC:        ymlConfig.UBool(\"withUTC\", true),\n\t\twithEpoch:      ymlConfig.UBool(\"withEpoch\", true),\n\t\twithDatePrefix: ymlConfig.UBool(\"withDatePrefix\", true),\n\t\tcenterAlign:    ymlConfig.UBool(\"centerAlign\", false),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/digitalclock/widget.go",
    "content": "package digitalclock\n\nimport (\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget is a text widget struct to hold info about the current widget\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n}\n\n// NewWidget creates a new widget using settings\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\tif settings.centerAlign {\n\t\twidget.View.SetTextAlign(tview.AlignCenter)\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\twidget.display()\n}\n"
  },
  {
    "path": "modules/digitalocean/display.go",
    "content": "package digitalocean\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst maxColWidth = 12\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tcolumnSet := widget.settings.columns\n\n\ttitle := widget.CommonSettings().Title\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif len(columnSet) < 1 {\n\t\treturn title, \" no columns defined\", false\n\t}\n\n\tstr := fmt.Sprintf(\" [::b][%s]\", widget.settings.Colors.Subheading)\n\n\tfor _, colName := range columnSet {\n\t\ttruncName := utils.Truncate(colName, maxColWidth, false)\n\n\t\tstr += fmt.Sprintf(\"%-12s\", truncName)\n\t}\n\n\tstr += \"\\n\"\n\n\tfor idx, droplet := range widget.droplets {\n\t\t// This defines the formatting for the row, one tab-separated string for each defined column\n\t\tfmtStr := \" [%s]\"\n\n\t\tfor range columnSet {\n\t\t\tfmtStr += \"%-12s\"\n\t\t}\n\n\t\tvals := []interface{}{\n\t\t\twidget.RowColor(idx),\n\t\t}\n\n\t\t// Dynamically access the droplet to get the requested columns values\n\t\tfor _, colName := range columnSet {\n\t\t\tval, err := droplet.StringValueForProperty(colName)\n\t\t\tif err != nil {\n\t\t\t\tval = \"???\"\n\t\t\t}\n\n\t\t\ttruncVal := utils.Truncate(val, maxColWidth, false)\n\n\t\t\tvals = append(vals, truncVal)\n\t\t}\n\n\t\t// And format, print, and color the row\n\t\trow := fmt.Sprintf(fmtStr, vals...)\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, 33)\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n"
  },
  {
    "path": "modules/digitalocean/droplet.go",
    "content": "package digitalocean\n\nimport (\n\t\"strings\"\n\n\t\"github.com/digitalocean/godo\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\n// Droplet represents WTF's view of a DigitalOcean droplet\ntype Droplet struct {\n\tgodo.Droplet\n\n\tImage  godo.Image\n\tRegion godo.Region\n}\n\n// NewDroplet creates and returns an instance of Droplet\nfunc NewDroplet(doDroplet godo.Droplet) *Droplet {\n\treturn &Droplet{\n\t\tdoDroplet,\n\t\t*doDroplet.Image,\n\t\t*doDroplet.Region,\n\t}\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// StringValueForProperty returns a string value for the given column\nfunc (drop *Droplet) StringValueForProperty(propName string) (string, error) {\n\t// Figure out if we should forward this property to a sub-object\n\t// Lets us support \"Region.Name\" column definitions\n\tsplit := strings.Split(propName, \".\")\n\n\tswitch split[0] {\n\tcase \"Image\":\n\t\treturn utils.StringValueForProperty(drop.Image, split[1])\n\tcase \"Region\":\n\t\treturn utils.StringValueForProperty(drop.Region, split[1])\n\tdefault:\n\t\treturn utils.StringValueForProperty(drop, propName)\n\t}\n}\n"
  },
  {
    "path": "modules/digitalocean/droplet_properties_table.go",
    "content": "package digitalocean\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype dropletPropertiesTable struct {\n\tdroplet     *Droplet\n\tpropertyMap map[string]string\n\n\tcolWidth0   int\n\tcolWidth1   int\n\ttableHeight int\n}\n\n// newDropletPropertiesTable creates and returns an instance of DropletPropertiesTable\nfunc newDropletPropertiesTable(droplet *Droplet) *dropletPropertiesTable {\n\tpropTable := &dropletPropertiesTable{\n\t\tdroplet: droplet,\n\n\t\tcolWidth0:   24,\n\t\tcolWidth1:   47,\n\t\ttableHeight: 16,\n\t}\n\n\tpropTable.propertyMap = propTable.buildPropertyMap()\n\n\treturn propTable\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// buildPropertyMap creates a mapping of droplet property names to droplet property values\nfunc (propTable *dropletPropertiesTable) buildPropertyMap() map[string]string {\n\tpropMap := map[string]string{}\n\n\tif propTable.droplet == nil {\n\t\treturn propMap\n\t}\n\n\tpublicV4, _ := propTable.droplet.PublicIPv4()\n\tpublicV6, _ := propTable.droplet.PublicIPv6()\n\n\tpropMap[\"CPUs\"] = strconv.Itoa(propTable.droplet.Vcpus)\n\tpropMap[\"Created\"] = propTable.droplet.Created\n\tpropMap[\"Disk\"] = strconv.Itoa(propTable.droplet.Disk)\n\tpropMap[\"Features\"] = utils.Truncate(strings.Join(propTable.droplet.Features, \",\"), propTable.colWidth1, true)\n\tpropMap[\"Image\"] = fmt.Sprintf(\"%s (%s)\", propTable.droplet.Image.Name, propTable.droplet.Image.Distribution)\n\tpropMap[\"Memory\"] = strconv.Itoa(propTable.droplet.Memory)\n\tpropMap[\"Public IP v4\"] = publicV4\n\tpropMap[\"Public IP v6\"] = publicV6\n\tpropMap[\"Region\"] = fmt.Sprintf(\"%s (%s)\", propTable.droplet.Region.Name, propTable.droplet.Region.Slug)\n\tpropMap[\"Size\"] = propTable.droplet.SizeSlug\n\tpropMap[\"Status\"] = propTable.droplet.Status\n\tpropMap[\"Tags\"] = utils.Truncate(strings.Join(propTable.droplet.Tags, \",\"), propTable.colWidth1, true)\n\tpropMap[\"URN\"] = utils.Truncate(propTable.droplet.URN(), propTable.colWidth1, true)\n\tpropMap[\"VPC\"] = propTable.droplet.VPCUUID\n\n\treturn propMap\n}\n\n// render creates a new Table and returns it as a displayable string\nfunc (propTable *dropletPropertiesTable) render() string {\n\ttbl := view.NewInfoTable(\n\t\t[]string{\"Property\", \"Value\"},\n\t\tpropTable.propertyMap,\n\t\tpropTable.colWidth0,\n\t\tpropTable.colWidth1,\n\t\tpropTable.tableHeight,\n\t)\n\n\treturn tbl.Render()\n}\n"
  },
  {
    "path": "modules/digitalocean/keyboard.go",
    "content": "package digitalocean\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"?\", widget.showInfo, \"Show info about the selected droplet\")\n\n\twidget.SetKeyboardChar(\"b\", widget.dropletRestart, \"Reboot the selected droplet\")\n\twidget.SetKeyboardChar(\"j\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"k\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"p\", widget.dropletEnabledPrivateNetworking, \"Enable private networking for the selected drople\")\n\twidget.SetKeyboardChar(\"s\", widget.dropletShutDown, \"Shut down the selected droplet\")\n\twidget.SetKeyboardChar(\"u\", widget.Unselect, \"Clear selection\")\n\n\twidget.SetKeyboardKey(tcell.KeyCtrlD, widget.dropletDestroy, \"Destroy the selected droplet\")\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.showInfo, \"Show info about the selected droplet\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n}\n"
  },
  {
    "path": "modules/digitalocean/settings.go",
    "content": "package digitalocean\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"DigitalOcean\"\n)\n\n// defaultColumns defines the default set of columns to display in the widget\n// This can be over-ridden in the cofig by explicitly defining a set of columns\nvar defaultColumns = []interface{}{\n\t\"Name\",\n\t\"Status\",\n\t\"Region.Slug\",\n}\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey     string   `help:\"Your DigitalOcean API key.\"`\n\tcolumns    []string `help:\"A list of the droplet properties to display.\"`\n\tdateFormat string   `help:\"The format to display dates and times in.\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:     ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_DIGITALOCEAN_API_KEY\"))),\n\t\tcolumns:    utils.ToStrs(ymlConfig.UList(\"columns\", defaultColumns)),\n\t\tdateFormat: ymlConfig.UString(\"dateFormat\", wtf.DateFormat),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/digitalocean/widget.go",
    "content": "package digitalocean\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/digitalocean/godo\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\t\"golang.org/x/oauth2\"\n)\n\n/* -------------------- Oauth2 Token -------------------- */\n\ntype tokenSource struct {\n\tAccessToken string\n}\n\n// Token creates and returns an Oauth2 token\nfunc (t *tokenSource) Token() (*oauth2.Token, error) {\n\ttoken := &oauth2.Token{\n\t\tAccessToken: t.AccessToken,\n\t}\n\treturn token, nil\n}\n\n/* -------------------- Widget -------------------- */\n\n// Widget is the container for droplet data\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tapp      *tview.Application\n\tclient   *godo.Client\n\tdroplets []*Droplet\n\tpages    *tview.Pages\n\tsettings *Settings\n\n\terr error\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tapp:      tviewApp,\n\t\tpages:    pages,\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\n\twidget.View.SetScrollable(true)\n\n\twidget.SetRenderFunction(widget.display)\n\n\twidget.createClient()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Fetch retrieves droplet data\nfunc (widget *Widget) Fetch() error {\n\tif widget.client == nil {\n\t\treturn errors.New(\"client could not be initialized\")\n\t}\n\n\tvar err error\n\twidget.droplets, err = widget.dropletsFetch()\n\treturn err\n}\n\n// Next selects the next item in the list\nfunc (widget *Widget) Next() {\n\twidget.ScrollableWidget.Next()\n}\n\n// Prev selects the previous item in the list\nfunc (widget *Widget) Prev() {\n\twidget.ScrollableWidget.Prev()\n}\n\n// Refresh updates the data for this widget and displays it onscreen\nfunc (widget *Widget) Refresh() {\n\terr := widget.Fetch()\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\twidget.err = nil\n\t\twidget.SetItemCount(len(widget.droplets))\n\t}\n\n\twidget.display()\n}\n\n// Unselect clears the selection of list items\nfunc (widget *Widget) Unselect() {\n\twidget.ScrollableWidget.Unselect()\n\twidget.RenderFunction()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// createClient create a persisten DigitalOcean client for use in the calls below\nfunc (widget *Widget) createClient() {\n\ttokenSource := &tokenSource{\n\t\tAccessToken: widget.settings.apiKey,\n\t}\n\n\toauthClient := oauth2.NewClient(context.Background(), tokenSource)\n\twidget.client = godo.NewClient(oauthClient)\n}\n\n// currentDroplet returns the currently-selected droplet, if there is one\n// Returns nil if no droplet is selected\nfunc (widget *Widget) currentDroplet() *Droplet {\n\tif len(widget.droplets) == 0 {\n\t\treturn nil\n\t}\n\n\tif len(widget.droplets) <= widget.Selected {\n\t\treturn nil\n\t}\n\n\treturn widget.droplets[widget.Selected]\n}\n\n// dropletsFetch uses the DigitalOcean API to fetch information about all the available droplets\nfunc (widget *Widget) dropletsFetch() ([]*Droplet, error) {\n\tdropletList := []*Droplet{}\n\topts := &godo.ListOptions{}\n\n\tfor {\n\t\tdoDroplets, resp, err := widget.client.Droplets.List(context.Background(), opts)\n\t\tif err != nil {\n\t\t\treturn dropletList, err\n\t\t}\n\n\t\tfor _, doDroplet := range doDroplets {\n\t\t\tdroplet := NewDroplet(doDroplet)\n\t\t\tdropletList = append(dropletList, droplet)\n\t\t}\n\n\t\tif resp.Links == nil || resp.Links.IsLastPage() {\n\t\t\tbreak\n\t\t}\n\n\t\tpage, err := resp.Links.CurrentPage()\n\t\tif err != nil {\n\t\t\treturn dropletList, err\n\t\t}\n\n\t\t// Set the page we want for the next request\n\t\topts.Page = page + 1\n\t}\n\n\treturn dropletList, nil\n}\n\n/* -------------------- Droplet Actions -------------------- */\n\n// dropletDestroy destroys the selected droplet\nfunc (widget *Widget) dropletDestroy() {\n\tcurrDroplet := widget.currentDroplet()\n\tif currDroplet == nil {\n\t\treturn\n\t}\n\n\t_, err := widget.client.Droplets.Delete(context.Background(), currDroplet.ID)\n\tif err != nil {\n\t\treturn\n\t}\n\n\twidget.dropletRemoveSelected()\n\twidget.Refresh()\n}\n\n// dropletEnabledPrivateNetworking enabled private networking on the selected droplet\nfunc (widget *Widget) dropletEnabledPrivateNetworking() {\n\tcurrDroplet := widget.currentDroplet()\n\tif currDroplet == nil {\n\t\treturn\n\t}\n\n\t_, _, err := widget.client.DropletActions.EnablePrivateNetworking(context.Background(), currDroplet.ID)\n\tif err != nil {\n\t\treturn\n\t}\n\n\twidget.Refresh()\n}\n\n// dropletRemoveSelected removes the currently-selected droplet from the internal list of droplets\nfunc (widget *Widget) dropletRemoveSelected() {\n\tcurrDroplet := widget.currentDroplet()\n\tif currDroplet != nil {\n\t\twidget.droplets[len(widget.droplets)-1], widget.droplets[widget.Selected] = widget.droplets[widget.Selected], widget.droplets[len(widget.droplets)-1]\n\t\twidget.droplets = widget.droplets[:len(widget.droplets)-1]\n\t}\n}\n\n// dropletRestart restarts the selected droplet\nfunc (widget *Widget) dropletRestart() {\n\tcurrDroplet := widget.currentDroplet()\n\tif currDroplet == nil {\n\t\treturn\n\t}\n\n\t_, _, err := widget.client.DropletActions.Reboot(context.Background(), currDroplet.ID)\n\tif err != nil {\n\t\treturn\n\t}\n\twidget.Refresh()\n}\n\n// dropletShutDown powers down the selected droplet\nfunc (widget *Widget) dropletShutDown() {\n\tcurrDroplet := widget.currentDroplet()\n\tif currDroplet == nil {\n\t\treturn\n\t}\n\n\t_, _, err := widget.client.DropletActions.Shutdown(context.Background(), currDroplet.ID)\n\tif err != nil {\n\t\treturn\n\t}\n\twidget.Refresh()\n}\n\n/* -------------------- Common Actions -------------------- */\n\n// showInfo shows a modal window with information about the selected droplet\nfunc (widget *Widget) showInfo() {\n\tdroplet := widget.currentDroplet()\n\tif droplet == nil {\n\t\treturn\n\t}\n\n\tcloseFunc := func() {\n\t\twidget.pages.RemovePage(\"info\")\n\t\twidget.app.SetFocus(widget.View)\n\t}\n\n\tpropTable := newDropletPropertiesTable(droplet).render()\n\tpropTable += utils.CenterText(\"Esc to close\", 80)\n\n\tmodal := view.NewBillboardModal(propTable, closeFunc)\n\tmodal.SetTitle(fmt.Sprintf(\"  %s  \", droplet.Name))\n\n\twidget.pages.AddPage(\"info\", modal, false, true)\n\twidget.app.SetFocus(modal)\n\n\twidget.app.QueueUpdateDraw(func() {\n\t\twidget.app.Draw()\n\t})\n}\n"
  },
  {
    "path": "modules/docker/client.go",
    "content": "package docker\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/docker/docker/api/types\"\n\t\"github.com/docker/docker/api/types/container\"\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc (widget *Widget) getSystemInfo() string {\n\tinfo, err := widget.cli.Info(context.Background())\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not get docker system info\").Error()\n\t}\n\n\tdiskUsage, err := widget.cli.DiskUsage(context.Background(), types.DiskUsageOptions{})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not get disk usage\").Error()\n\t}\n\n\tvar duContainer int64\n\tfor _, c := range diskUsage.Containers {\n\t\tduContainer += c.SizeRw\n\t}\n\tvar duImg int64\n\tfor _, im := range diskUsage.Images {\n\t\tduImg += im.Size\n\t}\n\tvar duVol int64\n\tfor _, v := range diskUsage.Volumes {\n\t\tduVol += v.UsageData.Size\n\t}\n\n\tsysInfo := []struct {\n\t\tname  string\n\t\tvalue string\n\t}{\n\t\t{\n\t\t\tname:  \"name:\",\n\t\t\tvalue: fmt.Sprintf(\"[%s]%s\", widget.settings.Colors.EvenForeground, info.Name),\n\t\t}, {\n\t\t\tname:  \"version:\",\n\t\t\tvalue: fmt.Sprintf(\"[%s]%s\", widget.settings.Colors.EvenForeground, info.ServerVersion),\n\t\t}, {\n\t\t\tname:  \"root:\",\n\t\t\tvalue: fmt.Sprintf(\"[%s]%s\", widget.settings.Colors.EvenForeground, info.DockerRootDir),\n\t\t},\n\t\t{\n\t\t\tname: \"containers:\",\n\t\t\tvalue: fmt.Sprintf(\"[lime]%d[white]/[yellow]%d[white]/[red]%d\",\n\t\t\t\tinfo.ContainersRunning,\n\t\t\t\tinfo.ContainersPaused, info.ContainersStopped),\n\t\t},\n\t\t{\n\t\t\tname:  \"images:\",\n\t\t\tvalue: fmt.Sprintf(\"[%s]%d\", widget.settings.Colors.EvenForeground, info.Images),\n\t\t},\n\t\t{\n\t\t\tname:  \"volumes:\",\n\t\t\tvalue: fmt.Sprintf(\"[%s]%v\", widget.settings.Colors.EvenForeground, len(diskUsage.Volumes)),\n\t\t},\n\t\t{\n\t\t\tname:  \"memory limit:\",\n\t\t\tvalue: fmt.Sprintf(\"[%s]%s\", widget.settings.Colors.EvenForeground, humanize.Bytes(uint64(info.MemTotal))),\n\t\t},\n\t\t{\n\t\t\tname: \"disk usage:\",\n\t\t\tvalue: fmt.Sprintf(`\n    [%s]* containers: [%s]%s\n    [%s]* images:     [%s]%s\n    [%s]* volumes:    [%s]%s\n    [%s]* [::b]total:      [%s]%s[::-]\n`,\n\t\t\t\twidget.settings.labelColor,\n\t\t\t\twidget.settings.Colors.EvenForeground,\n\t\t\t\thumanize.Bytes(uint64(duContainer)),\n\n\t\t\t\twidget.settings.labelColor,\n\t\t\t\twidget.settings.Colors.EvenForeground,\n\t\t\t\thumanize.Bytes(uint64(duImg)),\n\n\t\t\t\twidget.settings.labelColor,\n\t\t\t\twidget.settings.Colors.EvenForeground,\n\t\t\t\thumanize.Bytes(uint64(duVol)),\n\n\t\t\t\twidget.settings.labelColor,\n\t\t\t\twidget.settings.Colors.EvenForeground,\n\t\t\t\thumanize.Bytes(uint64(duContainer+duImg+duVol))),\n\t\t},\n\t}\n\n\tpadSlice(true, sysInfo, func(i int) string {\n\t\treturn sysInfo[i].name\n\t}, func(i int, newVal string) {\n\t\tsysInfo[i].name = newVal\n\t})\n\n\tresult := \"\"\n\tfor _, info := range sysInfo {\n\t\tresult += fmt.Sprintf(\"[%s]%s %s\\n\", widget.settings.labelColor, info.name, info.value)\n\t}\n\n\treturn result\n}\n\nfunc (widget *Widget) getContainerStates() string {\n\tcntrs, err := widget.cli.ContainerList(context.Background(), container.ListOptions{All: true})\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \" could not get container list\").Error()\n\t}\n\n\tif len(cntrs) == 0 {\n\t\treturn \" no containers\"\n\t}\n\n\tcolorMap := map[string]string{\n\t\t\"created\":    \"green\",\n\t\t\"running\":    \"lime\",\n\t\t\"paused\":     \"yellow\",\n\t\t\"restarting\": \"yellow\",\n\t\t\"removing\":   \"yellow\",\n\t\t\"exited\":     \"red\",\n\t\t\"dead\":       \"red\",\n\t}\n\n\tcontainers := []struct {\n\t\tname  string\n\t\tstate string\n\t}{}\n\tfor _, c := range cntrs {\n\t\tcontainer := struct {\n\t\t\tname  string\n\t\t\tstate string\n\t\t}{\n\t\t\tname:  c.Names[0],\n\t\t\tstate: c.State,\n\t\t}\n\n\t\tcontainer.name = strings.ReplaceAll(container.name, \"/\", \"\")\n\t\tcontainers = append(containers, container)\n\t}\n\n\tsort.Slice(containers, func(i, j int) bool {\n\t\treturn containers[i].name < containers[j].name\n\t})\n\n\tpadSlice(false, containers, func(i int) string {\n\t\treturn containers[i].name\n\t}, func(i int, val string) {\n\t\tcontainers[i].name = val\n\t})\n\n\tresult := \"\"\n\tfor _, c := range containers {\n\t\tresult += fmt.Sprintf(\"[white]%s [%s]%s\\n\", c.name, colorMap[c.state], c.state)\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "modules/docker/example-conf.yml",
    "content": "wtf:\n  colors:\n    # background: black\n    # foreground: blue\n    border:\n      focusable: darkslateblue\n      focused: orange\n      normal: gray\n    checked: yellow\n    highlight: \n      fore: black\n      back: gray\n    rows:\n      even: yellow\n      odd: white\n  grid:\n    # How _wide_ the columns are, in terminal characters. In this case we have\n    # four columns, each of which are 35 characters wide.\n    # columns: [50, ]\n    # How _high_ the rows are, in terminal lines. In this case we have four rows\n    # that support ten line of text and one of four.\n    # rows: [50]\n  refreshInterval: 1\n  openFileUtil: \"open\"\n  mods:\n    docker:\n      type: docker\n      title: \"💻\"\n      enabled: true\n      position:\n        top: 0\n        left: 0\n        height: 3\n        width: 3\n      refreshInterval: 1\n      labelColor: lightblue\n  "
  },
  {
    "path": "modules/docker/settings.go",
    "content": "package docker\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"docker\"\n)\n\n// Settings defines the configuration options for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tlabelColor string\n}\n\n// NewSettingsFromYAML creates and returns an instance of Settings with configuration options populated\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon:     cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t\tlabelColor: ymlConfig.UString(\"labelColor\", \"white\"),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/docker/utils.go",
    "content": "package docker\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"reflect\"\n\t\"strconv\"\n)\n\nfunc padSlice(padLeft bool, slice interface{}, getter func(i int) string, setter func(i int, newVal string)) {\n\trv := reflect.ValueOf(slice)\n\tlength := rv.Len()\n\tmaxLen := 0\n\tfor i := 0; i < length; i++ {\n\t\tval := getter(i)\n\t\tmaxLen = int(math.Max(float64(len(val)), float64(maxLen)))\n\t}\n\n\tsign := \"-\"\n\tif padLeft {\n\t\tsign = \"\"\n\t}\n\n\tfor i := 0; i < length; i++ {\n\t\tval := getter(i)\n\t\tval = fmt.Sprintf(\"%\"+sign+strconv.Itoa(maxLen)+\"s\", val)\n\t\tsetter(i, val)\n\t}\n}\n"
  },
  {
    "path": "modules/docker/widget.go",
    "content": "package docker\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/docker/docker/client\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\tcli           *client.Client\n\tsettings      *Settings\n\tdisplayBuffer string\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\t\tsettings:   settings,\n\t}\n\n\twidget.View.SetScrollable(true)\n\n\tcli, err := client.NewClientWithOpts(client.FromEnv)\n\tif err != nil {\n\t\twidget.displayBuffer = errors.Wrap(err, \"could not create client\").Error()\n\t} else {\n\t\twidget.cli = cli\n\t}\n\n\twidget.refreshDisplayBuffer()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\twidget.refreshDisplayBuffer()\n\twidget.Redraw(widget.display)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) display() (string, string, bool) {\n\treturn widget.CommonSettings().Title, widget.displayBuffer, true\n}\n\nfunc (widget *Widget) refreshDisplayBuffer() {\n\tif widget.cli == nil {\n\t\treturn\n\t}\n\n\twidget.displayBuffer = \"\"\n\n\twidget.displayBuffer += fmt.Sprintf(\"[%s] System[white]\\n\", widget.settings.Colors.Subheading)\n\twidget.displayBuffer += widget.getSystemInfo()\n\n\twidget.displayBuffer += \"\\n\"\n\n\twidget.displayBuffer += fmt.Sprintf(\"[%s] Containers[white]\\n\", widget.settings.Colors.Subheading)\n\twidget.displayBuffer += widget.getContainerStates()\n}\n"
  },
  {
    "path": "modules/feedreader/keyboard.go",
    "content": "package feedreader\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openStory, \"Open story in browser\")\n\twidget.SetKeyboardChar(\"t\", widget.toggleDisplayText, \"Toggle display between title, link and title+content\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openStory, \"Open story in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/feedreader/settings.go",
    "content": "package feedreader\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Feed Reader\"\n)\n\ntype colors struct {\n\tsource      string `help:\"Color to use for feed source titles.\" optional:\"true\" default:\"green\"`\n\tpublishDate string `help:\"Color to use for publish dates.\" optional:\"true\" default:\"orange\"`\n}\n\n// auth stores [username, password]-credentials for private RSS feeds using Basic Auth\ntype auth struct {\n\tusername string\n\tpassword string\n}\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tcolors\n\n\tfeeds           []string        `help:\"An array of RSS and Atom feed URLs\"`\n\tfeedLimit       int             `help:\"The maximum number of stories to display for each feed\"`\n\tshowSource      bool            `help:\"Whether or not to show feed source in front of item titles.\" values:\"true or false\" optional:\"true\" default:\"true\"`\n\tshowPublishDate bool            `help:\"Whether or not to show publish date in front of item titles.\" values:\"true or false\" optional:\"true\" default:\"false\"`\n\tdateFormat      string          `help:\"Date format to use for publish dates\" values:\"Any valid Go time layout which is handled by Time.Format\" optional:\"true\" default:\"Jan 02\"`\n\tcredentials     map[string]auth `help:\"Map of private feed URLs with required authentication credentials\"`\n\tdisableHTTP2    bool            `help:\"Whether or not to use the HTTP/2 protocol. Certain sites, such as reddit.com, will not work unless HTTP/2 is disabled.\" values:\"true or false\" optional:\"true\" default:\"false\"`\n\tuserAgent       string          `help:\"HTTP User-Agent to use when fetching RSS feeds.\" optional:\"true\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig, globalConfig *config.Config) *Settings {\n\tsettings := &Settings{\n\t\tCommon:          cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t\tfeeds:           utils.ToStrs(ymlConfig.UList(\"feeds\")),\n\t\tfeedLimit:       ymlConfig.UInt(\"feedLimit\", -1),\n\t\tshowSource:      ymlConfig.UBool(\"showSource\", true),\n\t\tshowPublishDate: ymlConfig.UBool(\"showPublishDate\", false),\n\t\tdateFormat:      ymlConfig.UString(\"dateFormat\", \"Jan 02\"),\n\t\tcredentials:     make(map[string]auth),\n\t\tdisableHTTP2:    ymlConfig.UBool(\"disableHTTP2\", false),\n\t\tuserAgent:       ymlConfig.UString(\"userAgent\", \"wtfutil (https://github.com/wtfutil/wtf)\"),\n\t}\n\n\tsettings.source = ymlConfig.UString(\"colors.source\", \"green\")\n\tsettings.publishDate = ymlConfig.UString(\"colors.publishDate\", \"orange\")\n\n\t// If feeds cannot be parsed as list try parsing as a map with username+password fields\n\tif len(settings.feeds) == 0 {\n\t\tcredentials := make(map[string]auth)\n\t\tfeeds := make([]string, 0)\n\t\tfor url, creds := range ymlConfig.UMap(\"feeds\") {\n\t\t\tparsed, ok := creds.(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tuser, ok := parsed[\"username\"].(string)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpass, ok := parsed[\"password\"].(string)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcredentials[url] = auth{\n\t\t\t\tusername: user,\n\t\t\t\tpassword: pass,\n\t\t\t}\n\t\t\tfeeds = append(feeds, url)\n\t\t}\n\t\tsettings.feeds = feeds\n\t\tsettings.credentials = credentials\n\t}\n\n\treturn settings\n}\n"
  },
  {
    "path": "modules/feedreader/widget.go",
    "content": "package feedreader\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"html\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/mmcdole/gofeed\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\t\"jaytaylor.com/html2text\"\n)\n\ntype ShowType int\n\nconst (\n\tSHOW_TITLE ShowType = iota\n\tSHOW_LINK\n\tSHOW_CONTENT\n)\n\n// FeedItem represents an item returned from an RSS or Atom feed\ntype FeedItem struct {\n\titem        *gofeed.Item\n\tsourceTitle string\n\tviewed      bool\n}\n\n// Widget is the container for RSS and Atom data\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tstories  []*FeedItem\n\tparser   *gofeed.Parser\n\tsettings *Settings\n\terr      error\n\tshowType ShowType\n}\n\nfunc rotateShowType(showtype ShowType) ShowType {\n\treturnValue := SHOW_TITLE\n\tswitch showtype {\n\tcase SHOW_TITLE:\n\t\treturnValue = SHOW_LINK\n\tcase SHOW_LINK:\n\t\treturnValue = SHOW_CONTENT\n\tcase SHOW_CONTENT:\n\t\treturnValue = SHOW_TITLE\n\t}\n\treturn returnValue\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\tparser := gofeed.NewParser()\n\tif settings.disableHTTP2 {\n\t\t// If HTTP/2 is disabled, we override the parser client\n\t\t// with a client using a simple HTTP transport which\n\t\t// removes the client's default behavior of first\n\t\t// trying HTTP/2 before downgrading to older protocol\n\t\t// versions.\n\t\tparser.Client = &http.Client{\n\t\t\tTransport: &http.Transport{\n\t\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\t\t\tMaxVersion: tls.VersionTLS13,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\tparser.UserAgent = settings.userAgent\n\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tparser:   parser,\n\t\tsettings: settings,\n\t\tshowType: SHOW_TITLE,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Fetch retrieves RSS and Atom feed data\nfunc (widget *Widget) Fetch(feedURLs []string) ([]*FeedItem, error) {\n\tvar data []*FeedItem\n\n\tfor _, feedURL := range feedURLs {\n\t\tfeedItems, err := widget.fetchForFeed(feedURL)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdata = append(data, feedItems...)\n\t}\n\n\tdata = widget.sort(data)\n\n\treturn data, nil\n}\n\n// Refresh updates the data in the widget\nfunc (widget *Widget) Refresh() {\n\tfeedItems, err := widget.Fetch(widget.settings.feeds)\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.stories = nil\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\twidget.err = nil\n\t\twidget.stories = feedItems\n\t\twidget.SetItemCount(len(feedItems))\n\t}\n\n\twidget.Render()\n}\n\n// Render sets up the widget data for redrawing to the screen\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) fetchForFeed(feedURL string) ([]*FeedItem, error) {\n\tvar (\n\t\tfeed *gofeed.Feed\n\t\terr  error\n\t)\n\tif auth, isPrivateRSS := widget.settings.credentials[feedURL]; isPrivateRSS {\n\t\twidget.parser.AuthConfig = &gofeed.Auth{\n\t\t\tUsername: auth.username,\n\t\t\tPassword: auth.password,\n\t\t}\n\t\tfeed, err = widget.parser.ParseURL(feedURL)\n\t\twidget.parser.AuthConfig = nil\n\t} else {\n\t\tfeed, err = widget.parser.ParseURL(feedURL)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar feedItems []*FeedItem\n\n\tfor idx, gofeedItem := range feed.Items {\n\t\tif widget.settings.feedLimit >= 1 && idx >= widget.settings.feedLimit {\n\t\t\t// We only want to get the widget.settings.feedLimit latest articles,\n\t\t\t// not all of them. To get all, set feedLimit to < 1\n\t\t\tbreak\n\t\t}\n\n\t\tfeedItem := &FeedItem{\n\t\t\titem:        gofeedItem,\n\t\t\tsourceTitle: feed.Title,\n\t\t\tviewed:      false,\n\t\t}\n\n\t\tfeedItems = append(feedItems, feedItem)\n\t}\n\n\treturn feedItems, nil\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\tdata := widget.stories\n\tif len(data) == 0 {\n\t\treturn title, \"No data\", false\n\t}\n\tvar str string\n\n\tfor idx, feedItem := range data {\n\t\trowColor := widget.RowColor(idx)\n\n\t\tif feedItem.viewed {\n\t\t\t// Grays out viewed items in the list, while preserving background highlighting when selected\n\t\t\trowColor = \"gray\"\n\t\t\tif idx == widget.Selected {\n\t\t\t\trowColor = fmt.Sprintf(\"gray:%s\", widget.settings.Colors.HighlightedBackground)\n\t\t\t}\n\t\t}\n\n\t\tdisplayText := widget.getShowText(feedItem, rowColor)\n\n\t\trow := fmt.Sprintf(\n\t\t\t\"[%s]%2d. %s[white]\",\n\t\t\trowColor,\n\t\t\tidx+1,\n\t\t\tdisplayText,\n\t\t)\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(feedItem.item.Title))\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) getShowText(feedItem *FeedItem, rowColor string) string {\n\tif feedItem == nil {\n\t\treturn \"\"\n\t}\n\n\tspace := regexp.MustCompile(`\\s+`)\n\tsource := \"\"\n\tpublishDate := \"\"\n\ttitle := space.ReplaceAllString(feedItem.item.Title, \" \")\n\n\tif widget.settings.showSource && feedItem.sourceTitle != \"\" {\n\t\tsource = \"[\" + widget.settings.source + \"]\" + feedItem.sourceTitle + \" \"\n\t}\n\tif widget.settings.showPublishDate && feedItem.item.Published != \"\" {\n\t\tpublishDate = \"[\" + widget.settings.publishDate + \"]\" + feedItem.item.PublishedParsed.Format(widget.settings.dateFormat) + \" \"\n\t}\n\n\t// Convert any escaped characters to their character representation\n\ttitle = html.UnescapeString(source + publishDate + \"[\" + rowColor + \"]\" + title)\n\n\tswitch widget.showType {\n\tcase SHOW_LINK:\n\t\treturn feedItem.item.Link\n\tcase SHOW_CONTENT:\n\t\ttext, _ := html2text.FromString(feedItem.item.Content, html2text.Options{PrettyTables: true})\n\t\treturn strings.TrimSpace(title + \"\\n\" + strings.TrimSpace(text))\n\tdefault:\n\t\treturn title\n\t}\n}\n\n// feedItems are sorted by published date\nfunc (widget *Widget) sort(feedItems []*FeedItem) []*FeedItem {\n\tsort.Slice(feedItems, func(i, j int) bool {\n\t\treturn feedItems[i].item.PublishedParsed != nil &&\n\t\t\tfeedItems[j].item.PublishedParsed != nil &&\n\t\t\tfeedItems[i].item.PublishedParsed.After(*feedItems[j].item.PublishedParsed)\n\t})\n\n\treturn feedItems\n}\n\nfunc (widget *Widget) openStory() {\n\tsel := widget.GetSelected()\n\n\tif sel >= 0 && widget.stories != nil && sel < len(widget.stories) {\n\t\tstory := widget.stories[sel]\n\t\tstory.viewed = true\n\n\t\tutils.OpenFile(story.item.Link)\n\t}\n}\n\nfunc (widget *Widget) toggleDisplayText() {\n\twidget.showType = rotateShowType(widget.showType)\n\twidget.Render()\n}\n"
  },
  {
    "path": "modules/feedreader/widget_test.go",
    "content": "package feedreader\n\nimport (\n\t\"testing\"\n\n\t\"github.com/mmcdole/gofeed\"\n\t\"gotest.tools/assert\"\n)\n\nfunc Test_getShowText(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfeedItem *FeedItem\n\t\tshowType ShowType\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"with nil FeedItem\",\n\t\t\tfeedItem: nil,\n\t\t\tshowType: SHOW_TITLE,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"with plain title\",\n\t\t\tfeedItem: &FeedItem{\n\t\t\t\titem: &gofeed.Item{Title: \"Cats and Dogs\"},\n\t\t\t},\n\t\t\tshowType: SHOW_TITLE,\n\t\t\texpected: \"[white]Cats and Dogs\",\n\t\t},\n\t\t{\n\t\t\tname: \"with escaped title\",\n\t\t\tfeedItem: &FeedItem{\n\t\t\t\titem: &gofeed.Item{Title: \"&lt;Cats and Dogs&gt;\"},\n\t\t\t},\n\t\t\tshowType: SHOW_TITLE,\n\t\t\texpected: \"[white]<Cats and Dogs>\",\n\t\t},\n\t\t{\n\t\t\tname: \"with unescaped title\",\n\t\t\tfeedItem: &FeedItem{\n\t\t\t\titem: &gofeed.Item{Title: \"<Cats and Dogs>\"},\n\t\t\t},\n\t\t\tshowType: SHOW_TITLE,\n\t\t\texpected: \"[white]<Cats and Dogs>\",\n\t\t},\n\t\t{\n\t\t\tname: \"with source-title\",\n\t\t\tfeedItem: &FeedItem{\n\t\t\t\tsourceTitle: \"WTF\",\n\t\t\t\titem:        &gofeed.Item{Title: \"<Cats and Dogs>\"},\n\t\t\t},\n\t\t\tshowType: SHOW_TITLE,\n\t\t\texpected: \"[green]WTF [white]<Cats and Dogs>\",\n\t\t},\n\t\t{\n\t\t\tname: \"with link\",\n\t\t\tfeedItem: &FeedItem{\n\t\t\t\titem: &gofeed.Item{Title: \"Cats and Dogs\", Link: \"https://cats.com/dog.xml\"},\n\t\t\t},\n\t\t\tshowType: SHOW_LINK,\n\t\t\texpected: \"https://cats.com/dog.xml\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\twidget := &Widget{\n\t\t\t\tsettings: &Settings{\n\t\t\t\t\tcolors: colors{\n\t\t\t\t\t\tsource:      \"green\",\n\t\t\t\t\t\tpublishDate: \"orange\",\n\t\t\t\t\t},\n\t\t\t\t\tshowSource: true,\n\t\t\t\t},\n\t\t\t\tshowType: tt.showType,\n\t\t\t}\n\n\t\t\tactual := widget.getShowText(tt.feedItem, \"white\")\n\n\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/football/client.go",
    "content": "package football\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\nvar (\n\tfootballAPIUrl = \"https://api.football-data.org/v2\"\n)\n\ntype leagueInfo struct {\n\tid      int\n\tcaption string\n}\n\ntype Client struct {\n\tapiKey string\n}\n\nfunc NewClient(apiKey string) *Client {\n\tclient := Client{\n\t\tapiKey: apiKey,\n\t}\n\n\treturn &client\n}\n\nfunc (client *Client) footballRequest(path string, id int) (*http.Response, error) {\n\n\turl := fmt.Sprintf(\"%s/competitions/%d/%s\", footballAPIUrl, id, path)\n\treq, err := http.NewRequest(\"GET\", url, http.NoBody)\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Header.Add(\"X-Auth-Token\", client.apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "modules/football/settings.go",
    "content": "package football\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"football\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey        string `help:\"Your Football-data API token.\"`\n\tleague        string `help:\"Name of the competition. For example PL\"`\n\tfavTeam       string `help:\"Teams to follow in mentioned league\"`\n\tmatchesFrom   int    `help:\"Matches till Today (Today - Number of days), Default: 2\"`\n\tmatchesTo     int    `help:\"Matches from Today (Today + Number of days), Default: 5\"`\n\tstandingCount int    `help:\"Top N number of teams in standings, Default: 5\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:        ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_FOOTBALL_API_KEY\"))),\n\t\tleague:        ymlConfig.UString(\"league\", ymlConfig.UString(\"league\", os.Getenv(\"WTF_FOOTBALL_LEAGUE\"))),\n\t\tfavTeam:       ymlConfig.UString(\"favTeam\", ymlConfig.UString(\"favTeam\", os.Getenv(\"WTF_FOOTBALL_TEAM\"))),\n\t\tmatchesFrom:   ymlConfig.UInt(\"matchesFrom\", 5),\n\t\tmatchesTo:     ymlConfig.UInt(\"matchesTo\", 5),\n\t\tstandingCount: ymlConfig.UInt(\"standingCount\", 5),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\tsettings.SetDocumentationPath(\"sports/football\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/football/types.go",
    "content": "package football\n\ntype Team struct {\n\tName string `json:\"name\"`\n}\n\ntype LeagueStandings struct {\n\tStandings []struct {\n\t\tTable []Table `json:\"table\"`\n\t} `json:\"standings\"`\n}\n\ntype Table struct {\n\tDraw           int  `json:\"draw\"`\n\tGoalDifference int  `json:\"goalDifference\"`\n\tLost           int  `json:\"lost\"`\n\tWon            int  `json:\"won\"`\n\tPlayedGames    int  `json:\"playedGames\"`\n\tPoints         int  `json:\"points\"`\n\tPosition       int  `json:\"position\"`\n\tTeam           Team `json:\"team\"`\n}\n\ntype LeagueFixtuers struct {\n\tMatches []Matches `json:\"matches\"`\n}\n\ntype Matches struct {\n\tAwayTeam Team   `json:\"awayTeam\"`\n\tHomeTeam Team   `json:\"homeTeam\"`\n\tScore    Score  `json:\"score\"`\n\tStage    string `json:\"stage\"`\n\tStatus   string `json:\"status\"`\n\tDate     string `json:\"utcDate\"`\n}\n\ntype Score struct {\n\tFullTime ScoreByTime `json:\"fullTime\"`\n\tHalfTime ScoreByTime `json:\"halfTime\"`\n\tWinner   string      `json:\"winner\"`\n}\n\ntype ScoreByTime struct {\n\tAwayTeam int `json:\"awayTeam\"`\n\tHomeTeam int `json:\"homeTeam\"`\n}\n"
  },
  {
    "path": "modules/football/util.go",
    "content": "package football\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/olekukonko/tablewriter\"\n)\n\nfunc createTable(header []string, buf *bytes.Buffer) *tablewriter.Table {\n\n\ttable := tablewriter.NewWriter(buf)\n\tif len(header) != 0 {\n\t\ttable.SetHeader(header)\n\t}\n\ttable.SetBorder(false)\n\ttable.SetCenterSeparator(\" \")\n\ttable.SetColumnSeparator(\" \")\n\ttable.SetRowSeparator(\" \")\n\ttable.SetAlignment(tablewriter.ALIGN_LEFT)\n\n\treturn table\n}\n\nfunc parseDateString(d string) string {\n\n\treturn fmt.Sprintf(\"🕙 %s\", strings.Replace(d, \"T\", \" \", 1))\n}\n\nfunc getDateString(offset int) string {\n\n\ttoday := time.Now()\n\treturn today.AddDate(0, 0, offset).Format(\"2006-01-02\")\n\n}\n"
  },
  {
    "path": "modules/football/widget.go",
    "content": "package football\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nvar leagueID = map[string]leagueInfo{\n\t\"BSA\": {2013, \"Brazil Série A\"},\n\t\"PL\":  {2021, \"English Premier League\"},\n\t\"EC\":  {2016, \"English Championship\"},\n\t\"EUC\": {2018, \"European Championship\"},\n\t\"EL2\": {444, \"Campeonato Brasileiro da Série A\"},\n\t\"CL\":  {2001, \"UEFA Champions League\"},\n\t\"FL1\": {2015, \"French Ligue 1\"},\n\t\"GB\":  {2002, \"German Bundesliga\"},\n\t\"ISA\": {2019, \"Italy Serie A\"},\n\t\"NE\":  {2003, \"Netherlands Eredivisie\"},\n\t\"PPL\": {2017, \"Portugal Primeira Liga\"},\n\t\"SPD\": {2014, \"Spain Primera Division\"},\n\t\"WC\":  {2000, \"FIFA World Cup\"},\n}\n\ntype Widget struct {\n\tview.TextWidget\n\t*Client\n\tsettings *Settings\n\tLeague   leagueInfo\n\terr      error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\tvar widget Widget\n\n\tleagueId, err := getLeague(settings.league)\n\tif err != nil {\n\t\twidget = Widget{\n\t\t\terr:      fmt.Errorf(\"unable to get the league id for provided league '%s'\", settings.league),\n\t\t\tClient:   NewClient(settings.apiKey),\n\t\t\tsettings: settings,\n\t\t}\n\n\t\treturn &widget\n\t}\n\n\twidget = Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\t\tClient:     NewClient(settings.apiKey),\n\t\tLeague:     leagueId,\n\t\tsettings:   settings,\n\t}\n\n\treturn &widget\n}\n\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\n\tvar content string\n\ttitle := fmt.Sprintf(\"%s %s\", widget.CommonSettings().Title, widget.League.caption)\n\twrap := false\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\tcontent += widget.GetStandings(widget.League.id)\n\tcontent += widget.GetMatches(widget.League.id)\n\n\treturn title, content, wrap\n}\n\nfunc getLeague(league string) (leagueInfo, error) {\n\n\tvar l leagueInfo\n\tif val, ok := leagueID[league]; ok {\n\t\treturn val, nil\n\t}\n\treturn l, fmt.Errorf(\"no such league\")\n}\n\n// GetStandings of particular league\nfunc (widget *Widget) GetStandings(leagueId int) string {\n\n\tvar l LeagueStandings\n\tvar content string\n\tcontent += \"Standings:\\n\\n\"\n\tbuf := new(bytes.Buffer)\n\ttStandings := createTable([]string{\"No.\", \"Team\", \"MP\", \"Won\", \"Draw\", \"Lost\", \"GD\", \"Points\"}, buf)\n\tresp, err := widget.footballRequest(\"standings\", leagueId)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"Error fetching standings: %s\", err.Error())\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\tdata, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"Error fetching standings: %s\", err.Error())\n\t}\n\terr = json.Unmarshal(data, &l)\n\tif err != nil {\n\t\treturn \"Error fetching standings\"\n\t}\n\n\tif len(l.Standings) == 0 {\n\t\treturn \"Error fetching standings\"\n\t}\n\n\tfor _, i := range l.Standings[0].Table {\n\t\tif i.Position <= widget.settings.standingCount {\n\t\t\trow := []string{strconv.Itoa(i.Position), i.Team.Name, strconv.Itoa(i.PlayedGames), strconv.Itoa(i.Won), strconv.Itoa(i.Draw), strconv.Itoa(i.Lost), strconv.Itoa(i.GoalDifference), strconv.Itoa(i.Points)}\n\t\t\ttStandings.Append(row)\n\t\t}\n\t}\n\n\ttStandings.Render()\n\tcontent += buf.String()\n\n\treturn content\n}\n\n// GetMatches of particular league\nfunc (widget *Widget) GetMatches(leagueId int) string {\n\n\tvar l LeagueFixtuers\n\tvar content string\n\tscheduledBuf := new(bytes.Buffer)\n\tplayedBuf := new(bytes.Buffer)\n\n\ttScheduled := createTable([]string{}, scheduledBuf)\n\ttPlayed := createTable([]string{}, playedBuf)\n\n\tfrom := getDateString(-widget.settings.matchesFrom)\n\tto := getDateString(widget.settings.matchesTo)\n\n\trequestPath := fmt.Sprintf(\"matches?dateFrom=%s&dateTo=%s\", from, to)\n\tresp, err := widget.footballRequest(requestPath, leagueId)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"Error fetching matches: %s\", err.Error())\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\tdata, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"Error fetching matches: %s\", err.Error())\n\t}\n\terr = json.Unmarshal(data, &l)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"Error fetching matches: %s\", err.Error())\n\t}\n\n\tif len(l.Matches) == 0 {\n\t\treturn \"Error fetching matches\"\n\t}\n\n\tfor _, m := range l.Matches {\n\n\t\twidget.markFavorite(&m)\n\n\t\tswitch m.Status {\n\t\tcase \"SCHEDULED\":\n\t\t\trow := []string{m.HomeTeam.Name, \"🆚\", m.AwayTeam.Name, parseDateString(m.Date)}\n\t\t\ttScheduled.Append(row)\n\t\tcase \"FINISHED\":\n\t\t\trow := []string{m.HomeTeam.Name, strconv.Itoa(m.Score.FullTime.HomeTeam), \"🆚\", m.AwayTeam.Name, strconv.Itoa(m.Score.FullTime.AwayTeam)}\n\t\t\ttPlayed.Append(row)\n\t\t}\n\t}\n\n\ttScheduled.Render()\n\ttPlayed.Render()\n\tif playedBuf.String() != \"\" {\n\t\tcontent += \"\\nMatches Played:\\n\\n\"\n\t\tcontent += playedBuf.String()\n\n\t}\n\tif scheduledBuf.String() != \"\" {\n\t\tcontent += \"\\nUpcoming Matches:\\n\\n\"\n\t\tcontent += scheduledBuf.String()\n\t}\n\n\treturn content\n}\n\nfunc (widget *Widget) markFavorite(m *Matches) {\n\n\tswitch {\n\n\tcase widget.settings.favTeam == \"\":\n\t\treturn\n\tcase strings.Contains(m.AwayTeam.Name, widget.settings.favTeam):\n\t\tm.AwayTeam.Name = fmt.Sprintf(\"%s ⭐\", m.AwayTeam.Name)\n\tcase strings.Contains(m.HomeTeam.Name, widget.settings.favTeam):\n\t\tm.HomeTeam.Name = fmt.Sprintf(\"%s ⭐\", m.HomeTeam.Name)\n\t}\n}\n"
  },
  {
    "path": "modules/gcal/cal_event.go",
    "content": "package gcal\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"google.golang.org/api/calendar/v3\"\n)\n\ntype CalEvent struct {\n\tevent *calendar.Event\n}\n\nfunc NewCalEvent(event *calendar.Event) *CalEvent {\n\tcalEvent := CalEvent{\n\t\tevent: event,\n\t}\n\n\treturn &calEvent\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (calEvent *CalEvent) AllDay() bool {\n\treturn len(calEvent.event.Start.Date) > 0\n}\n\nfunc (calEvent *CalEvent) ConflictsWith(otherEvents []*CalEvent) bool {\n\thasConflict := false\n\n\tfor _, otherEvent := range otherEvents {\n\t\tif calEvent.event == otherEvent.event {\n\t\t\tcontinue\n\t\t}\n\n\t\tif calEvent.Start().Before(otherEvent.End()) && calEvent.End().After(otherEvent.Start()) {\n\t\t\thasConflict = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn hasConflict\n}\n\nfunc (calEvent *CalEvent) Now() bool {\n\treturn time.Now().After(calEvent.Start()) && time.Now().Before(calEvent.End())\n}\n\nfunc (calEvent *CalEvent) Past() bool {\n\tif calEvent.AllDay() {\n\t\t// FIXME: This should calculate properly\n\t\treturn false\n\t}\n\n\treturn !calEvent.Now() && calEvent.Start().Before(time.Now())\n}\n\nfunc (calEvent *CalEvent) ResponseFor(email string) string {\n\tfor _, attendee := range calEvent.event.Attendees {\n\t\tif attendee.Email == email {\n\t\t\treturn attendee.ResponseStatus\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n/* -------------------- DateTimes -------------------- */\n\nfunc (calEvent *CalEvent) End() time.Time {\n\tvar calcTime string\n\tvar end time.Time\n\n\tif calEvent.AllDay() {\n\t\tcalcTime = calEvent.event.End.Date\n\t\tend, _ = time.ParseInLocation(\"2006-01-02\", calcTime, time.Local)\n\t} else {\n\t\tcalcTime = calEvent.event.End.DateTime\n\t\tend, _ = time.Parse(time.RFC3339, calcTime)\n\t}\n\n\treturn end\n}\n\nfunc (calEvent *CalEvent) Start() time.Time {\n\tvar calcTime string\n\tvar start time.Time\n\n\tif calEvent.AllDay() {\n\t\tcalcTime = calEvent.event.Start.Date\n\t\tstart, _ = time.ParseInLocation(\"2006-01-02\", calcTime, time.Local)\n\t} else {\n\t\tcalcTime = calEvent.event.Start.DateTime\n\t\tstart, _ = time.Parse(time.RFC3339, calcTime)\n\t}\n\n\treturn start\n}\n\nfunc (calEvent *CalEvent) Timestamp(hourFormat string, showEndTime bool) string {\n\tif calEvent.AllDay() {\n\t\tstartTime, _ := time.ParseInLocation(\"2006-01-02\", calEvent.event.Start.Date, time.Local)\n\t\treturn startTime.Format(utils.FriendlyDateFormat)\n\t}\n\n\tstartTime, _ := time.Parse(time.RFC3339, calEvent.event.Start.DateTime)\n\tendTime, _ := time.Parse(time.RFC3339, calEvent.event.End.DateTime)\n\n\ttimeFormat := utils.MinimumTimeFormat24\n\tif hourFormat == \"12\" {\n\t\ttimeFormat = utils.MinimumTimeFormat12\n\t}\n\n\tif showEndTime {\n\t\treturn fmt.Sprintf(\"%s-%s\", startTime.Format(timeFormat), endTime.Format(timeFormat))\n\t}\n\n\treturn startTime.Format(timeFormat)\n}\n"
  },
  {
    "path": "modules/gcal/client.go",
    "content": "/*\n* This butt-ugly code is direct from Google itself\n* https://developers.google.com/calendar/quickstart/go\n*\n* With some changes by me to improve things a bit.\n */\n\npackage gcal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n\t\"google.golang.org/api/calendar/v3\"\n\t\"google.golang.org/api/option\"\n)\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Fetch() ([]*CalEvent, error) {\n\tctx := context.Background()\n\n\tsecretPath, _ := utils.ExpandHomeDir(widget.settings.secretFile)\n\n\tb, err := os.ReadFile(filepath.Clean(secretPath))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconfig, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient := getClient(ctx, config, widget.settings.email)\n\n\tsrv, err := calendar.NewService(context.Background(), option.WithHTTPClient(client))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get calendar events\n\tvar events calendar.Events\n\n\tstartTime := fromMidnight().Format(time.RFC3339)\n\teventLimit := int64(widget.settings.eventCount)\n\n\ttimezone := widget.settings.timezone\n\n\tcalendarIDs, err := widget.getCalendarIdList(srv)\n\tfor _, calendarID := range calendarIDs {\n\t\tcalendarEvents, listErr := srv.Events.List(calendarID).TimeZone(timezone).ShowDeleted(false).TimeMin(startTime).MaxResults(eventLimit).SingleEvents(true).OrderBy(\"startTime\").Do()\n\t\tif listErr != nil {\n\t\t\tbreak\n\t\t}\n\t\tevents.Items = append(events.Items, calendarEvents.Items...)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Sort events\n\ttimeDateChooser := func(event *calendar.Event) (time.Time, error) {\n\t\tif len(event.Start.Date) > 0 {\n\t\t\treturn time.Parse(\"2006-01-02\", event.Start.Date)\n\t\t}\n\n\t\treturn time.Parse(time.RFC3339, event.Start.DateTime)\n\t}\n\n\tsort.Slice(events.Items, func(i, j int) bool {\n\t\tdateA, _ := timeDateChooser(events.Items[i])\n\t\tdateB, _ := timeDateChooser(events.Items[j])\n\t\treturn dateA.Before(dateB)\n\t})\n\n\t// Wrap the calendar events in our custom CalEvent\n\tcalEvents := []*CalEvent{}\n\tfor _, event := range events.Items {\n\t\tcalEvents = append(calEvents, NewCalEvent(event))\n\t}\n\n\treturn calEvents, err\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc fromMidnight() time.Time {\n\tnow := time.Now()\n\treturn time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())\n}\n\n// getClient uses a Context and Config to retrieve a Token\n// then generate a Client. It returns the generated Client.\nfunc getClient(ctx context.Context, config *oauth2.Config, name string) *http.Client {\n\tcacheFile, err := tokenCacheFile(name)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to get path to cached credential file. %v\", err)\n\t}\n\ttok, err := tokenFromFile(cacheFile)\n\tif err != nil {\n\t\ttok = getTokenFromWeb(config)\n\t\tsaveToken(cacheFile, tok)\n\t}\n\treturn config.Client(ctx, tok)\n}\n\nfunc isAuthenticated(name string) bool {\n\tcacheFile, err := tokenCacheFile(name)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to get path to cached credential file. %v\", err)\n\t}\n\t_, err = tokenFromFile(cacheFile)\n\treturn err == nil\n}\n\nfunc (widget *Widget) authenticate() {\n\tsecretPath, _ := utils.ExpandHomeDir(filepath.Clean(widget.settings.secretFile))\n\n\tb, err := os.ReadFile(filepath.Clean(secretPath))\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to read secret file. %v\", widget.settings.secretFile)\n\t}\n\n\tconfig, _ := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope)\n\ttok := getTokenFromWeb(config)\n\tcacheFile, _ := tokenCacheFile(widget.settings.email)\n\tsaveToken(cacheFile, tok)\n}\n\n// getTokenFromWeb uses Config to request a Token.\n// It returns the retrieved Token.\nfunc getTokenFromWeb(config *oauth2.Config) *oauth2.Token {\n\tauthURL := config.AuthCodeURL(\"state-token\", oauth2.AccessTypeOffline)\n\tfmt.Printf(\"Go to the following link in your browser then type the \"+\n\t\t\"authorization code: \\n%v (press 'return' before inserting the code)\", authURL)\n\n\tvar code string\n\tif _, err := fmt.Scan(&code); err != nil {\n\t\tlog.Fatalf(\"Unable to read authorization code %v\", err)\n\t}\n\n\ttok, err := config.Exchange(context.Background(), code)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to retrieve token from web %v\", err)\n\t}\n\treturn tok\n}\n\n// tokenCacheFile generates credential file path/filename.\n// It returns the generated credential path/filename.\nfunc tokenCacheFile(name string) (string, error) {\n\tconfigDir, err := cfg.WtfConfigDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\toldFile := configDir + \"/gcal-auth.json\"\n\tnewFileName := fmt.Sprintf(\"%s-gcal-auth.json\", name)\n\tif _, err := os.Stat(oldFile); err == nil {\n\t\trenamedFile := configDir + \"/\" + newFileName\n\t\terr := os.Rename(oldFile, renamedFile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn renamedFile, nil\n\t}\n\treturn cfg.CreateFile(newFileName)\n}\n\n// tokenFromFile retrieves a Token from a given file path.\n// It returns the retrieved Token and any read error encountered.\nfunc tokenFromFile(file string) (*oauth2.Token, error) {\n\tf, err := os.Open(filepath.Clean(file))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tt := &oauth2.Token{}\n\terr = json.NewDecoder(f).Decode(t)\n\tdefer func() { _ = f.Close() }()\n\n\treturn t, err\n}\n\n// saveToken uses a file path to create a file and store the\n// token in it.\nfunc saveToken(file string, token *oauth2.Token) {\n\tfmt.Printf(\"Saving credential file to: %s\\n\", file)\n\tf, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)\n\tif err != nil {\n\t\tlog.Fatalf(\"unable to cache oauth token: %v\", err)\n\t}\n\tdefer func() { _ = f.Close() }()\n\n\terr = json.NewEncoder(f).Encode(token)\n\tif err != nil {\n\t\tlog.Fatalf(\"unable to encode oauth token: %v\", err)\n\t}\n}\n\nfunc (widget *Widget) getCalendarIdList(srv *calendar.Service) ([]string, error) {\n\t// Return single calendar if settings specify we should\n\tif !widget.settings.multiCalendar {\n\t\tid, err := srv.CalendarList.Get(\"primary\").Do()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn []string{id.Id}, nil\n\t}\n\n\t// Get all user calendars with at the least writing access\n\tvar calendarIds []string\n\tvar pageToken string\n\tfor {\n\t\tcalendarList, err := srv.CalendarList.List().ShowHidden(false).MinAccessRole(widget.settings.calendarReadLevel).PageToken(pageToken).Do()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, calendarListItem := range calendarList.Items {\n\t\t\tcalendarIds = append(calendarIds, calendarListItem.Id)\n\t\t}\n\n\t\tpageToken = calendarList.NextPageToken\n\t\tif pageToken == \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn calendarIds, nil\n}\n"
  },
  {
    "path": "modules/gcal/display.go",
    "content": "package gcal\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := widget.settings.Title\n\tcalEvents := widget.calEvents\n\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif len(calEvents) == 0 {\n\t\treturn title, \"No calendar events\", false\n\t}\n\n\tvar str string\n\tvar prevEvent *CalEvent\n\n\tif !widget.settings.showDeclined {\n\t\tcalEvents = widget.removeDeclined(calEvents)\n\t}\n\n\tfor _, calEvent := range calEvents {\n\t\tif calEvent.AllDay() && !widget.settings.showAllDay {\n\t\t\tcontinue\n\t\t}\n\n\t\tts := calEvent.Timestamp(widget.settings.hourFormat, widget.settings.showEndTime)\n\t\ttimestamp := fmt.Sprintf(\"[%s]%s\", widget.eventTimeColor(), ts)\n\t\tif calEvent.AllDay() {\n\t\t\ttimestamp = \"\"\n\t\t}\n\n\t\teventTitle := fmt.Sprintf(\"[%s]%s\",\n\t\t\twidget.titleColor(calEvent),\n\t\t\twidget.eventSummary(calEvent, calEvent.ConflictsWith(calEvents)),\n\t\t)\n\n\t\tlineOne := fmt.Sprintf(\n\t\t\t\"%s %s %s %s[white]\\n\",\n\t\t\twidget.dayDivider(calEvent, prevEvent),\n\t\t\twidget.responseIcon(calEvent),\n\t\t\ttimestamp,\n\t\t\teventTitle,\n\t\t)\n\n\t\tstr += fmt.Sprintf(\"%s   %s%s\\n\",\n\t\t\tlineOne,\n\t\t\twidget.location(calEvent),\n\t\t\twidget.timeUntil(calEvent),\n\t\t)\n\n\t\tif (widget.location(calEvent) != \"\") || (widget.timeUntil(calEvent) != \"\") {\n\t\t\tstr += \"\\n\"\n\t\t}\n\n\t\tprevEvent = calEvent\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) dayDivider(event, prevEvent *CalEvent) string {\n\tvar prevStartTime time.Time\n\n\tif prevEvent != nil {\n\t\tprevStartTime = prevEvent.Start()\n\t}\n\n\t// round times to midnight for comparison\n\ttoMidnight := func(t time.Time) time.Time {\n\t\tt = t.Local()\n\t\treturn time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())\n\t}\n\tprevStartDay := toMidnight(prevStartTime)\n\teventStartDay := toMidnight(event.Start())\n\n\tif !eventStartDay.Equal(prevStartDay) {\n\t\treturn fmt.Sprintf(\"[%s]\",\n\t\t\twidget.settings.day) +\n\t\t\tevent.Start().Format(utils.FullDateFormat) +\n\t\t\t\"\\n\"\n\t}\n\n\treturn \"\"\n}\n\nfunc (widget *Widget) descriptionColor(calEvent *CalEvent) string {\n\tif calEvent.Past() {\n\t\treturn widget.settings.past\n\t}\n\n\treturn widget.settings.description\n}\n\nfunc (widget *Widget) eventTimeColor() string {\n\treturn widget.settings.eventTime\n}\n\nfunc (widget *Widget) eventSummary(calEvent *CalEvent, conflict bool) string {\n\tsummary := calEvent.event.Summary\n\n\tif calEvent.Now() {\n\t\tsummary = fmt.Sprintf(\n\t\t\t\"%s %s\",\n\t\t\twidget.settings.currentIcon,\n\t\t\tsummary,\n\t\t)\n\t}\n\n\tif conflict {\n\t\treturn fmt.Sprintf(\"%s %s\", widget.settings.conflictIcon, summary)\n\t}\n\n\treturn summary\n}\n\n// timeUntil returns the number of hours or days until the event\n// If the event is in the past, returns nil\nfunc (widget *Widget) timeUntil(calEvent *CalEvent) string {\n\tduration := time.Until(calEvent.Start()).Round(time.Minute)\n\n\tif duration < 0 {\n\t\treturn \"\"\n\t}\n\n\tdays := duration / (24 * time.Hour)\n\tduration -= days * (24 * time.Hour)\n\n\thours := duration / time.Hour\n\tduration -= hours * time.Hour\n\n\tmins := duration / time.Minute\n\n\tuntilStr := \"\"\n\n\tcolor := \"[lightblue]\"\n\tswitch {\n\tcase days > 0:\n\t\tuntilStr = fmt.Sprintf(\"%dd\", days)\n\tcase hours > 0:\n\t\tuntilStr = fmt.Sprintf(\"%dh\", hours)\n\tdefault:\n\t\tuntilStr = fmt.Sprintf(\"%dm\", mins)\n\t\tif mins < 30 {\n\t\t\tcolor = \"[red]\"\n\t\t}\n\t}\n\n\treturn color + untilStr + \"[white]\"\n}\n\nfunc (widget *Widget) titleColor(calEvent *CalEvent) string {\n\tcolor := widget.settings.title\n\n\tfor _, untypedArr := range widget.settings.highlights {\n\t\thighlightElements := utils.ToStrs(untypedArr.([]interface{}))\n\n\t\tmatch, _ := regexp.MatchString(\n\t\t\tstrings.ToLower(highlightElements[0]),\n\t\t\tstrings.ToLower(calEvent.event.Summary),\n\t\t)\n\n\t\tif match {\n\t\t\tcolor = highlightElements[1]\n\t\t}\n\t}\n\n\tif calEvent.Past() {\n\t\tcolor = widget.settings.past\n\t}\n\n\treturn color\n}\n\nfunc (widget *Widget) location(calEvent *CalEvent) string {\n\tif !widget.settings.withLocation {\n\t\treturn \"\"\n\t}\n\n\tif calEvent.event.Location == \"\" {\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprintf(\n\t\t\"[%s]%s \",\n\t\twidget.descriptionColor(calEvent),\n\t\tcalEvent.event.Location,\n\t)\n}\n\nfunc (widget *Widget) responseIcon(calEvent *CalEvent) string {\n\tif !widget.settings.displayResponseStatus {\n\t\treturn \"\"\n\t}\n\n\ticon := \"[gray]\"\n\n\tswitch calEvent.ResponseFor(widget.settings.email) {\n\tcase \"accepted\":\n\t\treturn icon + \"✔\"\n\tcase \"declined\":\n\t\treturn icon + \"✘\"\n\tcase \"needsAction\":\n\t\treturn icon + \"?\"\n\tcase \"tentative\":\n\t\treturn icon + \"~\"\n\tdefault:\n\t\treturn icon + \" \"\n\t}\n}\n\nfunc (widget *Widget) removeDeclined(events []*CalEvent) []*CalEvent {\n\tvar ret []*CalEvent\n\tfor _, e := range events {\n\t\tif e.ResponseFor(widget.settings.email) != \"declined\" {\n\t\t\tret = append(ret, e)\n\t\t}\n\t}\n\treturn ret\n}\n"
  },
  {
    "path": "modules/gcal/display_test.go",
    "content": "package gcal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"google.golang.org/api/calendar/v3\"\n)\n\nfunc Test_display_content(t *testing.T) {\n\tstartTime := &calendar.EventDateTime{DateTime: \"1986-04-19T01:00:00.00Z\"}\n\tendTime := &calendar.EventDateTime{DateTime: \"1986-04-19T02:00:00.00Z\"}\n\tevent := &calendar.Event{Summary: \"Foo\", Start: startTime, End: endTime}\n\n\ttestCases := []struct {\n\t\tdescriptionWanted string\n\t\tevents            []*CalEvent\n\t\tname              string\n\t\tsettings          *Settings\n\t}{\n\t\t{\n\t\t\tname:              \"Event content without any events\",\n\t\t\tsettings:          &Settings{Common: &cfg.Common{}},\n\t\t\tevents:            nil,\n\t\t\tdescriptionWanted: \"No calendar events\",\n\t\t},\n\t\t{\n\t\t\tname:              \"Event content with a single event, without end times displayed\",\n\t\t\tsettings:          &Settings{Common: &cfg.Common{}, showEndTime: false},\n\t\t\tevents:            []*CalEvent{NewCalEvent(event)},\n\t\t\tdescriptionWanted: \"[]Saturday, Apr 19\\n  []01:00 []Foo[white]\\n   \\n\",\n\t\t},\n\t\t{\n\t\t\tname:              \"Event content with a single event without showEndTime explicitly set in settings\",\n\t\t\tsettings:          &Settings{Common: &cfg.Common{}},\n\t\t\tevents:            []*CalEvent{NewCalEvent(event)},\n\t\t\tdescriptionWanted: \"[]Saturday, Apr 19\\n  []01:00 []Foo[white]\\n   \\n\",\n\t\t},\n\t\t{\n\t\t\tname:              \"Event content with a single event with end times displayed\",\n\t\t\tsettings:          &Settings{Common: &cfg.Common{}, showEndTime: true},\n\t\t\tevents:            []*CalEvent{NewCalEvent(event)},\n\t\t\tdescriptionWanted: \"[]Saturday, Apr 19\\n  []01:00-02:00 []Foo[white]\\n   \\n\",\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := &Widget{calEvents: tt.events, settings: tt.settings, err: nil}\n\t\t\t_, description, err := w.content()\n\n\t\t\tassert.Equal(t, false, err, tt.name)\n\t\t\tassert.Equal(t, tt.descriptionWanted, description, tt.name)\n\t\t})\n\t}\n\n}\n"
  },
  {
    "path": "modules/gcal/settings.go",
    "content": "package gcal\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Calendar\"\n)\n\ntype colors struct {\n\tday         string\n\tdescription string `help:\"The default color for calendar event descriptions.\" values:\"Any X11 color name.\" optional:\"true\"`\n\teventTime   string `help:\"The default color for calendar event times.\" values:\"Any X11 color name.\" optional:\"true\"`\n\tpast        string `help:\"The color for calendar events that have passed.\" values:\"Any X11 color name.\" optional:\"true\"`\n\ttitle       string `help:\"The default colour for calendar event titles.\" values:\"Any X11 color name.\" optional:\"true\"`\n\n\thighlights []interface{} `help:\"A list of arrays that define a regular expression pattern and a color. If a calendar event title matches a regular expression, the title will be drawn in that colour. Over-rides the default title colour.\" values:\"An array of a valid regular expression, any X11 color name.\" optional:\"true\"`\n}\n\n// Settings defines the configuration options for this module\ntype Settings struct {\n\tcolors\n\t*cfg.Common\n\n\tconflictIcon          string `help:\"The icon displayed beside calendar events that have conflicting times (they intersect or overlap in some way).\" values:\"Any displayable unicode character.\" optional:\"true\"`\n\tcurrentIcon           string `help:\"The icon displayed beside the current calendar event.\" values:\"Any displayable unicode character.\" optional:\"true\"`\n\tdisplayResponseStatus bool   `help:\"Whether or not to display your response status to the calendar event.\" values:\"true or false\" optional:\"true\"`\n\temail                 string `help:\"The email address associated with your Google account. Necessary for determining 'responseStatus'.\" values:\"A valid email address string.\"`\n\teventCount            int    `help:\"The number of calendar events to display.\" values:\"A positive integer, 0..n.\" optional:\"true\"`\n\thourFormat            string `help:\"The format of the clock.\" values:\"12 or 24\"`\n\tmultiCalendar         bool   `help:\"Whether or not to display your primary calendar or all calendars you have access to.\" values:\"true or false\" optional:\"true\"`\n\tsecretFile            string `help:\"Your Google client secret JSON file.\" values:\"A string representing a file path to the JSON secret file.\"`\n\tshowAllDay            bool   `help:\"Whether or not to display all-day events\" values:\"true or false\" optional:\"true\" default:\"true\"`\n\tshowDeclined          bool   `help:\"Whether or not to display events you’ve declined to attend.\" values:\"true or false\" optional:\"true\"`\n\tshowEndTime           bool   `help:\"Display the end time of events, in addition to start time.\" values:\"true or false\" optional:\"true\" default:\"false\"`\n\twithLocation          bool   `help:\"Whether or not to show the location of the appointment.\" values:\"true or false\"`\n\ttimezone              string `help:\"The time zone used to display calendar event times.\" values:\"A valid TZ database time zone string\" optional:\"true\"`\n\tcalendarReadLevel     string `help:\"The calender read level specifies level you want to read events. Default: writer \" values:\"reader, writer\" optional:\"true\"`\n}\n\n// NewSettingsFromYAML creates and returns an instance of Settings with configuration options populated\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tconflictIcon:          ymlConfig.UString(\"conflictIcon\", \"🚨\"),\n\t\tcurrentIcon:           ymlConfig.UString(\"currentIcon\", \"🔸\"),\n\t\tdisplayResponseStatus: ymlConfig.UBool(\"displayResponseStatus\", true),\n\t\temail:                 ymlConfig.UString(\"email\", \"\"),\n\t\teventCount:            ymlConfig.UInt(\"eventCount\", 10),\n\t\thourFormat:            ymlConfig.UString(\"hourFormat\", \"24\"),\n\t\tmultiCalendar:         ymlConfig.UBool(\"multiCalendar\", false),\n\t\tsecretFile:            ymlConfig.UString(\"secretFile\", \"\"),\n\t\tshowAllDay:            ymlConfig.UBool(\"showAllDay\", true),\n\t\tshowEndTime:           ymlConfig.UBool(\"showEndTime\", false),\n\t\tshowDeclined:          ymlConfig.UBool(\"showDeclined\", false),\n\t\twithLocation:          ymlConfig.UBool(\"withLocation\", true),\n\t\ttimezone:              ymlConfig.UString(\"timezone\", \"\"),\n\t\tcalendarReadLevel:     ymlConfig.UString(\"calendarReadLevel\", \"writer\"),\n\t}\n\n\tsettings.day = ymlConfig.UString(\"colors.day\", settings.Colors.Subheading)\n\tsettings.description = ymlConfig.UString(\"colors.description\", \"white\")\n\n\t// settings.colors.eventTime is a new feature introduced via issue #638. Prior to this, the color of the event\n\t// time was (unintentionally) customized via settings.colors.description. To maintain backwards compatibility\n\t// for users who might be already using this to set the color of the event time, we try to determine the default\n\t// from settings.colors.description. If it is not set, then the default value of \"white\" is used.  Finally, if a\n\t// user sets a value for colors.eventTime, it overrides the defaults.\n\t//\n\t// PS: We should have a deprecation plan for supporting this backwards compatibility feature.\n\tsettings.eventTime = ymlConfig.UString(\"colors.eventTime\", settings.description)\n\n\tsettings.highlights = ymlConfig.UList(\"colors.highlights\")\n\tsettings.past = ymlConfig.UString(\"colors.past\", \"gray\")\n\tsettings.title = ymlConfig.UString(\"colors.title\", \"white\")\n\n\tsettings.SetDocumentationPath(\"google/gcal\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/gcal/widget.go",
    "content": "package gcal\n\nimport (\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tcalEvents []*CalEvent\n\terr       error\n\tsettings  *Settings\n\ttviewApp  *tview.Application\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\ttviewApp: tviewApp,\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Disable() {\n\twidget.TextWidget.Disable()\n}\n\nfunc (widget *Widget) Refresh() {\n\tif isAuthenticated(widget.settings.email) {\n\t\twidget.fetchAndDisplayEvents()\n\t\treturn\n\t}\n\n\twidget.tviewApp.Suspend(widget.authenticate)\n\twidget.Refresh()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) fetchAndDisplayEvents() {\n\tcalEvents, err := widget.Fetch()\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.calEvents = []*CalEvent{}\n\t} else {\n\t\twidget.err = nil\n\t\twidget.calEvents = calEvents\n\t}\n\n\twidget.display()\n}\n"
  },
  {
    "path": "modules/gerrit/display.go",
    "content": "package gerrit\n\nimport (\n\t\"fmt\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tproject := widget.currentGerritProject()\n\tif project == nil {\n\t\treturn title, \"Gerrit project data is unavailable\", true\n\t}\n\n\ttitle = fmt.Sprintf(\"%s- %s\", widget.CommonSettings().Title, widget.title(project))\n\n\t_, _, width, _ := widget.View.GetRect()\n\tstr := widget.settings.PaginationMarker(len(widget.GerritProjects), widget.Idx, width) + \"\\n\"\n\tstr += fmt.Sprintf(\" [%s]Stats[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += widget.displayStats(project)\n\tstr += \"\\n\"\n\tstr += fmt.Sprintf(\" [%s]Open Incoming Reviews[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += widget.displayMyIncomingReviews(project)\n\tstr += \"\\n\"\n\tstr += fmt.Sprintf(\" [%s]My Outgoing Reviews[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += widget.displayMyOutgoingReviews(project)\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) displayMyIncomingReviews(project *GerritProject) string {\n\tif len(project.IncomingReviews) == 0 {\n\t\treturn \" [grey]none[white]\\n\"\n\t}\n\n\tstr := \"\"\n\tfor idx, r := range project.IncomingReviews {\n\t\tstr += fmt.Sprintf(\" [%s] [green]%d[white] [%s] %s\\n\", widget.rowColor(idx), r.Number, widget.rowColor(idx), r.Subject)\n\t}\n\n\treturn str\n}\n\nfunc (widget *Widget) displayMyOutgoingReviews(project *GerritProject) string {\n\tif len(project.OutgoingReviews) == 0 {\n\t\treturn \" [grey]none[white]\\n\"\n\t}\n\n\tstr := \"\"\n\tfor idx, r := range project.OutgoingReviews {\n\t\tstr += fmt.Sprintf(\" [%s] [green]%d[white] [%s] %s\\n\", widget.rowColor(idx+len(project.IncomingReviews)), r.Number, widget.rowColor(idx+len(project.IncomingReviews)), r.Subject)\n\t}\n\n\treturn str\n}\n\nfunc (widget *Widget) displayStats(project *GerritProject) string {\n\tstr := fmt.Sprintf(\n\t\t\" Reviews: %d\\n\",\n\t\tproject.ReviewCount,\n\t)\n\n\treturn str\n}\n\nfunc (widget *Widget) rowColor(idx int) string {\n\tif widget.View.HasFocus() && (idx == widget.selected) {\n\t\treturn widget.settings.DefaultFocusedRowColor()\n\t}\n\n\treturn widget.settings.RowColor(idx)\n}\n\nfunc (widget *Widget) title(project *GerritProject) string {\n\treturn fmt.Sprintf(\"[green]%s [white]\", project.Path)\n}\n"
  },
  {
    "path": "modules/gerrit/gerrit_repo.go",
    "content": "package gerrit\n\nimport (\n\t\"context\"\n\n\tglb \"github.com/andygrunwald/go-gerrit\"\n)\n\ntype GerritProject struct {\n\tgerrit *glb.Client\n\tPath   string\n\n\tChanges         *[]glb.ChangeInfo\n\tReviewCount     int\n\tIncomingReviews []glb.ChangeInfo\n\tOutgoingReviews []glb.ChangeInfo\n}\n\nfunc NewGerritProject(path string, gerrit *glb.Client) *GerritProject {\n\tproject := GerritProject{\n\t\tgerrit: gerrit,\n\t\tPath:   path,\n\t}\n\n\treturn &project\n}\n\n// Refresh reloads the gerrit data via the Gerrit API\nfunc (project *GerritProject) Refresh(username string) {\n\tproject.Changes, _ = project.loadChanges()\n\n\tproject.ReviewCount = project.countReviews(project.Changes)\n\tproject.IncomingReviews = project.myIncomingReviews(project.Changes, username)\n\tproject.OutgoingReviews = project.myOutgoingReviews(project.Changes, username)\n\n}\n\n/* -------------------- Counts -------------------- */\n\nfunc (project *GerritProject) countReviews(changes *[]glb.ChangeInfo) int {\n\tif changes == nil {\n\t\treturn 0\n\t}\n\n\treturn len(*changes)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// myOutgoingReviews returns a list of my outgoing reviews created by username on this project\nfunc (project *GerritProject) myOutgoingReviews(changes *[]glb.ChangeInfo, username string) []glb.ChangeInfo {\n\tvar ors []glb.ChangeInfo\n\n\tif changes == nil {\n\t\treturn ors\n\t}\n\n\tfor _, change := range *changes {\n\t\tuser := change.Owner\n\n\t\tif user.Username == username {\n\t\t\tors = append(ors, change)\n\t\t}\n\t}\n\n\treturn ors\n}\n\n// myIncomingReviews returns a list of merge requests for which username has been requested to ChangeInfo\nfunc (project *GerritProject) myIncomingReviews(changes *[]glb.ChangeInfo, username string) []glb.ChangeInfo {\n\tvar irs []glb.ChangeInfo\n\n\tif changes == nil {\n\t\treturn irs\n\t}\n\n\tfor _, change := range *changes {\n\t\treviewers := change.Reviewers\n\n\t\tfor _, reviewer := range reviewers[\"REVIEWER\"] {\n\t\t\tif reviewer.Username == username {\n\t\t\t\tirs = append(irs, change)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn irs\n}\n\nfunc (project *GerritProject) loadChanges() (*[]glb.ChangeInfo, error) {\n\topt := &glb.QueryChangeOptions{}\n\topt.Query = []string{\"(projects:\" + project.Path + \"+ is:open + owner:self) \" + \" OR \" +\n\t\t\"(projects:\" + project.Path + \" + is:open + ((reviewer:self + -owner:self + -star:ignore) + OR + assignee:self))\"}\n\topt.AdditionalFields = []string{\"DETAILED_LABELS\", \"DETAILED_ACCOUNTS\"}\n\tchanges, _, err := project.gerrit.Changes.QueryChanges(context.Background(), opt)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn changes, err\n}\n"
  },
  {
    "path": "modules/gerrit/keyboard.go",
    "content": "package gerrit\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"h\", widget.prevProject, \"Select previous project\")\n\twidget.SetKeyboardChar(\"l\", widget.nextProject, \"Select next project\")\n\twidget.SetKeyboardChar(\"j\", widget.nextReview, \"Select next review\")\n\twidget.SetKeyboardChar(\"k\", widget.prevReview, \"Select previous review\")\n\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.prevProject, \"Select previous project\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.nextProject, \"Select next project\")\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.nextReview, \"Select next review\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.prevReview, \"Select previous review\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.unselect, \"Clear selection\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openReview, \"Open review in browser\")\n}\n"
  },
  {
    "path": "modules/gerrit/settings.go",
    "content": "package gerrit\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Gerrit\"\n)\n\ntype colors struct {\n\trows struct {\n\t\teven string `help:\"Define the foreground color for even-numbered rows.\" values:\"Any X11 color name.\" optional:\"true\"`\n\t\todd  string `help:\"Define the foreground color for odd-numbered rows.\" values:\"Any X11 color name.\" optional:\"true\"`\n\t}\n}\n\ntype Settings struct {\n\tcolors\n\t*cfg.Common\n\n\tdomain                  string        `help:\"Your Gerrit corporate domain.\"`\n\tpassword                string        `help:\"Your Gerrit HTTP Password.\"`\n\tprojects                []interface{} `help:\"A list of Gerrit project names to fetch data for.\"`\n\tusername                string        `help:\"Your Gerrit username.\"`\n\tverifyServerCertificate bool          `help:\"Determines whether or not the server’s certificate chain and host name are verified.\" values:\"true or false\" optional:\"true\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tdomain:                  ymlConfig.UString(\"domain\", \"\"),\n\t\tpassword:                ymlConfig.UString(\"password\", os.Getenv(\"WTF_GERRIT_PASSWORD\")),\n\t\tprojects:                ymlConfig.UList(\"projects\"),\n\t\tusername:                ymlConfig.UString(\"username\", \"\"),\n\t\tverifyServerCertificate: ymlConfig.UBool(\"verifyServerCertificate\", true),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.password).\n\t\tService(settings.domain).Load()\n\n\tsettings.rows.even = ymlConfig.UString(\"colors.rows.even\", \"white\")\n\tsettings.rows.odd = ymlConfig.UString(\"colors.rows.odd\", \"blue\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/gerrit/widget.go",
    "content": "package gerrit\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\n\tglb \"github.com/andygrunwald/go-gerrit\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tgerrit *glb.Client\n\n\tGerritProjects []*GerritProject\n\tIdx            int\n\n\tselected int\n\tsettings *Settings\n\terr      error\n}\n\nvar (\n\tGerritURLPattern = regexp.MustCompile(`^(http|https)://(.*)$`)\n)\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, _ *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tIdx: 0,\n\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\n\twidget.unselect()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\thttpClient := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tInsecureSkipVerify: !widget.settings.verifyServerCertificate,\n\t\t\t},\n\t\t\tProxy: http.ProxyFromEnvironment,\n\t\t},\n\t}\n\n\tgerritUrl := widget.settings.domain\n\tsubmatches := GerritURLPattern.FindAllStringSubmatch(widget.settings.domain, -1)\n\n\tif len(submatches) > 0 && len(submatches[0]) > 2 {\n\t\tsubmatch := submatches[0]\n\t\tgerritUrl = fmt.Sprintf(\n\t\t\t\"%s://%s:%s@%s\",\n\t\t\tsubmatch[1],\n\t\t\twidget.settings.username,\n\t\t\twidget.settings.password,\n\t\t\tsubmatch[2],\n\t\t)\n\t}\n\tgerrit, err := glb.NewClient(context.Background(), gerritUrl, httpClient)\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.gerrit = nil\n\t\twidget.GerritProjects = nil\n\t} else {\n\t\twidget.err = nil\n\t\twidget.gerrit = gerrit\n\t\twidget.GerritProjects = widget.buildProjectCollection(widget.settings.projects)\n\t\tfor _, project := range widget.GerritProjects {\n\t\t\tproject.Refresh(widget.settings.username)\n\t\t}\n\t}\n\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) nextProject() {\n\twidget.Idx++\n\twidget.unselect()\n\tif widget.Idx == len(widget.GerritProjects) {\n\t\twidget.Idx = 0\n\t}\n\n\twidget.unselect()\n}\n\nfunc (widget *Widget) prevProject() {\n\twidget.Idx--\n\tif widget.Idx < 0 {\n\t\twidget.Idx = len(widget.GerritProjects) - 1\n\t}\n\n\twidget.unselect()\n}\n\nfunc (widget *Widget) nextReview() {\n\twidget.selected++\n\tproject := widget.GerritProjects[widget.Idx]\n\tif widget.selected >= project.ReviewCount {\n\t\twidget.selected = 0\n\t}\n\n\twidget.display()\n}\n\nfunc (widget *Widget) prevReview() {\n\twidget.selected--\n\tproject := widget.GerritProjects[widget.Idx]\n\tif widget.selected < 0 {\n\t\twidget.selected = project.ReviewCount - 1\n\t}\n\n\twidget.display()\n}\n\nfunc (widget *Widget) openReview() {\n\tsel := widget.selected\n\tproject := widget.GerritProjects[widget.Idx]\n\tif sel >= 0 && sel < project.ReviewCount {\n\t\tchange := glb.ChangeInfo{}\n\t\tif sel < len(project.IncomingReviews) {\n\t\t\tchange = project.IncomingReviews[sel]\n\t\t} else {\n\t\t\tchange = project.OutgoingReviews[sel-len(project.IncomingReviews)]\n\t\t}\n\t\tutils.OpenFile(fmt.Sprintf(\"%s/%s/%d\", widget.settings.domain, \"#/c\", change.Number))\n\t}\n}\n\nfunc (widget *Widget) unselect() {\n\twidget.selected = -1\n\twidget.display()\n}\n\nfunc (widget *Widget) buildProjectCollection(projectData []interface{}) []*GerritProject {\n\tgerritProjects := []*GerritProject{}\n\n\tfor _, name := range projectData {\n\t\tproject := NewGerritProject(name.(string), widget.gerrit)\n\t\tgerritProjects = append(gerritProjects, project)\n\t}\n\n\treturn gerritProjects\n}\n\nfunc (widget *Widget) currentGerritProject() *GerritProject {\n\tif len(widget.GerritProjects) == 0 {\n\t\treturn nil\n\t}\n\n\tif widget.Idx < 0 || widget.Idx >= len(widget.GerritProjects) {\n\t\treturn nil\n\t}\n\n\treturn widget.GerritProjects[widget.Idx]\n}\n"
  },
  {
    "path": "modules/git/display.go",
    "content": "package git\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\trepoData := widget.currentData()\n\tif repoData == nil {\n\t\treturn widget.CommonSettings().Title, \" Git repo data is unavailable \", false\n\t}\n\n\twidgetTitle := \"\"\n\tif widget.settings.lastFolderTitle {\n\t\tpathParts := strings.Split(repoData.Repository, \"/\")\n\t\twidgetTitle += pathParts[len(pathParts)-1]\n\n\t} else {\n\t\twidgetTitle = repoData.Repository\n\t}\n\tif widget.settings.branchInTitle {\n\t\twidgetTitle += fmt.Sprintf(\" <%s>\", repoData.Branch)\n\t}\n\ttitle := \"\"\n\tif widget.settings.showModuleName {\n\t\ttitle = fmt.Sprintf(\n\t\t\t\"%s - %s[white]\",\n\t\t\twidget.CommonSettings().Title,\n\t\t\twidgetTitle,\n\t\t)\n\t} else {\n\t\ttitle = fmt.Sprintf(\n\t\t\t\"%s[white]\",\n\t\t\twidgetTitle,\n\t\t)\n\t}\n\n\t_, _, width, _ := widget.View.GetRect()\n\tstr := widget.settings.PaginationMarker(len(widget.GitRepos), widget.Idx, width) + \"\\n\"\n\tfor _, v := range widget.settings.sections {\n\t\tif v == \"branch\" {\n\t\t\tstr += fmt.Sprintf(\" [%s]Branch[white]\\n\", widget.settings.Colors.Subheading)\n\t\t\tstr += fmt.Sprintf(\" %s\", repoData.Branch)\n\t\t} else if v == \"files\" && (widget.settings.showFilesIfEmpty || len(repoData.ChangedFiles) > 1) {\n\t\t\tstr += widget.formatChanges(repoData.ChangedFiles)\n\t\t} else if v == \"commits\" {\n\t\t\tstr += widget.formatCommits(repoData.Commits)\n\t\t}\n\t\tstr += \"\\n\"\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) formatChanges(data []string) string {\n\tstr := fmt.Sprintf(\" [%s]Changed Files[white]\\n\", widget.settings.Colors.Subheading)\n\n\tif len(data) == 1 {\n\t\tstr += \" [grey]none[white]\\n\"\n\t} else {\n\t\tfor _, line := range data {\n\t\t\tstr += widget.formatChange(line)\n\t\t}\n\t}\n\n\treturn str\n}\n\nfunc (widget *Widget) formatChange(line string) string {\n\tif line == \"\" {\n\t\treturn \"\"\n\t}\n\n\tline = strings.TrimSpace(line)\n\tfirstChar, _ := utf8.DecodeRuneInString(line)\n\n\t// Revisit this and kill the ugly duplication\n\tswitch firstChar {\n\tcase 'A':\n\t\tline = strings.Replace(line, \"A\", \"[green]A[white]\", 1)\n\tcase 'D':\n\t\tline = strings.Replace(line, \"D\", \"[red]D[white]\", 1)\n\tcase 'M':\n\t\tline = strings.Replace(line, \"M\", \"[yellow]M[white]\", 1)\n\tcase 'R':\n\t\tline = strings.Replace(line, \"R\", \"[purple]R[white]\", 1)\n\t}\n\n\treturn fmt.Sprintf(\" %s\\n\", strings.ReplaceAll(line, \"\\\"\", \"\"))\n}\n\nfunc (widget *Widget) formatCommits(data []string) string {\n\tstr := fmt.Sprintf(\" [%s]Recent Commits[white]\\n\", widget.settings.Colors.Subheading)\n\n\tfor _, line := range data {\n\t\tstr += widget.formatCommit(line)\n\t}\n\n\treturn str\n}\n\nfunc (widget *Widget) formatCommit(line string) string {\n\treturn fmt.Sprintf(\" %s\\n\", strings.ReplaceAll(line, \"\\\"\", \"\"))\n}\n"
  },
  {
    "path": "modules/git/git_repo.go",
    "content": "package git\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\ntype GitRepo struct {\n\tBranch       string\n\tChangedFiles []string\n\tCommits      []string\n\tRepository   string\n\tPath         string\n}\n\nfunc NewGitRepo(repoPath string, commitCount int, commitFormat, dateFormat string) *GitRepo {\n\trepo := GitRepo{Path: repoPath}\n\n\trepo.Branch = repo.branch()\n\trepo.ChangedFiles = repo.changedFiles()\n\trepo.Commits = repo.commits(commitCount, commitFormat, dateFormat)\n\trepo.Repository = strings.TrimSpace(repo.repository())\n\n\treturn &repo\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (repo *GitRepo) branch() string {\n\targ := []string{repo.gitDir(), repo.workTree(), \"rev-parse\", \"--abbrev-ref\", \"HEAD\"}\n\n\tcmd := exec.Command(__go_cmd, arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\n\treturn str\n}\n\nfunc (repo *GitRepo) changedFiles() []string {\n\targ := []string{repo.gitDir(), repo.workTree(), \"status\", \"--porcelain\"}\n\n\tcmd := exec.Command(__go_cmd, arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\n\tdata := strings.Split(str, \"\\n\")\n\n\treturn data\n}\n\nfunc (repo *GitRepo) commits(commitCount int, commitFormat, dateFormat string) []string {\n\tdateStr := fmt.Sprintf(\"--date=format:\\\"%s\\\"\", dateFormat)\n\tnumStr := fmt.Sprintf(\"-n %d\", commitCount)\n\tcommitStr := fmt.Sprintf(\"--pretty=format:\\\"%s\\\"\", commitFormat)\n\n\targ := []string{repo.gitDir(), repo.workTree(), \"log\", dateStr, numStr, commitStr}\n\n\tcmd := exec.Command(__go_cmd, arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\n\tdata := strings.Split(str, \"\\n\")\n\n\treturn data\n}\n\nfunc (repo *GitRepo) repository() string {\n\targ := []string{repo.gitDir(), repo.workTree(), \"rev-parse\", \"--show-toplevel\"}\n\tcmd := exec.Command(__go_cmd, arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\n\treturn str\n}\nfunc (repo *GitRepo) pull() string {\n\targ := []string{repo.gitDir(), repo.workTree(), \"pull\"}\n\tcmd := exec.Command(__go_cmd, arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\treturn str\n}\n\nfunc (repo *GitRepo) checkout(branch string) string {\n\targ := []string{repo.gitDir(), repo.workTree(), \"checkout\", branch}\n\tcmd := exec.Command(__go_cmd, arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\treturn str\n}\n\nfunc (repo *GitRepo) gitDir() string {\n\treturn fmt.Sprintf(\"--git-dir=%s/.git\", repo.Path)\n}\n\nfunc (repo *GitRepo) workTree() string {\n\treturn fmt.Sprintf(\"--work-tree=%s\", repo.Path)\n}\n"
  },
  {
    "path": "modules/git/keyboard.go",
    "content": "package git\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardChar(\"p\", widget.Pull, \"Pull repo\")\n\twidget.SetKeyboardChar(\"c\", widget.Checkout, \"Checkout branch\")\n\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next source\")\n}\n"
  },
  {
    "path": "modules/git/settings.go",
    "content": "package git\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Git\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcommitCount      int           `help:\"The number of past commits to display.\" values:\"A positive integer, 0..n.\" optional:\"true\"`\n\tsections         []interface{} `help:\"Sections to show\" optional:\"true\"`\n\tshowModuleName   bool          `help:\"Whether to show 'Git - ' before information in title\" optional:\"true\" default:\"true\"`\n\tbranchInTitle    bool          `help:\"Whether to show branch name in title instead of the widget body itself\" optional:\"true\" default:\"false\"`\n\tshowFilesIfEmpty bool          `help:\"Whether to show Changed Files section if no changed files\" optional:\"true\" default:\"true\"`\n\tlastFolderTitle  bool          `help:\"Whether to show only last part of directory path instead of full path\" optional:\"true\" default:\"false\"`\n\tcommitFormat     string        `help:\"The string format for the commit message.\" optional:\"true\"`\n\tdateFormat       string        `help:\"The string format for the date/time in the commit message.\" optional:\"true\"`\n\trepositories     []interface{} `help:\"Defines which git repositories to watch.\" values:\"A list of zero or more local file paths pointing to valid git repositories.\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tcommitCount:      ymlConfig.UInt(\"commitCount\", 10),\n\t\tsections:         ymlConfig.UList(\"sections\"),\n\t\tshowModuleName:   ymlConfig.UBool(\"showModuleName\", true),\n\t\tbranchInTitle:    ymlConfig.UBool(\"branchInTitle\", false),\n\t\tshowFilesIfEmpty: ymlConfig.UBool(\"showFilesIfEmpty\", true),\n\t\tlastFolderTitle:  ymlConfig.UBool(\"lastFolderTitle\", false),\n\t\tcommitFormat:     ymlConfig.UString(\"commitFormat\", \"[forestgreen]%h [white]%s [grey]%an on %cd[white]\"),\n\t\tdateFormat:       ymlConfig.UString(\"dateFormat\", \"%b %d, %Y\"),\n\t\trepositories:     ymlConfig.UList(\"repositories\"),\n\t}\n\tif len(settings.sections) == 0 {\n\t\tfor _, v := range []string{\"branch\", \"files\", \"commits\"} {\n\t\t\tsettings.sections = append(settings.sections, v)\n\t\t}\n\t}\n\n\treturn &settings\n}\n\nfunc (widget *Widget) ConfigText() string {\n\treturn utils.HelpFromInterface(Settings{})\n}\n"
  },
  {
    "path": "modules/git/variables.go",
    "content": "//go:build !windows\n\npackage git\n\nconst (\n\t__go_cmd = \"git\"\n)\n"
  },
  {
    "path": "modules/git/variables_win.go",
    "content": "//go:build windows\n\npackage git\n\nconst (\n\t__go_cmd = \"git.exe\"\n)\n"
  },
  {
    "path": "modules/git/widget.go",
    "content": "package git\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst (\n\tmodalHeight = 7\n\tmodalWidth  = 80\n\toffscreen   = -1000\n)\n\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tGitRepos []*GitRepo\n\n\tpages    *tview.Pages\n\tsettings *Settings\n\ttviewApp *tview.Application\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"repository\", \"repositories\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\ttviewApp: tviewApp,\n\t\tpages:    pages,\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\n\twidget.SetDisplayFunction(widget.display)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Checkout() {\n\tform := widget.modalForm(\"Branch to checkout:\", \"\")\n\n\tcheckoutFctn := func() {\n\t\ttext := form.GetFormItem(0).(*tview.InputField).GetText()\n\t\trepoToCheckout := widget.GitRepos[widget.Idx]\n\t\trepoToCheckout.checkout(text)\n\t\twidget.pages.RemovePage(\"modal\")\n\t\twidget.tviewApp.SetFocus(widget.View)\n\t\twidget.display()\n\t\twidget.Refresh()\n\t}\n\n\twidget.addButtons(form, checkoutFctn)\n\twidget.modalFocus(form)\n}\n\nfunc (widget *Widget) Pull() {\n\trepoToPull := widget.GitRepos[widget.Idx]\n\trepoToPull.pull()\n\twidget.Refresh()\n\n}\n\nfunc (widget *Widget) Refresh() {\n\trepoPaths := utils.ToStrs(widget.settings.repositories)\n\n\twidget.GitRepos = widget.gitRepos(repoPaths)\n\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) addCheckoutButton(form *tview.Form, fctn func()) {\n\tform.AddButton(\"Checkout\", fctn)\n}\n\nfunc (widget *Widget) addButtons(form *tview.Form, checkoutFctn func()) {\n\twidget.addCheckoutButton(form, checkoutFctn)\n\twidget.addCancelButton(form)\n}\n\nfunc (widget *Widget) addCancelButton(form *tview.Form) {\n\tcancelFn := func() {\n\t\twidget.pages.RemovePage(\"modal\")\n\t\twidget.tviewApp.SetFocus(widget.View)\n\t\twidget.display()\n\t}\n\n\tform.AddButton(\"Cancel\", cancelFn)\n\tform.SetCancelFunc(cancelFn)\n}\n\nfunc (widget *Widget) modalFocus(form *tview.Form) {\n\tframe := widget.modalFrame(form)\n\twidget.pages.AddPage(\"modal\", frame, false, true)\n\twidget.tviewApp.SetFocus(frame)\n}\n\nfunc (widget *Widget) modalForm(lbl, text string) *tview.Form {\n\tform := tview.NewForm()\n\tform.SetButtonsAlign(tview.AlignCenter)\n\tform.SetButtonTextColor(tview.Styles.PrimaryTextColor)\n\n\tform.AddInputField(lbl, text, 60, nil, nil)\n\n\treturn form\n}\n\nfunc (widget *Widget) modalFrame(form *tview.Form) *tview.Frame {\n\tframe := tview.NewFrame(form)\n\tframe.SetBorders(0, 0, 0, 0, 0, 0)\n\tframe.SetRect(offscreen, offscreen, modalWidth, modalHeight)\n\tframe.SetBorder(true)\n\tframe.SetBorders(1, 1, 0, 0, 1, 1)\n\n\tdrawFunc := func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {\n\t\tw, h := screen.Size()\n\t\tframe.SetRect((w/2)-(width/2), (h/2)-(height/2), width, height)\n\t\treturn x, y, width, height\n\t}\n\n\tframe.SetDrawFunc(drawFunc)\n\n\treturn frame\n}\n\nfunc (widget *Widget) currentData() *GitRepo {\n\tif len(widget.GitRepos) == 0 {\n\t\treturn nil\n\t}\n\n\tif widget.Idx < 0 || widget.Idx >= len(widget.GitRepos) {\n\t\treturn nil\n\t}\n\n\treturn widget.GitRepos[widget.Idx]\n}\n\nfunc (widget *Widget) gitRepos(repoPaths []string) []*GitRepo {\n\trepos := []*GitRepo{}\n\n\tfor _, repoPath := range repoPaths {\n\t\tif strings.HasSuffix(repoPath, string(os.PathSeparator)) {\n\t\t\trepos = append(repos, widget.findGitRepositories(make([]*GitRepo, 0), repoPath)...)\n\n\t\t} else {\n\t\t\trepo := NewGitRepo(\n\t\t\t\trepoPath,\n\t\t\t\twidget.settings.commitCount,\n\t\t\t\twidget.settings.commitFormat,\n\t\t\t\twidget.settings.dateFormat,\n\t\t\t)\n\n\t\t\trepos = append(repos, repo)\n\t\t}\n\t}\n\n\treturn repos\n}\n\nfunc (widget *Widget) findGitRepositories(repositories []*GitRepo, directory string) []*GitRepo {\n\tdirectory = strings.TrimSuffix(directory, string(os.PathSeparator))\n\n\tfiles, err := os.ReadDir(directory)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tvar path string\n\n\tfor _, file := range files {\n\t\tif file.IsDir() {\n\t\t\tpath = directory + string(os.PathSeparator) + file.Name()\n\n\t\t\tif file.Name() == \".git\" {\n\t\t\t\tpath = strings.TrimSuffix(path, string(os.PathSeparator)+\".git\")\n\n\t\t\t\trepo := NewGitRepo(\n\t\t\t\t\tpath,\n\t\t\t\t\twidget.settings.commitCount,\n\t\t\t\t\twidget.settings.commitFormat,\n\t\t\t\t\twidget.settings.dateFormat,\n\t\t\t\t)\n\n\t\t\t\trepositories = append(repositories, repo)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif file.Name() == \"vendor\" || file.Name() == \"node_modules\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trepositories = widget.findGitRepositories(repositories, path)\n\t\t}\n\t}\n\n\treturn repositories\n}\n"
  },
  {
    "path": "modules/github/display.go",
    "content": "package github\n\nimport (\n\t\"fmt\"\n\n\tghb \"github.com/google/go-github/v32/github\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\trepo := widget.currentGithubRepo()\n\tusername := widget.settings.username\n\n\t// Choses the correct place to scroll to when changing sources\n\tif len(widget.View.GetHighlights()) > 0 {\n\t\twidget.View.ScrollToHighlight()\n\t} else {\n\t\twidget.View.ScrollToBeginning()\n\t}\n\n\t// initial maxItems count\n\twidget.Items = make([]int, 0)\n\twidget.SetItemCount(len(repo.myReviewRequests((username))))\n\n\ttitle := fmt.Sprintf(\"%s - %s\", widget.CommonSettings().Title, widget.title(repo))\n\tif repo == nil {\n\t\treturn title, \" GitHub repo data is unavailable \", false\n\t} else if repo.Err != nil {\n\t\treturn title, repo.Err.Error(), true\n\t}\n\n\t_, _, width, _ := widget.View.GetRect()\n\tstr := widget.settings.PaginationMarker(len(widget.GithubRepos), widget.Idx, width)\n\tif widget.settings.showStats {\n\t\tstr += fmt.Sprintf(\"\\n [%s]Stats[white]\\n\", widget.settings.Colors.Subheading)\n\t\tstr += widget.displayStats(repo)\n\t}\n\tif widget.settings.showOpenReviewRequests {\n\t\tstr += fmt.Sprintf(\"\\n [%s]Open Review Requests[white]\\n\", widget.settings.Colors.Subheading)\n\t\tstr += widget.displayMyReviewRequests(repo, username)\n\t}\n\tif widget.settings.showMyPullRequests {\n\t\tstr += fmt.Sprintf(\"\\n [%s]My Pull Requests[white]\\n\", widget.settings.Colors.Subheading)\n\t\tstr += widget.displayMyPullRequests(repo, username)\n\t}\n\tfor _, customQuery := range widget.settings.customQueries {\n\t\tstr += fmt.Sprintf(\"\\n [%s]%s[white]\\n\", widget.settings.Colors.Subheading, customQuery.title)\n\t\tstr += widget.displayCustomQuery(repo, customQuery.filter, customQuery.perPage)\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) displayMyPullRequests(repo *Repo, username string) string {\n\tprs := repo.myPullRequests(username, widget.settings.enableStatus)\n\n\tprLength := len(prs)\n\n\tif prLength == 0 {\n\t\treturn \" [grey]none[white]\\n\"\n\t}\n\n\tmaxItems := widget.GetItemCount()\n\n\tstr := \"\"\n\tfor idx, pr := range prs {\n\t\tstr += fmt.Sprintf(` %s[green][\"%d\"]%4d[\"\"][white] %s`, widget.mergeString(pr), maxItems+idx, *pr.Number, *pr.Title)\n\t\tstr += \"\\n\"\n\t\twidget.Items = append(widget.Items, *pr.Number)\n\t}\n\n\twidget.SetItemCount(maxItems + prLength)\n\n\treturn str\n}\n\nfunc (widget *Widget) displayCustomQuery(repo *Repo, filter string, perPage int) string {\n\tres := repo.customIssueQuery(filter, perPage)\n\n\tif res == nil {\n\t\treturn \" [grey]Invalid Query[white]\\n\"\n\t}\n\n\tissuesLength := len(res.Issues)\n\n\tif issuesLength == 0 {\n\t\treturn \" [grey]none[white]\\n\"\n\t}\n\n\tmaxItems := widget.GetItemCount()\n\n\tstr := \"\"\n\tfor idx, issue := range res.Issues {\n\t\tstr += fmt.Sprintf(` [green][\"%d\"]%4d[\"\"][white] %s`, maxItems+idx, *issue.Number, *issue.Title)\n\t\tstr += \"\\n\"\n\t\twidget.Items = append(widget.Items, *issue.Number)\n\t}\n\n\twidget.SetItemCount(maxItems + issuesLength)\n\n\treturn str\n}\n\nfunc (widget *Widget) displayMyReviewRequests(repo *Repo, username string) string {\n\tprs := repo.myReviewRequests(username)\n\n\tif len(prs) == 0 {\n\t\treturn \" [grey]none[white]\\n\"\n\t}\n\n\tstr := \"\"\n\tfor idx, pr := range prs {\n\t\tstr += fmt.Sprintf(` [green][\"%d\"]%4d[\"\"][white] %s`, idx, *pr.Number, *pr.Title)\n\t\tstr += \"\\n\"\n\t\twidget.Items = append(widget.Items, *pr.Number)\n\t}\n\n\treturn str\n}\n\nfunc (widget *Widget) displayStats(repo *Repo) string {\n\tlocPrinter, err := widget.settings.LocalizedPrinter()\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\tstr := fmt.Sprintf(\n\t\t\" PRs: %s  Issues: %s  Stars: %s\\n\",\n\t\tlocPrinter.Sprintf(\"%d\", repo.PullRequestCount()),\n\t\tlocPrinter.Sprintf(\"%d\", repo.IssueCount()),\n\t\tlocPrinter.Sprintf(\"%d\", repo.StarCount()),\n\t)\n\n\treturn str\n}\n\nfunc (widget *Widget) title(repo *Repo) string {\n\treturn fmt.Sprintf(\n\t\t\"[%s]%s - %s[white]\",\n\t\twidget.settings.Colors.Title,\n\t\trepo.Owner,\n\t\trepo.Name,\n\t)\n}\n\nvar mergeIcons = map[string]string{\n\t\"dirty\":    \"[red]\\u0021[white] \",\n\t\"clean\":    \"[green]\\u2713[white] \",\n\t\"unstable\": \"[red]\\u2717[white] \",\n\t\"blocked\":  \"[red]\\u2717[white] \",\n}\n\nfunc (widget *Widget) mergeString(pr *ghb.PullRequest) string {\n\tif !widget.settings.enableStatus {\n\t\treturn \"\"\n\t}\n\tif str, ok := mergeIcons[pr.GetMergeableState()]; ok {\n\t\treturn str\n\t}\n\treturn \"? \"\n}\n"
  },
  {
    "path": "modules/github/github_repo.go",
    "content": "package github\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\tghb \"github.com/google/go-github/v32/github\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"golang.org/x/oauth2\"\n)\n\nconst (\n\tpullRequestsPath = \"/pulls\"\n\tissuesPath       = \"/issues\"\n)\n\n// Repo defines a new GitHub Repo structure\ntype Repo struct {\n\tapiKey    string\n\tbaseURL   string\n\tuploadURL string\n\n\tName         string\n\tOwner        string\n\tPullRequests []*ghb.PullRequest\n\tRemoteRepo   *ghb.Repository\n\tErr          error\n}\n\n// NewGithubRepo returns a new Github Repo with a name, owner, apiKey, baseURL and uploadURL\nfunc NewGithubRepo(name, owner, apiKey, baseURL, uploadURL string) *Repo {\n\trepo := Repo{\n\t\tName:  name,\n\t\tOwner: owner,\n\n\t\tapiKey:    apiKey,\n\t\tbaseURL:   baseURL,\n\t\tuploadURL: uploadURL,\n\t}\n\n\treturn &repo\n}\n\n// Open will open the GitHub Repo URL using the utils helper\nfunc (repo *Repo) Open() {\n\tutils.OpenFile(*repo.RemoteRepo.HTMLURL)\n}\n\n// OpenPulls will open the GitHub Pull Requests URL using the utils helper\nfunc (repo *Repo) OpenPulls() {\n\tutils.OpenFile(*repo.RemoteRepo.HTMLURL + pullRequestsPath)\n}\n\n// OpenIssues will open the GitHub Issues URL using the utils helper\nfunc (repo *Repo) OpenIssues() {\n\tutils.OpenFile(*repo.RemoteRepo.HTMLURL + issuesPath)\n}\n\n// Refresh reloads the github data via the Github API\nfunc (repo *Repo) Refresh() {\n\tprs, err := repo.loadPullRequests()\n\trepo.Err = err\n\trepo.PullRequests = prs\n\tif err != nil {\n\t\treturn\n\t}\n\tremote, err := repo.loadRemoteRepository()\n\trepo.Err = err\n\trepo.RemoteRepo = remote\n}\n\n/* -------------------- Counts -------------------- */\n\n// IssueCount return the total amount of issues as an int\nfunc (repo *Repo) IssueCount() int {\n\tif repo.RemoteRepo == nil {\n\t\treturn 0\n\t}\n\n\tissuesLessPulls := *repo.RemoteRepo.OpenIssuesCount - len(repo.PullRequests)\n\n\treturn issuesLessPulls\n}\n\n// PullRequestCount returns the total amount of pull requests as an int\nfunc (repo *Repo) PullRequestCount() int {\n\treturn len(repo.PullRequests)\n}\n\n// StarCount returns the total amount of stars this repo has gained as an int\nfunc (repo *Repo) StarCount() int {\n\tif repo.RemoteRepo == nil {\n\t\treturn 0\n\t}\n\n\treturn *repo.RemoteRepo.StargazersCount\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (repo *Repo) isGitHubEnterprise() bool {\n\tif len(repo.baseURL) > 0 {\n\t\tif repo.uploadURL == \"\" {\n\t\t\trepo.uploadURL = repo.baseURL\n\t\t}\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (repo *Repo) oauthClient() *http.Client {\n\ttokenService := oauth2.StaticTokenSource(\n\t\t&oauth2.Token{AccessToken: repo.apiKey},\n\t)\n\n\treturn oauth2.NewClient(context.Background(), tokenService)\n}\n\nfunc (repo *Repo) githubClient() (*ghb.Client, error) {\n\toauthClient := repo.oauthClient()\n\n\tif repo.isGitHubEnterprise() {\n\t\treturn ghb.NewEnterpriseClient(repo.baseURL, repo.uploadURL, oauthClient)\n\t}\n\n\treturn ghb.NewClient(oauthClient), nil\n}\n\n// myPullRequests returns a list of pull requests created by username on this repo\nfunc (repo *Repo) myPullRequests(username string, showStatus bool) []*ghb.PullRequest {\n\tprs := []*ghb.PullRequest{}\n\n\tfor _, pr := range repo.PullRequests {\n\t\tuser := *pr.User\n\n\t\tif *user.Login == username {\n\t\t\tprs = append(prs, pr)\n\t\t}\n\t}\n\n\tif showStatus {\n\t\tprs = repo.individualPRs(prs)\n\t}\n\n\treturn prs\n}\n\n// individualPRs takes a list of pull requests (presumably returned from\n// github.PullRequests.List) and fetches them individually to get more detailed\n// status info on each. see: https://developer.github.com/v3/git/#checking-mergeability-of-pull-requests\nfunc (repo *Repo) individualPRs(prs []*ghb.PullRequest) []*ghb.PullRequest {\n\tgithub, err := repo.githubClient()\n\tif err != nil {\n\t\treturn prs\n\t}\n\n\tvar ret []*ghb.PullRequest\n\tfor i := range prs {\n\t\tpr, _, err := github.PullRequests.Get(context.Background(), repo.Owner, repo.Name, prs[i].GetNumber())\n\t\tif err != nil {\n\t\t\t// worst case, just keep the original one\n\t\t\tret = append(ret, prs[i])\n\t\t} else {\n\t\t\tret = append(ret, pr)\n\t\t}\n\t}\n\treturn ret\n}\n\n// myReviewRequests returns a list of pull requests for which username has been\n// requested to do a code review\nfunc (repo *Repo) myReviewRequests(username string) []*ghb.PullRequest {\n\tprs := []*ghb.PullRequest{}\n\n\tfor _, pr := range repo.PullRequests {\n\t\tfor _, reviewer := range pr.RequestedReviewers {\n\t\t\tif *reviewer.Login == username {\n\t\t\t\tprs = append(prs, pr)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn prs\n}\n\nfunc (repo *Repo) customIssueQuery(filter string, perPage int) *ghb.IssuesSearchResult {\n\tgithub, err := repo.githubClient()\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\topts := &ghb.SearchOptions{}\n\tif perPage != 0 {\n\t\topts.PerPage = perPage\n\t}\n\n\tprs, _, _ := github.Search.Issues(context.Background(), fmt.Sprintf(\"%s repo:%s/%s\", filter, repo.Owner, repo.Name), opts)\n\treturn prs\n}\n\nfunc (repo *Repo) loadPullRequests() ([]*ghb.PullRequest, error) {\n\tgithub, err := repo.githubClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\topts := &ghb.PullRequestListOptions{}\n\topts.PerPage = 100\n\n\tprs, _, err := github.PullRequests.List(context.Background(), repo.Owner, repo.Name, opts)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prs, nil\n}\n\nfunc (repo *Repo) loadRemoteRepository() (*ghb.Repository, error) {\n\tgithub, err := repo.githubClient()\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trepository, _, err := github.Repositories.Get(context.Background(), repo.Owner, repo.Name)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn repository, nil\n}\n"
  },
  {
    "path": "modules/github/keyboard.go",
    "content": "package github\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardChar(\"o\", widget.openRepo, \"Open item in browser\")\n\twidget.SetKeyboardChar(\"p\", widget.openPulls, \"Open pull requests in browser\")\n\twidget.SetKeyboardChar(\"i\", widget.openIssues, \"Open issues in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openPr, \"Open PR in browser\")\n\twidget.SetKeyboardKey(tcell.KeyInsert, widget.openRepo, \"Open item in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/github/settings.go",
    "content": "package github\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"GitHub\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey                 string        `help:\"Your GitHub API token.\"`\n\tbaseURL                string        `help:\"Your GitHub Enterprise API URL.\" optional:\"true\"`\n\tcustomQueries          []customQuery `help:\"Custom queries allow you to filter pull requests and issues however you like. Give the query a title and a filter. Filters can be copied directly from GitHub’s UI.\" optional:\"true\"`\n\tenableStatus           bool          `help:\"Display pull request mergeability status (‘dirty’, ‘clean’, ‘unstable’, ‘blocked’).\" optional:\"true\"`\n\trepositories           []string      `help:\"A list of github repositories.\" values:\"Example: wtfutil/wtf\"`\n\tshowMyPullRequests     bool          `help:\"Show my pull requests section\" optional:\"true\"`\n\tshowOpenReviewRequests bool          `help:\"Show open review requests section\" optional:\"true\"`\n\tshowStats              bool          `help:\"Show repository stats section\" optional:\"true\"`\n\tuploadURL              string        `help:\"Your GitHub Enterprise upload URL (often the same as API URL).\" optional:\"true\"`\n\tusername               string        `help:\"Your GitHub username. Used to figure out which review requests you’ve been added to.\"`\n}\n\ntype customQuery struct {\n\ttitle   string `help:\"Display title for this query\"`\n\tfilter  string `help:\"Github query filter\"`\n\tperPage int    `help:\"Number of issues to show\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:                 ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_GITHUB_TOKEN\"))),\n\t\tbaseURL:                ymlConfig.UString(\"baseURL\", os.Getenv(\"WTF_GITHUB_BASE_URL\")),\n\t\tenableStatus:           ymlConfig.UBool(\"enableStatus\", false),\n\t\tshowMyPullRequests:     ymlConfig.UBool(\"showMyPullRequests\", true),\n\t\tshowOpenReviewRequests: ymlConfig.UBool(\"showOpenReviewRequests\", true),\n\t\tshowStats:              ymlConfig.UBool(\"showStats\", true),\n\t\tuploadURL:              ymlConfig.UString(\"uploadURL\", os.Getenv(\"WTF_GITHUB_UPLOAD_URL\")),\n\t\tusername:               ymlConfig.UString(\"username\"),\n\t}\n\tsettings.repositories = cfg.ParseAsMapOrList(ymlConfig, \"repositories\")\n\tsettings.customQueries = parseCustomQueries(ymlConfig)\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).\n\t\tService(settings.baseURL).Load()\n\n\treturn &settings\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc parseCustomQueries(ymlConfig *config.Config) []customQuery {\n\tresult := []customQuery{}\n\tif customQueries, err := ymlConfig.Map(\"customQueries\"); err == nil {\n\t\tfor _, query := range customQueries {\n\t\t\tc := customQuery{}\n\t\t\tfor key, value := range query.(map[string]interface{}) {\n\t\t\t\tswitch key {\n\t\t\t\tcase \"title\":\n\t\t\t\t\tc.title = value.(string)\n\t\t\t\tcase \"filter\":\n\t\t\t\t\tc.filter = value.(string)\n\t\t\t\tcase \"perPage\":\n\t\t\t\t\tc.perPage = value.(int)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif c.title != \"\" && c.filter != \"\" {\n\t\t\t\tresult = append(result, c)\n\t\t\t}\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "modules/github/widget.go",
    "content": "package github\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget define wtf widget to register widget later\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tGithubRepos []*Repo\n\n\tsettings *Settings\n\tSelected int\n\tmaxItems int\n\tItems    []int\n}\n\n// NewWidget creates a new instance of the widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"repository\", \"repositories\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.GithubRepos = widget.buildRepoCollection(widget.settings.repositories)\n\n\twidget.initializeKeyboardControls()\n\n\twidget.View.SetRegions(true)\n\twidget.SetDisplayFunction(widget.display)\n\n\twidget.Unselect()\n\n\twidget.Sources = widget.settings.repositories\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// SetItemCount sets the amount of PRs RRs and other PRs throughout the widgets display creation\nfunc (widget *Widget) SetItemCount(items int) {\n\twidget.maxItems = items\n}\n\n// GetItemCount returns the amount of PRs RRs and other PRs calculated so far as an int\nfunc (widget *Widget) GetItemCount() int {\n\treturn widget.maxItems\n}\n\n// GetSelected returns the index of the currently highlighted item as an int\nfunc (widget *Widget) GetSelected() int {\n\tif widget.Selected < 0 {\n\t\treturn 0\n\t}\n\treturn widget.Selected\n}\n\n// Next cycles the currently highlighted text down\nfunc (widget *Widget) Next() {\n\twidget.Selected++\n\tif widget.Selected >= widget.maxItems {\n\t\twidget.Selected = 0\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Prev cycles the currently highlighted text up\nfunc (widget *Widget) Prev() {\n\twidget.Selected--\n\tif widget.Selected < 0 {\n\t\twidget.Selected = widget.maxItems - 1\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Unselect stops highlighting the text and jumps the scroll position to the top\nfunc (widget *Widget) Unselect() {\n\twidget.Selected = -1\n\twidget.View.Highlight()\n\twidget.View.ScrollToBeginning()\n}\n\n// Refresh reloads the github data via the Github API and reruns the display\nfunc (widget *Widget) Refresh() {\n\tfor _, repo := range widget.GithubRepos {\n\t\trepo.Refresh()\n\t}\n\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) buildRepoCollection(repoData []string) []*Repo {\n\tgithubRepos := []*Repo{}\n\n\tfor _, repo := range repoData {\n\t\tsplit := strings.Split(repo, \"/\")\n\t\towner, name := split[0], split[1]\n\t\trepo := NewGithubRepo(\n\t\t\tname,\n\t\t\towner,\n\t\t\twidget.settings.apiKey,\n\t\t\twidget.settings.baseURL,\n\t\t\twidget.settings.uploadURL,\n\t\t)\n\n\t\tgithubRepos = append(githubRepos, repo)\n\t}\n\n\treturn githubRepos\n}\n\nfunc (widget *Widget) currentGithubRepo() *Repo {\n\tif len(widget.GithubRepos) == 0 {\n\t\treturn nil\n\t}\n\n\tif widget.Idx < 0 || widget.Idx >= len(widget.GithubRepos) {\n\t\treturn nil\n\t}\n\n\treturn widget.GithubRepos[widget.Idx]\n}\n\nfunc (widget *Widget) openPr() {\n\tcurrentSelection := widget.View.GetHighlights()\n\tif widget.Selected >= 0 && len(widget.Items) > 0 && currentSelection[0] != \"\" {\n\t\turl := (*widget.currentGithubRepo().RemoteRepo.HTMLURL + \"/pull/\" + strconv.Itoa(widget.Items[widget.Selected]))\n\t\tutils.OpenFile(url)\n\t}\n}\n\nfunc (widget *Widget) openRepo() {\n\trepo := widget.currentGithubRepo()\n\n\tif repo != nil {\n\t\trepo.Open()\n\t}\n}\n\nfunc (widget *Widget) openPulls() {\n\trepo := widget.currentGithubRepo()\n\n\tif repo != nil {\n\t\trepo.OpenPulls()\n\t}\n}\n\nfunc (widget *Widget) openIssues() {\n\trepo := widget.currentGithubRepo()\n\n\tif repo != nil {\n\t\trepo.OpenIssues()\n\t}\n}\n"
  },
  {
    "path": "modules/gitlab/display.go",
    "content": "package gitlab\n\nimport (\n\t\"fmt\"\n\n\tglab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) displayError() {\n\twidget.Redraw(widget.contentError)\n}\n\nfunc (widget *Widget) contentError() (string, string, bool) {\n\n\ttitle := fmt.Sprintf(\"%s - Error\", widget.CommonSettings().Title)\n\n\tif widget.configError != nil {\n\t\treturn title, fmt.Sprintf(\"Error: \\n [red]%v[white]\", widget.configError), false\n\n\t}\n\treturn title, \"Error\", false\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\n\tproject := widget.currentGitlabProject()\n\tif project == nil {\n\t\treturn widget.CommonSettings().Title, \" Gitlab project data is unavailable \", true\n\t}\n\n\t// initial maxItems count\n\twidget.Items = make([]ContentItem, 0)\n\twidget.SetItemCount(0)\n\n\ttitle := fmt.Sprintf(\"%s - %s\", widget.CommonSettings().Title, widget.title(project))\n\n\t_, _, width, _ := widget.View.GetRect()\n\tstr := widget.settings.PaginationMarker(len(widget.GitlabProjects), widget.Idx, width) + \"\\n\"\n\tstr += fmt.Sprintf(\" [%s]Stats[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += widget.displayStats(project)\n\tstr += \"\\n\"\n\tstr += fmt.Sprintf(\" [%s]Open Assigned Merge Requests[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += widget.displayMyAssignedMergeRequests(project, widget.settings.username)\n\tstr += \"\\n\"\n\tstr += fmt.Sprintf(\" [%s]My Merge Requests[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += widget.displayMyMergeRequests(project, widget.settings.username)\n\tstr += \"\\n\"\n\tstr += fmt.Sprintf(\" [%s]Open Assigned Issues[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += widget.displayMyAssignedIssues(project, widget.settings.username)\n\tstr += \"\\n\"\n\tstr += fmt.Sprintf(\" [%s]My Issues[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += widget.displayMyIssues(project, widget.settings.username)\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) displayMyMergeRequests(project *GitlabProject, username string) string {\n\tmrs := project.myMergeRequests()\n\treturn widget.renderMergeRequests(mrs)\n}\n\nfunc (widget *Widget) displayMyAssignedMergeRequests(project *GitlabProject, username string) string {\n\tmrs := project.myAssignedMergeRequests()\n\treturn widget.renderMergeRequests(mrs)\n}\n\nfunc (widget *Widget) displayMyAssignedIssues(project *GitlabProject, username string) string {\n\tissues := project.myAssignedIssues()\n\treturn widget.renderIssues(issues)\n}\n\nfunc (widget *Widget) displayMyIssues(project *GitlabProject, username string) string {\n\tissues := project.myIssues()\n\treturn widget.renderIssues(issues)\n}\n\nfunc (widget *Widget) renderMergeRequests(mrs []*glab.BasicMergeRequest) string {\n\n\tlength := len(mrs)\n\n\tif length == 0 {\n\t\treturn \" [grey]none[white]\\n\"\n\t}\n\tmaxItems := widget.GetItemCount()\n\n\tstr := \"\"\n\tfor idx, issue := range mrs {\n\t\tstr += fmt.Sprintf(` [green][\"%d\"]%4d[\"\"][white] %s`, maxItems+idx, issue.IID, issue.Title)\n\t\tstr += \"\\n\"\n\t\twidget.Items = append(widget.Items, ContentItem{Type: \"MR\", ID: issue.IID})\n\t}\n\twidget.SetItemCount(maxItems + length)\n\n\treturn str\n}\n\nfunc (widget *Widget) renderIssues(issues []*glab.Issue) string {\n\n\tlength := len(issues)\n\n\tif length == 0 {\n\t\treturn \" [grey]none[white]\\n\"\n\t}\n\tmaxItems := widget.GetItemCount()\n\n\tstr := \"\"\n\tfor idx, issue := range issues {\n\t\tstr += fmt.Sprintf(` [green][\"%d\"]%4d[\"\"][white] %s`, maxItems+idx, issue.IID, issue.Title)\n\t\tstr += \"\\n\"\n\t\twidget.Items = append(widget.Items, ContentItem{Type: \"ISSUE\", ID: issue.IID})\n\t}\n\twidget.SetItemCount(maxItems + length)\n\n\treturn str\n}\n\nfunc (widget *Widget) displayStats(project *GitlabProject) string {\n\tstr := fmt.Sprintf(\n\t\t\" MRs: %d  Issues: %d  Stars: %d\\n\",\n\t\tproject.MergeRequestCount(),\n\t\tproject.IssueCount(),\n\t\tproject.StarCount(),\n\t)\n\n\treturn str\n}\n\nfunc (widget *Widget) title(project *GitlabProject) string {\n\treturn fmt.Sprintf(\"[green]%s [white]\", project.path)\n}\n"
  },
  {
    "path": "modules/gitlab/gitlab_project.go",
    "content": "package gitlab\n\nimport (\n\tglab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\ntype context struct {\n\tclient *glab.Client\n\tuser   *glab.User\n}\n\nfunc newContext(settings *Settings) (*context, error) {\n\tbaseURL := settings.domain\n\tgitlabClient, _ := glab.NewClient(settings.apiKey, glab.WithBaseURL(baseURL))\n\n\tuser, _, err := gitlabClient.Users.CurrentUser()\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tctx := &context{\n\t\tclient: gitlabClient,\n\t\tuser:   user,\n\t}\n\n\treturn ctx, nil\n}\n\ntype GitlabProject struct {\n\tcontext *context\n\tpath    string\n\n\tMergeRequests         []*glab.BasicMergeRequest\n\tAssignedMergeRequests []*glab.BasicMergeRequest\n\tAuthoredMergeRequests []*glab.BasicMergeRequest\n\tAssignedIssues        []*glab.Issue\n\tAuthoredIssues        []*glab.Issue\n\tRemoteProject         *glab.Project\n}\n\nfunc NewGitlabProject(context *context, projectPath string) *GitlabProject {\n\tproject := GitlabProject{\n\t\tcontext: context,\n\t\tpath:    projectPath,\n\t}\n\n\treturn &project\n}\n\n// Refresh reloads the gitlab data via the Gitlab API\nfunc (project *GitlabProject) Refresh() {\n\tproject.MergeRequests, _ = project.loadMergeRequests()\n\tproject.AssignedMergeRequests, _ = project.loadAssignedMergeRequests()\n\tproject.AuthoredMergeRequests, _ = project.loadAuthoredMergeRequests()\n\tproject.AssignedIssues, _ = project.loadAssignedIssues()\n\tproject.AuthoredIssues, _ = project.loadAuthoredIssues()\n\tproject.RemoteProject, _ = project.loadRemoteProject()\n}\n\n/* -------------------- Counts -------------------- */\n\nfunc (project *GitlabProject) IssueCount() int {\n\tif project.RemoteProject == nil {\n\t\treturn 0\n\t}\n\n\treturn project.RemoteProject.OpenIssuesCount\n}\n\nfunc (project *GitlabProject) MergeRequestCount() int {\n\treturn len(project.MergeRequests)\n}\n\nfunc (project *GitlabProject) StarCount() int {\n\tif project.RemoteProject == nil {\n\t\treturn 0\n\t}\n\n\treturn project.RemoteProject.StarCount\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// myMergeRequests returns a list of merge requests\nfunc (project *GitlabProject) myMergeRequests() []*glab.BasicMergeRequest {\n\treturn project.AuthoredMergeRequests\n}\n\n// myAssignedMergeRequests returns a list of merge requests\n// assigned\nfunc (project *GitlabProject) myAssignedMergeRequests() []*glab.BasicMergeRequest {\n\treturn project.AssignedMergeRequests\n}\n\n// myAssignedIssues returns a list of issues\nfunc (project *GitlabProject) myAssignedIssues() []*glab.Issue {\n\treturn project.AssignedIssues\n}\n\n// myIssues returns a list of issues\nfunc (project *GitlabProject) myIssues() []*glab.Issue {\n\treturn project.AuthoredIssues\n}\n\nfunc (project *GitlabProject) loadMergeRequests() ([]*glab.BasicMergeRequest, error) {\n\tstate := \"opened\"\n\topts := glab.ListProjectMergeRequestsOptions{\n\t\tState: &state,\n\t}\n\n\tmrs, _, err := project.context.client.MergeRequests.ListProjectMergeRequests(project.path, &opts)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn mrs, nil\n}\n\nfunc (project *GitlabProject) loadAssignedMergeRequests() ([]*glab.BasicMergeRequest, error) {\n\tstate := \"opened\"\n\topts := glab.ListProjectMergeRequestsOptions{\n\t\tState:      &state,\n\t\tAssigneeID: glab.AssigneeID(project.context.user.ID),\n\t}\n\n\tmrs, _, err := project.context.client.MergeRequests.ListProjectMergeRequests(project.path, &opts)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn mrs, nil\n}\n\nfunc (project *GitlabProject) loadAuthoredMergeRequests() ([]*glab.BasicMergeRequest, error) {\n\tstate := \"opened\"\n\topts := glab.ListProjectMergeRequestsOptions{\n\t\tState:    &state,\n\t\tAuthorID: &project.context.user.ID,\n\t}\n\n\tmrs, _, err := project.context.client.MergeRequests.ListProjectMergeRequests(project.path, &opts)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn mrs, nil\n}\n\nfunc (project *GitlabProject) loadAssignedIssues() ([]*glab.Issue, error) {\n\tstate := \"opened\"\n\topts := glab.ListProjectIssuesOptions{\n\t\tState:      &state,\n\t\tAssigneeID: &project.context.user.ID,\n\t}\n\n\tissues, _, err := project.context.client.Issues.ListProjectIssues(project.path, &opts)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn issues, nil\n}\n\nfunc (project *GitlabProject) loadAuthoredIssues() ([]*glab.Issue, interface{}) {\n\tstate := \"opened\"\n\topts := glab.ListProjectIssuesOptions{\n\t\tState:    &state,\n\t\tAuthorID: &project.context.user.ID,\n\t}\n\n\tissues, _, err := project.context.client.Issues.ListProjectIssues(project.path, &opts)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn issues, nil\n}\n\nfunc (project *GitlabProject) loadRemoteProject() (*glab.Project, error) {\n\tprojectsitory, _, err := project.context.client.Projects.GetProject(project.path, nil)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn projectsitory, nil\n}\n"
  },
  {
    "path": "modules/gitlab/keyboard.go",
    "content": "package gitlab\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next project\")\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous project\")\n\twidget.SetKeyboardChar(\"o\", widget.openRepo, \"Open item in browser\")\n\twidget.SetKeyboardChar(\"p\", widget.openPulls, \"Open merge requests in browser\")\n\twidget.SetKeyboardChar(\"i\", widget.openIssues, \"Open issues in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next project\")\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous project\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openItemInBrowser, \"Open item in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/gitlab/settings.go",
    "content": "package gitlab\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"GitLab\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey   string   `help:\"A GitLab personal access token. Requires at least api access.\"`\n\tdomain   string   `help:\"Your GitLab corporate domain.\"`\n\tprojects []string `help:\"A list of key/value pairs each describing a GitLab project to fetch data for.\" values:\"Key: The name of the project. Value: The namespace of the project.\"`\n\tusername string   `help:\"Your GitLab username. Used to figure out which requests require your approval\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:   ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_GITLAB_TOKEN\"))),\n\t\tdomain:   ymlConfig.UString(\"domain\", \"https://gitlab.com\"),\n\t\tusername: ymlConfig.UString(\"username\"),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).\n\t\tService(settings.domain).Load()\n\n\tsettings.projects = cfg.ParseAsMapOrList(ymlConfig, \"projects\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/gitlab/widget.go",
    "content": "package gitlab\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype ContentItem struct {\n\tType string\n\tID   int\n}\n\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tGitlabProjects []*GitlabProject\n\n\tcontext  *context\n\tsettings *Settings\n\tSelected int\n\tmaxItems int\n\tItems    []ContentItem\n\n\tconfigError error\n}\n\n// NewWidget creates a new instance of the widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\tcontext, err := newContext(settings)\n\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"project\", \"projects\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tcontext:  context,\n\t\tsettings: settings,\n\n\t\tconfigError: err,\n\t}\n\n\twidget.GitlabProjects = widget.buildProjectCollection(context, settings.projects)\n\n\twidget.initializeKeyboardControls()\n\twidget.View.SetRegions(true)\n\twidget.SetDisplayFunction(widget.display)\n\n\twidget.Unselect()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.context == nil || widget.configError != nil {\n\t\twidget.displayError()\n\t\treturn\n\t}\n\n\tfor _, project := range widget.GitlabProjects {\n\t\tproject.Refresh()\n\t}\n\n\twidget.display()\n}\n\n// SetItemCount sets the amount of PRs RRs and other PRs throughout the widgets display creation\nfunc (widget *Widget) SetItemCount(items int) {\n\twidget.maxItems = items\n}\n\n// GetItemCount returns the amount of PRs RRs and other PRs calculated so far as an int\nfunc (widget *Widget) GetItemCount() int {\n\treturn widget.maxItems\n}\n\n// GetSelected returns the index of the currently highlighted item as an int\nfunc (widget *Widget) GetSelected() int {\n\tif widget.Selected < 0 {\n\t\treturn 0\n\t}\n\treturn widget.Selected\n}\n\n// Next cycles the currently highlighted text down\nfunc (widget *Widget) Next() {\n\twidget.Selected++\n\tif widget.Selected >= widget.maxItems {\n\t\twidget.Selected = 0\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Prev cycles the currently highlighted text up\nfunc (widget *Widget) Prev() {\n\twidget.Selected--\n\tif widget.Selected < 0 {\n\t\twidget.Selected = widget.maxItems - 1\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Unselect stops highlighting the text and jumps the scroll position to the top\nfunc (widget *Widget) Unselect() {\n\twidget.Selected = -1\n\twidget.View.Highlight()\n\twidget.View.ScrollToBeginning()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) buildProjectCollection(context *context, projectData []string) []*GitlabProject {\n\tgitlabProjects := []*GitlabProject{}\n\n\tfor _, projectPath := range projectData {\n\t\tproject := NewGitlabProject(context, projectPath)\n\t\tgitlabProjects = append(gitlabProjects, project)\n\t}\n\n\treturn gitlabProjects\n}\n\nfunc (widget *Widget) currentGitlabProject() *GitlabProject {\n\tif len(widget.GitlabProjects) == 0 {\n\t\treturn nil\n\t}\n\n\tif widget.Idx < 0 || widget.Idx >= len(widget.GitlabProjects) {\n\t\treturn nil\n\t}\n\n\treturn widget.GitlabProjects[widget.Idx]\n}\n\nfunc (widget *Widget) openItemInBrowser() {\n\tcurrentSelection := widget.View.GetHighlights()\n\tif widget.Selected >= 0 && currentSelection[0] != \"\" {\n\n\t\titem := widget.Items[widget.Selected]\n\t\turl := \"\"\n\n\t\tproject := widget.currentGitlabProject()\n\t\tif project == nil {\n\t\t\t// This is a problem. We will just bail out for now\n\t\t\treturn\n\t\t}\n\n\t\tswitch item.Type {\n\t\tcase \"MR\":\n\t\t\turl = (project.RemoteProject.WebURL + \"/merge_requests/\" + strconv.Itoa(item.ID))\n\t\tcase \"ISSUE\":\n\t\t\turl = (project.RemoteProject.WebURL + \"/issues/\" + strconv.Itoa(item.ID))\n\t\t}\n\n\t\tutils.OpenFile(url)\n\t}\n}\n\nfunc (widget *Widget) openRepo() {\n\tproject := widget.currentGitlabProject()\n\tif project == nil {\n\t\treturn\n\t}\n\turl := project.RemoteProject.WebURL\n\tutils.OpenFile(url)\n}\n\nfunc (widget *Widget) openPulls() {\n\tproject := widget.currentGitlabProject()\n\tif project == nil {\n\t\treturn\n\t}\n\turl := project.RemoteProject.WebURL + \"/merge_requests/\"\n\tutils.OpenFile(url)\n}\n\nfunc (widget *Widget) openIssues() {\n\tproject := widget.currentGitlabProject()\n\tif project == nil {\n\t\treturn\n\t}\n\turl := project.RemoteProject.WebURL + \"/issues/\"\n\tutils.OpenFile(url)\n}\n"
  },
  {
    "path": "modules/gitlabtodo/keyboard.go",
    "content": "package gitlabtodo\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openTodo, \"Open todo in browser\")\n\twidget.SetKeyboardChar(\"x\", widget.markAsDone, \"Mark todo as done\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openTodo, \"Open todo in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/gitlabtodo/settings.go",
    "content": "package gitlabtodo\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"GitLab Todos\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tnumberOfTodos int    `help:\"Defines number of stories to be displayed. Default is 10\" optional:\"true\"`\n\tapiKey        string `help:\"A GitLab personal access token. Requires at least api access.\"`\n\tdomain        string `help:\"Your GitLab corporate domain.\"`\n\tshowProject   bool   `help:\"Determines whether or not to show the project a given todo is for.\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tnumberOfTodos: ymlConfig.UInt(\"numberOfTodos\", 10),\n\t\tapiKey:        ymlConfig.UString(\"apiKey\", os.Getenv(\"WTF_GITLAB_TOKEN\")),\n\t\tdomain:        ymlConfig.UString(\"domain\", \"https://gitlab.com\"),\n\t\tshowProject:   ymlConfig.UBool(\"showProject\", true),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).\n\t\tService(settings.domain).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/gitlabtodo/widget.go",
    "content": "package gitlabtodo\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\n\t\"github.com/rivo/tview\"\n\tglab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\ttodos        []*glab.Todo\n\tgitlabClient *glab.Client\n\tsettings     *Settings\n\terr          error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.gitlabClient, _ = glab.NewClient(settings.apiKey, glab.WithBaseURL(settings.domain))\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\ttodos, err := widget.getTodos()\n\twidget.todos = todos\n\twidget.err = err\n\twidget.SetItemCount(len(todos))\n\n\twidget.Render()\n}\n\n// Render sets up the widget data for redrawing to the screen\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := fmt.Sprintf(\"GitLab ToDos (%d)\", len(widget.todos))\n\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif widget.todos == nil {\n\t\treturn title, \"No ToDos to display\", false\n\t}\n\n\tstr := widget.contentFrom(widget.todos)\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) getTodos() ([]*glab.Todo, error) {\n\topts := glab.ListTodosOptions{}\n\n\ttodos, _, err := widget.gitlabClient.Todos.ListTodos(&opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn todos, nil\n}\n\n// trim the todo body so it fits on a single line\nfunc (widget *Widget) trimTodoBody(body string) string {\n\tr := []rune(body)\n\n\t// Cut at first occurrence of a newline\n\tfor i, a := range r {\n\t\tif a == '\\n' {\n\t\t\treturn string(r[:i])\n\t\t}\n\t}\n\n\treturn body\n}\n\nfunc (widget *Widget) contentFrom(todos []*glab.Todo) string {\n\tvar str string\n\n\tfor idx, todo := range todos {\n\t\trow := fmt.Sprintf(`[%s]%2d. `, widget.RowColor(idx), idx+1)\n\t\tif widget.settings.showProject {\n\t\t\trow = fmt.Sprintf(`%s%s `, row, todo.Project.Path)\n\t\t}\n\t\trow = fmt.Sprintf(`%s[mediumpurple](%s)[%s] %s`,\n\t\t\trow,\n\t\t\ttodo.Author.Username,\n\t\t\twidget.RowColor(idx),\n\t\t\twidget.trimTodoBody(todo.Body),\n\t\t)\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(todo.Body))\n\t}\n\n\treturn str\n}\n\nfunc (widget *Widget) markAsDone() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.todos != nil && sel < len(widget.todos) {\n\t\ttodo := widget.todos[sel]\n\t\t_, err := widget.gitlabClient.Todos.MarkTodoAsDone(todo.ID)\n\t\tif err == nil {\n\t\t\twidget.Refresh()\n\t\t}\n\t}\n}\n\nfunc (widget *Widget) openTodo() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.todos != nil && sel < len(widget.todos) {\n\t\ttodo := widget.todos[sel]\n\t\tutils.OpenFile(todo.TargetURL)\n\t}\n}\n"
  },
  {
    "path": "modules/gitter/client.go",
    "content": "package gitter\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc GetMessages(roomId string, numberOfMessages int, apiToken string) ([]Message, error) {\n\tvar messages []Message\n\n\tresp, err := apiRequest(\"rooms/\"+roomId+\"/chatMessages?limit=\"+strconv.Itoa(numberOfMessages), apiToken)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = utils.ParseJSON(&messages, resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn messages, nil\n}\n\nfunc GetRoom(roomUri, apiToken string) (*Room, error) {\n\tvar rooms Rooms\n\n\tresp, err := apiRequest(\"rooms?q=\"+roomUri, apiToken)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = utils.ParseJSON(&rooms, resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, room := range rooms.Results {\n\t\tif room.URI == roomUri {\n\t\t\treturn &room, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nvar (\n\tapiBaseURL = \"https://api.gitter.im/v1/\"\n)\n\nfunc apiRequest(path, apiToken string) (*http.Response, error) {\n\treq, err := http.NewRequest(\"GET\", apiBaseURL+path, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbearer := fmt.Sprintf(\"Bearer %s\", apiToken)\n\treq.Header.Add(\"Authorization\", bearer)\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "modules/gitter/gitter.go",
    "content": "package gitter\n\nimport \"time\"\n\ntype Rooms struct {\n\tResults []Room `json:\"results\"`\n}\n\ntype Room struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n\tURI  string `json:\"uri\"`\n}\n\ntype User struct {\n\tID          string `json:\"id\"`\n\tUsername    string `json:\"username\"`\n\tDisplayName string `json:\"displayName\"`\n}\n\ntype Message struct {\n\tID     string    `json:\"id\"`\n\tText   string    `json:\"text\"`\n\tHTML   string    `json:\"html\"`\n\tSent   time.Time `json:\"sent\"`\n\tFrom   User      `json:\"fromUser\"`\n\tUnread bool      `json:\"unread\"`\n}\n"
  },
  {
    "path": "modules/gitter/keyboard.go",
    "content": "package gitter\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/gitter/settings.go",
    "content": "package gitter\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Gitter\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiToken         string `help:\"Your Gitter Personal Access Token.\"`\n\tnumberOfMessages int    `help:\"Maximum number of (newest) messages to be displayed. Default is 10\" optional:\"true\"`\n\troomURI          string `help:\"The room you want to display.\" values:\"Example: wtfutil/Lobby\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiToken:         ymlConfig.UString(\"apiToken\", os.Getenv(\"WTF_GITTER_API_TOKEN\")),\n\t\tnumberOfMessages: ymlConfig.UInt(\"numberOfMessages\", 10),\n\t\troomURI:          ymlConfig.UString(\"roomUri\", \"wtfutil/Lobby\"),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiToken).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/gitter/widget.go",
    "content": "package gitter\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// A Widget represents a Gitter widget\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tmessages []Message\n\tsettings *Settings\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Refresh)\n\twidget.initializeKeyboardControls()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\troom, err := GetRoom(widget.settings.roomURI, widget.settings.apiToken)\n\tif err != nil {\n\t\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, err.Error(), true })\n\t\treturn\n\t}\n\n\tif room == nil {\n\t\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, \"No room\", true })\n\t\treturn\n\t}\n\n\tmessages, err := GetMessages(room.ID, widget.settings.numberOfMessages, widget.settings.apiToken)\n\n\tif err != nil {\n\t\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, err.Error(), true })\n\t\treturn\n\t}\n\twidget.messages = messages\n\twidget.SetItemCount(len(messages))\n\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := fmt.Sprintf(\"%s - %s\", widget.CommonSettings().Title, widget.settings.roomURI)\n\tif len(widget.messages) == 0 {\n\t\treturn title, \"No Messages To Display\", false\n\t}\n\tvar str string\n\tfor idx, message := range widget.messages {\n\t\trow := fmt.Sprintf(\n\t\t\t`[%s] [blue]%s [lightslategray]%s: [%s]%s [aqua]%s`,\n\t\t\twidget.RowColor(idx),\n\t\t\tmessage.From.DisplayName,\n\t\t\tmessage.From.Username,\n\t\t\twidget.RowColor(idx),\n\t\t\tmessage.Text,\n\t\t\tmessage.Sent.Format(\"Jan 02, 15:04 MST\"),\n\t\t)\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(message.Text))\n\t}\n\n\treturn title, str, true\n}\n"
  },
  {
    "path": "modules/googleanalytics/client.go",
    "content": "package googleanalytics\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"golang.org/x/oauth2/google\"\n\tgaV3 \"google.golang.org/api/analytics/v3\"\n\tgaV4 \"google.golang.org/api/analyticsreporting/v4\"\n\t\"google.golang.org/api/option\"\n)\n\ntype websiteReport struct {\n\tName           string\n\tReport         *gaV4.GetReportsResponse\n\tRealtimeReport *gaV3.RealtimeData\n}\n\nfunc (widget *Widget) fetch() []websiteReport {\n\tsecretPath, err := utils.ExpandHomeDir(widget.settings.secretFile)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to parse secretFile path\")\n\t}\n\n\tserviceV4, err := makeReportServiceV4(secretPath)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to create v3 Google Analytics Reporting Service\")\n\t}\n\n\tvar serviceV3 *gaV3.Service\n\tif widget.settings.enableRealtime {\n\t\tserviceV3, err = makeReportServiceV3(secretPath)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Unable to create v3 Google Analytics Reporting Service\")\n\t\t}\n\t}\n\n\tvisitorsDataArray := getReports(\n\t\tserviceV4, widget.settings.viewIds, widget.settings.months, serviceV3,\n\t)\n\treturn visitorsDataArray\n}\n\nfunc buildNetClient(secretPath string) *http.Client {\n\tclientSecret, err := os.ReadFile(filepath.Clean(secretPath))\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to read secretPath. %v\", err)\n\t}\n\n\tjwtConfig, err := google.JWTConfigFromJSON(clientSecret, gaV4.AnalyticsReadonlyScope)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to get config from JSON. %v\", err)\n\t}\n\n\treturn jwtConfig.Client(context.Background())\n}\n\nfunc makeReportServiceV3(secretPath string) (*gaV3.Service, error) {\n\tclient := buildNetClient(secretPath)\n\n\tsvc, err := gaV3.NewService(context.Background(), option.WithHTTPClient(client))\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to create v3 Google Analytics Reporting Service\")\n\t}\n\n\treturn svc, err\n}\n\nfunc makeReportServiceV4(secretPath string) (*gaV4.Service, error) {\n\tclient := buildNetClient(secretPath)\n\tsvc, err := gaV4.NewService(context.Background(), option.WithHTTPClient(client))\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to create v4 Google Analytics Reporting Service\")\n\t}\n\n\treturn svc, err\n}\n\nfunc getReports(\n\tserviceV4 *gaV4.Service, viewIds map[string]interface{}, displayedMonths int, serviceV3 *gaV3.Service,\n) []websiteReport {\n\tstartDate := fmt.Sprintf(\"%s-01\", time.Now().AddDate(0, -displayedMonths+1, 0).Format(\"2006-01\"))\n\tvar websiteReports []websiteReport\n\n\tfor website, viewID := range viewIds {\n\t\t// For custom queries: https://ga-dev-tools.appspot.com/dimensions-metrics-explorer/\n\n\t\treq := &gaV4.GetReportsRequest{\n\t\t\tReportRequests: []*gaV4.ReportRequest{\n\t\t\t\t{\n\t\t\t\t\tViewId: viewID.(string),\n\t\t\t\t\tDateRanges: []*gaV4.DateRange{\n\t\t\t\t\t\t{StartDate: startDate, EndDate: \"today\"},\n\t\t\t\t\t},\n\t\t\t\t\tMetrics: []*gaV4.Metric{\n\t\t\t\t\t\t{Expression: \"ga:sessions\"},\n\t\t\t\t\t},\n\t\t\t\t\tDimensions: []*gaV4.Dimension{\n\t\t\t\t\t\t{Name: \"ga:month\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tresponse, err := serviceV4.Reports.BatchGet(req).Do()\n\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"GET request to analyticsreporting/v4 returned error with viewID: %s\", viewID)\n\t\t}\n\t\tif response.HTTPStatusCode != 200 {\n\t\t\tlog.Fatalf(\"Did not get expected HTTP response code\")\n\t\t}\n\n\t\treport := websiteReport{Name: website, Report: response}\n\t\tif serviceV3 != nil {\n\t\t\treport.RealtimeReport = getLiveCount(serviceV3, viewID.(string))\n\t\t}\n\t\twebsiteReports = append(websiteReports, report)\n\t}\n\treturn websiteReports\n}\n\nfunc getLiveCount(service *gaV3.Service, viewID string) *gaV3.RealtimeData {\n\tres, err := service.Data.Realtime.Get(\"ga:\"+viewID, \"rt:activeUsers\").Do()\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to fetch real time data for view ID %s: %v.  Have you enrolled in the real time beta?  If not, do so here: https://docs.google.com/forms/d/1qfRFysCikpgCMGqgF3yXdUyQW4xAlLyjKuOoOEFN2Uw/viewform\", viewID, err)\n\t}\n\n\treturn res\n}\n"
  },
  {
    "path": "modules/googleanalytics/display.go",
    "content": "package googleanalytics\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc (widget *Widget) createTable(websiteReports []websiteReport) string {\n\tcontent := \"\"\n\n\tif len(websiteReports) == 0 {\n\t\treturn content\n\t}\n\n\tif websiteReports[0].RealtimeReport != nil {\n\t\tcontent += \"Realtime Visitor Counts\\n\"\n\t\tfor _, websiteReport := range websiteReports {\n\t\t\twebsiteRow := fmt.Sprintf(\" %-20s\", websiteReport.Name)\n\n\t\t\tif websiteReport.RealtimeReport == nil {\n\t\t\t\twebsiteRow += \"No data found for given ViewId\"\n\t\t\t} else {\n\t\t\t\tif len(websiteReport.RealtimeReport.Rows) == 0 {\n\t\t\t\t\twebsiteRow += \"-\"\n\t\t\t\t} else {\n\t\t\t\t\twebsiteRow += fmt.Sprintf(\"%-10s\", websiteReport.RealtimeReport.Rows[0][0])\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcontent += websiteRow + \"\\n\"\n\t\t}\n\n\t\tcontent += \"\\n\"\n\t\tcontent += \"Historical Visitor Counts\\n\"\n\t}\n\n\tcontent += widget.createHeader()\n\n\tfor _, websiteReport := range websiteReports {\n\t\twebsiteRow := \"\"\n\n\t\tfor _, report := range websiteReport.Report.Reports {\n\t\t\twebsiteRow += fmt.Sprintf(\" %-20s\", websiteReport.Name)\n\t\t\treportRows := report.Data.Rows\n\t\t\tnoDataMonth := widget.settings.months - len(reportRows)\n\n\t\t\t// Fill in requested months with no data from query\n\t\t\tif noDataMonth > 0 {\n\t\t\t\twebsiteRow += strings.Repeat(\"-         \", noDataMonth)\n\t\t\t}\n\n\t\t\tif reportRows == nil {\n\t\t\t\twebsiteRow += \"No data found for given ViewId\"\n\t\t\t} else {\n\t\t\t\tfor _, row := range reportRows {\n\t\t\t\t\tmetrics := row.Metrics\n\n\t\t\t\t\tfor _, metric := range metrics {\n\t\t\t\t\t\twebsiteRow += fmt.Sprintf(\"%-10s\", metric.Values[0])\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcontent += websiteRow + \"\\n\"\n\t\t}\n\t}\n\n\treturn content\n}\n\nfunc (widget *Widget) createHeader() string {\n\t// Creates the table header of consisting of Months\n\tcurrentMonth := int(time.Now().Month())\n\twidgetStartMonth := currentMonth - widget.settings.months + 1\n\theader := \"                     \"\n\n\tfor i := widgetStartMonth; i < currentMonth+1; i++ {\n\t\theader += fmt.Sprintf(\"%-10s\", time.Month(i))\n\t}\n\theader += \"\\n\"\n\n\treturn header\n}\n"
  },
  {
    "path": "modules/googleanalytics/settings.go",
    "content": "package googleanalytics\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Google Analytics\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tmonths         int\n\tsecretFile     string `help:\"Your Google client secret JSON file.\" values:\"A string representing a file path to the JSON secret file.\"`\n\tviewIds        map[string]interface{}\n\tenableRealtime bool\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tmonths:         ymlConfig.UInt(\"months\"),\n\t\tsecretFile:     ymlConfig.UString(\"secretFile\"),\n\t\tviewIds:        ymlConfig.UMap(\"viewIds\"),\n\t\tenableRealtime: ymlConfig.UBool(\"enableRealtime\", false),\n\t}\n\n\tsettings.SetDocumentationPath(\"google/analytics\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/googleanalytics/widget.go",
    "content": "package googleanalytics\n\nimport (\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\nfunc (widget *Widget) Refresh() {\n\twebsiteReports := widget.fetch()\n\tcontentTable := widget.createTable(websiteReports)\n\n\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, contentTable, false })\n}\n"
  },
  {
    "path": "modules/grafana/client.go",
    "content": "package grafana\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sort\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\ntype AlertState int\n\nconst (\n\tAlerting AlertState = iota\n\tPending\n\tNoData\n\tPaused\n\tOk\n)\n\nvar toString = map[AlertState]string{\n\tAlerting: \"alerting\",\n\tPending:  \"pending\",\n\tNoData:   \"no_data\",\n\tPaused:   \"paused\",\n\tOk:       \"ok\",\n}\n\nvar toID = map[string]AlertState{\n\t\"alerting\": Alerting,\n\t\"pending\":  Pending,\n\t\"no_data\":  NoData,\n\t\"paused\":   Paused,\n\t\"ok\":       Ok,\n}\n\n// MarshalJSON marshals the enum as a quoted json string\nfunc (s AlertState) MarshalJSON() ([]byte, error) {\n\tbuffer := bytes.NewBufferString(`\"`)\n\tbuffer.WriteString(toString[s])\n\tbuffer.WriteString(`\"`)\n\treturn buffer.Bytes(), nil\n}\n\n// UnmarshalJSON unmashals a quoted json string to the enum value\nfunc (s *AlertState) UnmarshalJSON(b []byte) error {\n\tvar j string\n\terr := json.Unmarshal(b, &j)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// if we somehow get an invalid value we'll end up in the alerting state\n\t*s = toID[j]\n\treturn nil\n}\n\ntype Alert struct {\n\tName  string     `json:\"name\"`\n\tState AlertState `json:\"state\"`\n\tURL   string     `json:\"url\"`\n}\n\ntype Client struct {\n\tapiKey  string\n\tbaseURI string\n}\n\nfunc NewClient(settings *Settings) *Client {\n\treturn &Client{\n\t\tapiKey:  settings.apiKey,\n\t\tbaseURI: settings.baseURI,\n\t}\n}\n\nfunc (client *Client) Alerts() ([]Alert, error) {\n\t// query the alerts API of Grafana https://grafana.com/docs/grafana/latest/http_api/alerting/\n\treq, err := http.NewRequest(\"GET\", fmt.Sprintf(\"%s/api/alerts\", client.baseURI), http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif client.apiKey != \"\" {\n\t\treq.Header.Add(\"Authorization\", fmt.Sprintf(\"Bearer %s\", client.apiKey))\n\t}\n\n\tres, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = res.Body.Close() }()\n\n\tif res.StatusCode != 200 {\n\t\tmsg := struct {\n\t\t\tMsg string `json:\"message\"`\n\t\t}{}\n\t\terr = utils.ParseJSON(&msg, res.Body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, errors.New(msg.Msg)\n\t}\n\n\tvar out []Alert\n\terr = utils.ParseJSON(&out, res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].State < out[j].State\n\t})\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "modules/grafana/display.go",
    "content": "package grafana\n\nimport \"fmt\"\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\n\tvar out string\n\tif widget.Err != nil {\n\t\treturn title, widget.Err.Error(), false\n\t} else {\n\t\tfor idx, alert := range widget.Alerts {\n\t\t\tout += fmt.Sprintf(` [\"%d\"][%s]%s - %s[\"\"]`,\n\t\t\t\tidx,\n\t\t\t\tstateColor(alert.State),\n\t\t\t\tstateToEmoji(alert.State),\n\t\t\t\talert.Name,\n\t\t\t)\n\t\t\tout += \"\\n\"\n\t\t}\n\t}\n\n\treturn title, out, false\n}\n\nfunc stateColor(state AlertState) string {\n\tswitch state {\n\tcase Ok:\n\t\treturn \"green\"\n\tcase Paused:\n\t\treturn \"yellow\"\n\tcase Alerting:\n\t\treturn \"red\"\n\tcase Pending:\n\t\treturn \"orange\"\n\tcase NoData:\n\t\treturn \"yellow\"\n\tdefault:\n\t\treturn \"white\"\n\t}\n}\n\nfunc stateToEmoji(state AlertState) string {\n\tswitch state {\n\tcase Ok:\n\t\treturn \"✔\"\n\tcase Paused:\n\t\treturn \"⏸\"\n\tcase Alerting:\n\t\treturn \"✘\"\n\tcase Pending:\n\t\treturn \"?\"\n\tcase NoData:\n\t\treturn \"?\"\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "modules/grafana/keyboard.go",
    "content": "package grafana\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous alert\")\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next alert\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openAlert, \"Open alert in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/grafana/settings.go",
    "content": "package grafana\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Grafana\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey  string `help:\"Your Grafana API token.\"`\n\tbaseURI string `help:\"Base url of your grafana instance\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:  ymlConfig.UString(\"apiKey\", os.Getenv(\"WTF_GRAFANA_API_KEY\")),\n\t\tbaseURI: ymlConfig.UString(\"baseUri\", \"\"),\n\t}\n\n\tif settings.baseURI == \"\" {\n\t\tlog.Fatal(\"baseUri for grafana is empty, but is required\")\n\t}\n\tsettings.baseURI = strings.TrimSuffix(settings.baseURI, \"/\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/grafana/widget.go",
    "content": "package grafana\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tClient   *Client\n\tAlerts   []Alert\n\tErr      error\n\tSelected int\n\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, _ *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tClient:   NewClient(settings),\n\t\tSelected: -1,\n\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\twidget.View.SetRegions(true)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\talerts, err := widget.Client.Alerts()\n\tif err != nil {\n\t\twidget.Err = err\n\t\twidget.Alerts = nil\n\t} else {\n\t\twidget.Err = nil\n\t\twidget.Alerts = alerts\n\t}\n\n\twidget.Redraw(widget.content)\n}\n\n// GetSelected returns the index of the currently highlighted item as an int\nfunc (widget *Widget) GetSelected() int {\n\tif widget.Selected < 0 {\n\t\treturn 0\n\t}\n\treturn widget.Selected\n}\n\n// Next cycles the currently highlighted text down\nfunc (widget *Widget) Next() {\n\twidget.Selected++\n\tif widget.Selected >= len(widget.Alerts) {\n\t\twidget.Selected = 0\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Prev cycles the currently highlighted text up\nfunc (widget *Widget) Prev() {\n\twidget.Selected--\n\tif widget.Selected < 0 {\n\t\twidget.Selected = len(widget.Alerts) - 1\n\t}\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n\n// Unselect stops highlighting the text and jumps the scroll position to the top\nfunc (widget *Widget) Unselect() {\n\twidget.Selected = -1\n\twidget.View.Highlight()\n\twidget.View.ScrollToBeginning()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) openAlert() {\n\tcurrentSelection := widget.View.GetHighlights()\n\tif widget.Selected >= 0 && currentSelection[0] != \"\" {\n\t\turl := widget.Alerts[widget.GetSelected()].URL\n\t\tif url[0] == '/' {\n\t\t\turl = fmt.Sprintf(\"%s%s\", widget.settings.baseURI, url)\n\t\t}\n\t\tutils.OpenFile(url)\n\t}\n}\n"
  },
  {
    "path": "modules/gspreadsheets/client.go",
    "content": "/*\n* This butt-ugly code is direct from Google itself\n* https://developers.google.com/sheets/api/quickstart/go\n */\n\npackage gspreadsheets\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n\t\"google.golang.org/api/option\"\n\tsheets \"google.golang.org/api/sheets/v4\"\n)\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Fetch() ([]*sheets.ValueRange, error) {\n\tctx := context.Background()\n\n\tsecretPath, _ := utils.ExpandHomeDir(widget.settings.secretFile)\n\n\tb, err := os.ReadFile(filepath.Clean(secretPath))\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to read secretPath. %v\", err)\n\t\treturn nil, err\n\t}\n\n\tconfig, err := google.ConfigFromJSON(b, \"https://www.googleapis.com/auth/spreadsheets.readonly\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := getClient(ctx, config)\n\n\tsrv, err := sheets.NewService(context.Background(), option.WithHTTPClient(client))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcells := utils.ToStrs(widget.settings.cellAddresses)\n\n\tresponses := make([]*sheets.ValueRange, len(cells))\n\n\tfor i := 0; i < len(cells); i++ {\n\t\tresp, getErr := srv.Spreadsheets.Values.Get(widget.settings.sheetID, cells[i]).Do()\n\t\tif getErr != nil {\n\t\t\treturn nil, getErr\n\t\t}\n\t\tresponses[i] = resp\n\t}\n\n\treturn responses, err\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// getClient uses a Context and Config to retrieve a Token\n// then generate a Client. It returns the generated Client.\nfunc getClient(ctx context.Context, config *oauth2.Config) *http.Client {\n\tcacheFile, err := tokenCacheFile()\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to get path to cached credential file. %v\", err)\n\t}\n\ttok, err := tokenFromFile(cacheFile)\n\tif err != nil {\n\t\ttok = getTokenFromWeb(config)\n\t\tsaveToken(cacheFile, tok)\n\t}\n\treturn config.Client(ctx, tok)\n}\n\n// getTokenFromWeb uses Config to request a Token.\n// It returns the retrieved Token.\nfunc getTokenFromWeb(config *oauth2.Config) *oauth2.Token {\n\tauthURL := config.AuthCodeURL(\"state-token\", oauth2.AccessTypeOffline)\n\tfmt.Printf(\"Go to the following link in your browser then type the \"+\n\t\t\"authorization code: \\n%v\\n\", authURL)\n\n\tvar code string\n\tif _, err := fmt.Scan(&code); err != nil {\n\t\tlog.Fatalf(\"Unable to read authorization code %v\", err)\n\t}\n\n\ttok, err := config.Exchange(context.Background(), code)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to retrieve token from web %v\", err)\n\t}\n\treturn tok\n}\n\n// tokenCacheFile generates credential file path/filename.\n// It returns the generated credential path/filename.\nfunc tokenCacheFile() (string, error) {\n\tusr, err := user.Current()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\ttokenCacheDir := filepath.Join(usr.HomeDir, \".credentials\")\n\terr = os.MkdirAll(tokenCacheDir, 0700)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn filepath.Join(tokenCacheDir, url.QueryEscape(\"spreadsheets-go-quickstart.json\")), err\n}\n\n// tokenFromFile retrieves a Token from a given file path.\n// It returns the retrieved Token and any read error encountered.\nfunc tokenFromFile(file string) (*oauth2.Token, error) {\n\tf, err := os.Open(filepath.Clean(file))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tt := &oauth2.Token{}\n\terr = json.NewDecoder(f).Decode(t)\n\tdefer func() { _ = f.Close() }()\n\treturn t, err\n}\n\n// saveToken uses a file path to create a file and store the\n// token in it.\nfunc saveToken(file string, token *oauth2.Token) {\n\tfmt.Printf(\"Saving credential file to: %s\\n\", file)\n\tf, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to cache oauth token: %v\", err)\n\t}\n\tdefer func() { _ = f.Close() }()\n\n\terr = json.NewEncoder(f).Encode(token)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to encode oauth token: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "modules/gspreadsheets/settings.go",
    "content": "package gspreadsheets\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Google Spreadsheets\"\n)\n\ntype colors struct {\n\tvalues string\n}\n\ntype Settings struct {\n\tcolors\n\t*cfg.Common\n\n\tcellAddresses []interface{}\n\tcellNames     []interface{}\n\tsecretFile    string\n\tsheetID       string\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tcellNames:  ymlConfig.UList(\"cells.names\"),\n\t\tsecretFile: ymlConfig.UString(\"secretFile\"),\n\t\tsheetID:    ymlConfig.UString(\"sheetId\"),\n\t}\n\n\tsettings.values = ymlConfig.UString(\"colors.values\", \"green\")\n\n\tsettings.SetDocumentationPath(\"google/spreadsheet\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/gspreadsheets/widget.go",
    "content": "package gspreadsheets\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\tsheets \"google.golang.org/api/sheets/v4\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n\tcells    []*sheets.ValueRange\n\terr      error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tcells, err := widget.Fetch()\n\twidget.err = err\n\twidget.cells = cells\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif widget.cells == nil {\n\t\treturn title, \"No cells\", false\n\t}\n\n\tres := \"\"\n\n\tcells := utils.ToStrs(widget.settings.cellNames)\n\tfor i := 0; i < len(widget.cells); i++ {\n\t\tres += fmt.Sprintf(\"%s\\t[%s]%s\\n\", cells[i], widget.settings.values, widget.cells[i].Values[0][0])\n\t}\n\n\treturn title, res, false\n}\n"
  },
  {
    "path": "modules/hackernews/client.go",
    "content": "package hackernews\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc GetStories(storyType string) ([]int, error) {\n\tvar storyIds []int\n\n\tswitch strings.ToLower(storyType) {\n\tcase \"new\", \"top\", \"job\", \"ask\":\n\t\tresp, err := apiRequest(storyType + \"stories\")\n\t\tif err != nil {\n\t\t\treturn storyIds, err\n\t\t}\n\n\t\terr = utils.ParseJSON(&storyIds, bytes.NewReader(resp))\n\t\tif err != nil {\n\t\t\treturn storyIds, err\n\t\t}\n\t}\n\n\treturn storyIds, nil\n}\n\nfunc GetStory(id int) (Story, error) {\n\tvar story Story\n\n\tresp, err := apiRequest(\"item/\" + strconv.Itoa(id))\n\tif err != nil {\n\t\treturn story, err\n\t}\n\n\terr = utils.ParseJSON(&story, bytes.NewReader(resp))\n\tif err != nil {\n\t\treturn story, err\n\t}\n\n\treturn story, nil\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nvar (\n\tapiEndpoint = \"https://hacker-news.firebaseio.com/v0/\"\n)\n\nfunc apiRequest(path string) ([]byte, error) {\n\treq, err := http.NewRequest(\"GET\", apiEndpoint+path+\".json\", http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn body, nil\n}\n"
  },
  {
    "path": "modules/hackernews/keyboard.go",
    "content": "package hackernews\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openStory, \"Open story in browser\")\n\twidget.SetKeyboardChar(\"c\", widget.openComments, \"Open comments in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openStory, \"Open story in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/hackernews/settings.go",
    "content": "package hackernews\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"HackerNews\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tnumberOfStories int    `help:\"Defines number of stories to be displayed. Default is 10\" optional:\"true\"`\n\tstoryType       string `help:\"Category of story to see\" values:\"new, top, job, ask\" optional:\"true\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tnumberOfStories: ymlConfig.UInt(\"numberOfStories\", 10),\n\t\tstoryType:       ymlConfig.UString(\"storyType\", \"top\"),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/hackernews/story.go",
    "content": "package hackernews\n\nimport \"fmt\"\n\nconst (\n\thnStoryPath = \"https://news.ycombinator.com/item?id=\"\n)\n\n// Story represents a story submission on HackerNews\ntype Story struct {\n\tBy          string `json:\"by\"`\n\tDescendants int    `json:\"descendants\"`\n\tID          int    `json:\"id\"`\n\tKids        []int  `json:\"kids\"`\n\tScore       int    `json:\"score\"`\n\tTime        int    `json:\"time\"`\n\tTitle       string `json:\"title\"`\n\tType        string `json:\"type\"`\n\tURL         string `json:\"url\"`\n}\n\n// CommentLink return the link to the HackerNews story comments page\nfunc (story *Story) CommentLink() string {\n\treturn fmt.Sprintf(\"%s%d\", hnStoryPath, story.ID)\n}\n\n// Link returns the link to a story. If the story has an external link, that is returned\n// If the story has no external link, the HackerNews comments link is returned instead\nfunc (story *Story) Link() string {\n\tif story.URL != \"\" {\n\t\treturn story.URL\n\t}\n\n\t// Fall back to the HackerNews comment link\n\treturn story.CommentLink()\n}\n"
  },
  {
    "path": "modules/hackernews/story_test.go",
    "content": "package hackernews\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/assert\"\n)\n\nfunc Test_CommentLink(t *testing.T) {\n\tstory := Story{\n\t\tID: 3,\n\t}\n\n\tassert.Equal(t, \"https://news.ycombinator.com/item?id=3\", story.CommentLink())\n}\n\nfunc Test_Link(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tid       int\n\t\turl      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no external link\",\n\t\t\tid:       1,\n\t\t\turl:      \"\",\n\t\t\texpected: \"https://news.ycombinator.com/item?id=1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with external link\",\n\t\t\tid:       1,\n\t\t\turl:      \"https://www.link.ca\",\n\t\t\texpected: \"https://www.link.ca\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstory := Story{\n\t\t\t\tID:  tt.id,\n\t\t\t\tURL: tt.url,\n\t\t\t}\n\n\t\t\tactual := story.Link()\n\n\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/hackernews/widget.go",
    "content": "package hackernews\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tstories  []Story\n\tsettings *Settings\n\terr      error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\tstoryIds, err := GetStories(widget.settings.storyType)\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.stories = nil\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\tvar stories []Story\n\t\tfor idx := 0; idx < widget.settings.numberOfStories; idx++ {\n\t\t\tstory, e := GetStory(storyIds[idx])\n\t\t\tif e == nil {\n\t\t\t\tstories = append(stories, story)\n\t\t\t}\n\t\t}\n\t\twidget.stories = stories\n\t\twidget.SetItemCount(len(stories))\n\t}\n\n\twidget.Render()\n}\n\n// Render sets up the widget data for redrawing to the screen\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := fmt.Sprintf(\"%s - %s stories\", widget.CommonSettings().Title, widget.settings.storyType)\n\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif len(widget.stories) == 0 {\n\t\treturn title, \"No stories to display\", false\n\t}\n\n\tvar str string\n\tfor idx, story := range widget.stories {\n\t\tu, _ := url.Parse(story.URL)\n\n\t\trow := fmt.Sprintf(\n\t\t\t`[%s]%2d. %s [lightblue](%s)[white]`,\n\t\t\twidget.RowColor(idx),\n\t\t\tidx+1,\n\t\t\tstory.Title,\n\t\t\tstrings.TrimPrefix(u.Host, \"www.\"),\n\t\t)\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(story.Title))\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) openComments() {\n\tstory := widget.selectedStory()\n\tif story != nil {\n\t\tutils.OpenFile(story.CommentLink())\n\t}\n}\n\nfunc (widget *Widget) openStory() {\n\tstory := widget.selectedStory()\n\tif story != nil {\n\t\tutils.OpenFile(story.Link())\n\t}\n}\n\nfunc (widget *Widget) selectedStory() *Story {\n\tvar story *Story\n\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.stories != nil && sel < len(widget.stories) {\n\t\tstory = &widget.stories[sel]\n\t}\n\n\treturn story\n}\n"
  },
  {
    "path": "modules/healthchecks/keyboard.go",
    "content": "package healthchecks\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n}\n"
  },
  {
    "path": "modules/healthchecks/settings.go",
    "content": "package healthchecks\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Healthchecks.io\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey string   `help:\"An healthchecks API key.\" optional:\"false\"`\n\tapiURL string   `help:\"Base URL for API\" optional:\"true\"`\n\ttags   []string `help:\"Filters the checks and returns only the checks that are tagged with the specified value\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey: ymlConfig.UString(\"apiKey\", os.Getenv(\"WTF_HEALTHCHECKS_APIKEY\")),\n\t\tapiURL: ymlConfig.UString(\"apiURL\", \"https://hc-ping.com/\"),\n\t\ttags:   utils.ToStrs(ymlConfig.UList(\"tags\")),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).\n\t\tService(settings.apiURL).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/healthchecks/widget.go",
    "content": "package healthchecks\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst (\n\tuserAgent = \"WTFUtil\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\tchecks   []Checks\n\tsettings *Settings\n\terr      error\n}\n\ntype Health struct {\n\tChecks []Checks `json:\"checks\"`\n}\n\ntype Checks struct {\n\tName         string    `json:\"name\"`\n\tTags         string    `json:\"tags\"`\n\tDesc         string    `json:\"desc\"`\n\tGrace        int       `json:\"grace\"`\n\tNPings       int       `json:\"n_pings\"`\n\tStatus       string    `json:\"status\"`\n\tLastPing     time.Time `json:\"last_ping\"`\n\tNextPing     time.Time `json:\"next_ping\"`\n\tManualResume bool      `json:\"manual_resume\"`\n\tMethods      string    `json:\"methods\"`\n\tPingURL      string    `json:\"ping_url\"`\n\tUpdateURL    string    `json:\"update_url\"`\n\tPauseURL     string    `json:\"pause_url\"`\n\tChannels     string    `json:\"channels\"`\n\tTimeout      int       `json:\"timeout,omitempty\"`\n\tSchedule     string    `json:\"schedule,omitempty\"`\n\tTz           string    `json:\"tz,omitempty\"`\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\t\tsettings:         settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\tchecks, err := widget.getExistingChecks()\n\twidget.checks = checks\n\twidget.err = err\n\twidget.SetItemCount(len(checks))\n\twidget.Render()\n}\n\n// Render sets up the widget data for redrawing to the screen\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tnumUp := 0\n\tfor _, check := range widget.checks {\n\t\tif check.Status == \"up\" {\n\t\t\tnumUp++\n\t\t}\n\t}\n\n\ttitle := fmt.Sprintf(\"%v (%d/%d)\", widget.CommonSettings().Title, numUp, len(widget.checks))\n\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif widget.checks == nil {\n\t\treturn title, \"No checks to display\", false\n\t}\n\n\tstr := widget.contentFrom(widget.checks)\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) contentFrom(checks []Checks) string {\n\tvar str string\n\n\tfor _, check := range checks {\n\t\tprefix := \"\"\n\n\t\tswitch check.Status {\n\t\tcase \"up\":\n\t\t\tprefix += \"[green] + \"\n\t\tcase \"down\":\n\t\t\tprefix += \"[red] - \"\n\t\tcase \"paused\", \"new\":\n\t\t\tprefix += \"[lightgray] × \"\n\t\tdefault:\n\t\t\tprefix += \"[yellow] ~ \"\n\t\t}\n\n\t\tstr += fmt.Sprintf(`%s%s [gray](%s|%d)[white]%s`,\n\t\t\tprefix,\n\t\t\tcheck.Name,\n\t\t\ttimeSincePing(check.LastPing),\n\t\t\tcheck.NPings,\n\t\t\t\"\\n\",\n\t\t)\n\t}\n\n\treturn str\n}\n\nfunc timeSincePing(ts time.Time) string {\n\tdur := time.Since(ts)\n\treturn dur.Truncate(time.Second).String()\n}\n\nfunc makeURL(baseurl string, path string, tags []string) (string, error) {\n\tu, err := url.Parse(baseurl)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tu.Path = path\n\tq := u.Query()\n\t// If we have several tags\n\tif len(tags) > 0 {\n\t\tfor _, tag := range tags {\n\t\t\tq.Add(\"tag\", tag)\n\t\t}\n\t\tu.RawQuery = q.Encode()\n\t}\n\treturn u.String(), nil\n}\n\nfunc (widget *Widget) getExistingChecks() ([]Checks, error) {\n\t// See: https://healthchecks.io/docs/api/#list-checks\n\tu, err := makeURL(widget.settings.apiURL, \"/api/v1/checks/\", widget.settings.tags)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq, err := http.NewRequest(\"GET\", u, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"User-Agent\", userAgent)\n\treq.Header.Set(\"X-Api-Key\", widget.settings.apiKey)\n\tresp, err := http.DefaultClient.Do(req)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tvar health Health\n\terr = utils.ParseJSON(&health, resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn health.Checks, nil\n}\n"
  },
  {
    "path": "modules/hibp/client.go",
    "content": "package hibp\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n)\n\nconst (\n\tapiURL            = \"https://haveibeenpwned.com/api/v3/breachedaccount/\"\n\tclientTimeoutSecs = 2\n\tuserAgent         = \"WTFUtil\"\n)\n\ntype hibpError struct {\n\tStatusCode int    `json:\"statusCode\"`\n\tMessage    string `json:\"message\"`\n}\n\nfunc (widget *Widget) fullURL(account string, truncated bool) string {\n\ttruncStr := \"false\"\n\tif truncated {\n\t\ttruncStr = \"true\"\n\t}\n\n\treturn apiURL + account + fmt.Sprintf(\"?truncateResponse=%s\", truncStr)\n}\n\nfunc (widget *Widget) fetchForAccount(account string, since string) (*Status, error) {\n\tif account == \"\" {\n\t\treturn nil, nil\n\t}\n\n\thibpClient := http.Client{\n\t\tTimeout: time.Second * clientTimeoutSecs,\n\t}\n\n\tasTruncated := since == \"\"\n\n\trequest, err := http.NewRequest(http.MethodGet, widget.fullURL(account, asTruncated), http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequest.Header.Set(\"User-Agent\", userAgent)\n\trequest.Header.Set(\"hibp-api-key\", widget.settings.apiKey)\n\n\tresponse, getErr := hibpClient.Do(request)\n\tif getErr != nil {\n\t\treturn nil, err\n\t}\n\n\tbody, readErr := io.ReadAll(response.Body)\n\tif readErr != nil {\n\t\treturn nil, err\n\t}\n\n\thibpErr := widget.validateHTTPResponse(response.StatusCode, body)\n\tif hibpErr != nil {\n\t\treturn nil, errors.New(hibpErr.Message)\n\t}\n\n\tstat, err := widget.parseResponseBody(account, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn stat, nil\n}\n\nfunc (widget *Widget) parseResponseBody(account string, body []byte) (*Status, error) {\n\tbreaches := []Breach{}\n\tstat := NewStatus(account, breaches)\n\n\tif len(body) == 0 {\n\t\t// If the body is empty then there's no breaches\n\t\treturn stat, nil\n\t}\n\n\tjsonErr := json.Unmarshal(body, &breaches)\n\tif jsonErr != nil {\n\t\treturn stat, jsonErr\n\t}\n\n\tbreaches = widget.filterBreaches(breaches)\n\tstat.Breaches = breaches\n\n\treturn stat, nil\n}\n\nfunc (widget *Widget) filterBreaches(breaches []Breach) []Breach {\n\t// If there's no valid since value in the settings, there's no point in trying to filter\n\t// the breaches on that value, they'll all pass\n\tif !widget.settings.HasSince() {\n\t\treturn breaches\n\t}\n\n\tsinceDate, err := widget.settings.SinceDate()\n\tif err != nil {\n\t\treturn breaches\n\t}\n\n\tlatestBreaches := []Breach{}\n\n\tfor _, breach := range breaches {\n\t\tbreachDate, err := breach.BreachDate()\n\t\tif err != nil {\n\t\t\t// Append the erring breach here because a failing breach date doesn't mean that\n\t\t\t// the breach itself isn't applicable. The date could be missing or malformed,\n\t\t\t// in which case we err on the side of caution and assume that the breach is valid\n\t\t\tlatestBreaches = append(latestBreaches, breach)\n\t\t\tcontinue\n\t\t}\n\n\t\tif breachDate.After(sinceDate) {\n\t\t\tlatestBreaches = append(latestBreaches, breach)\n\t\t}\n\t}\n\n\treturn latestBreaches\n}\n\nfunc (widget *Widget) validateHTTPResponse(responseCode int, body []byte) *hibpError {\n\thibpErr := &hibpError{}\n\n\tswitch responseCode {\n\tcase 401, 402:\n\t\terr := json.Unmarshal(body, hibpErr)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\tdefault:\n\t\thibpErr = nil\n\t}\n\n\treturn hibpErr\n}\n"
  },
  {
    "path": "modules/hibp/hibp_breach.go",
    "content": "package hibp\n\nimport \"time\"\n\n// Breach represents a breach in the HIBP system\ntype Breach struct {\n\tDate string `json:\"BreachDate\"`\n\tName string `json:\"Name\"`\n}\n\n// BreachDate returns the date of the breach\nfunc (br *Breach) BreachDate() (time.Time, error) {\n\tdt, err := time.Parse(\"2006-01-02\", br.Date)\n\tif err != nil {\n\t\t// I would much rather return (nil, err) err but that doesn't seem possible\n\t\t// Not sure what a better value would be\n\t\treturn time.Now(), err\n\t}\n\n\treturn dt, nil\n}\n"
  },
  {
    "path": "modules/hibp/hibp_status.go",
    "content": "package hibp\n\n// Status represents the status of an account in the HIBP system\ntype Status struct {\n\tAccount  string\n\tBreaches []Breach\n}\n\n// NewStatus creates and returns an instance of Status\nfunc NewStatus(acct string, breaches []Breach) *Status {\n\tstat := Status{\n\t\tAccount:  acct,\n\t\tBreaches: breaches,\n\t}\n\n\treturn &stat\n}\n\n// HasBeenCompromised returns TRUE if the specified account has any breaches associated\n// with it, FALSE if no breaches are associated with it\nfunc (stat *Status) HasBeenCompromised() bool {\n\treturn stat.Len() > 0\n}\n\n// Len returns the number of breaches found for the specified account\nfunc (stat *Status) Len() int {\n\tif stat == nil || stat.Breaches == nil {\n\t\treturn 0\n\t}\n\n\treturn len(stat.Breaches)\n}\n"
  },
  {
    "path": "modules/hibp/settings.go",
    "content": "package hibp\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable   = false\n\tdefaultTitle       = \"HIBP\"\n\tminRefreshInterval = 6 * time.Hour\n)\n\ntype colors struct {\n\tok    string\n\tpwned string\n}\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\tcolors\n\t*cfg.Common\n\n\taccounts []string `help:\"A list of the accounts to check the HIBP database for.\"`\n\tapiKey   string   `help:\"Your HIBP API v3 API key\"`\n\tsince    string   `help:\"Only check for breaches after this date. Set this if you’ve been breached in the past, have taken steps to mitigate that (changing passwords, cancelling accounts, etc.) and now only want to know about future breaches.\" values:\"A date string in the format 'yyyy-mm-dd', ie. '2019-06-22'\" optional:\"true\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := &Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:   ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_HIBP_TOKEN\"))),\n\t\taccounts: utils.ToStrs(ymlConfig.UList(\"accounts\")),\n\t\tsince:    ymlConfig.UString(\"since\", \"\"),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\tsettings.ok = ymlConfig.UString(\"colors.ok\", \"white\")\n\tsettings.pwned = ymlConfig.UString(\"colors.pwned\", \"red\")\n\n\t// HIBP data doesn't need to be reloaded very often so to be gentle on this API we\n\t// enforce a minimum refresh interval\n\tif settings.RefreshInterval < minRefreshInterval {\n\t\tsettings.RefreshInterval = minRefreshInterval\n\t}\n\n\treturn settings\n}\n\n// HasSince returns TRUE if there's a valid \"since\" value setting, FALSE if there is not\nfunc (sett *Settings) HasSince() bool {\n\tif sett.since == \"\" {\n\t\treturn false\n\t}\n\n\t_, err := sett.SinceDate()\n\treturn err == nil\n}\n\n// SinceDate returns the \"since\" settings as a proper Time instance\nfunc (sett *Settings) SinceDate() (time.Time, error) {\n\tdt, err := time.Parse(\"2006-01-02\", sett.since)\n\tif err != nil {\n\t\treturn time.Now(), err\n\t}\n\n\treturn dt, nil\n}\n"
  },
  {
    "path": "modules/hibp/widget.go",
    "content": "package hibp\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget is the container for hibp data\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n\tstatuses []*Status\n\terr      error\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Fetch retrieves HIBP data from the HIBP API\nfunc (widget *Widget) Fetch(accounts []string) ([]*Status, error) {\n\tdata := []*Status{}\n\n\tfor _, account := range accounts {\n\t\tstat, err := widget.fetchForAccount(account, widget.settings.since)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdata = append(data, stat)\n\t}\n\n\treturn data, nil\n}\n\n// Refresh updates the data for this widget and displays it onscreen\nfunc (widget *Widget) Refresh() {\n\tstatuses, err := widget.Fetch(widget.settings.accounts)\n\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.statuses = nil\n\t} else {\n\t\twidget.err = nil\n\t\twidget.statuses = statuses\n\t}\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\ttitle += widget.sinceDateForTitle()\n\tstr := \"\"\n\n\tfor _, status := range widget.statuses {\n\t\tcolor := widget.settings.ok\n\n\t\tif status.HasBeenCompromised() {\n\t\t\tcolor = widget.settings.pwned\n\t\t}\n\n\t\tif status != nil {\n\t\t\tstr += fmt.Sprintf(\" [%s]%s[white]\\n\", color, status.Account)\n\t\t}\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) sinceDateForTitle() string {\n\tdateStr := \"\"\n\n\tif widget.settings.HasSince() {\n\t\tsinceStr := \"\"\n\n\t\tdt, err := widget.settings.SinceDate()\n\t\tif err != nil {\n\t\t\tsinceStr = widget.settings.since\n\t\t} else {\n\t\t\tsinceStr = dt.Format(\"Jan _2, 2006\")\n\t\t}\n\n\t\tdateStr = dateStr + \" since \" + sinceStr\n\t}\n\n\treturn dateStr\n}\n"
  },
  {
    "path": "modules/ipaddresses/ipapi/settings.go",
    "content": "package ipapi\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"IP API\"\n)\n\ntype colors struct {\n\tname  string\n\tvalue string\n}\n\ntype Settings struct {\n\tcolors\n\t*cfg.Common\n\targs []interface{} `help:\"Defines what data to display and the order.\" values:\"'ip', 'isp', 'as', 'asName', 'district', 'city', 'region', 'regionName', 'country', 'countryCode', 'continent', 'continentCode', 'coordinates', 'postalCode', 'currency', 'organization', 'timezone' and/or 'reverseDNS'\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\targs: ymlConfig.UList(\"args\"),\n\t}\n\n\tsettings.name = ymlConfig.UString(\"colors.name\", \"red\")\n\tsettings.value = ymlConfig.UString(\"colors.value\", \"white\")\n\tsettings.SetDocumentationPath(\"ipaddress/ipapi\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/ipaddresses/ipapi/widget.go",
    "content": "package ipapi\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget widget struct\ntype Widget struct {\n\tview.TextWidget\n\n\tresult   string\n\tsettings *Settings\n}\n\ntype ipinfo struct {\n\tQuery         string  `json:\"query\"`\n\tISP           string  `json:\"isp\"`\n\tAS            string  `json:\"as\"`\n\tASName        string  `json:\"asname\"`\n\tDistrict      string  `json:\"district\"`\n\tCity          string  `json:\"city\"`\n\tRegion        string  `json:\"region\"`\n\tRegionName    string  `json:\"regionName\"`\n\tCountry       string  `json:\"country\"`\n\tCountryCode   string  `json:\"countryCode\"`\n\tContinent     string  `json:\"continent\"`\n\tContinentCode string  `json:\"continentCode\"`\n\tLatitude      float64 `json:\"lat\"`\n\tLongitude     float64 `json:\"lon\"`\n\tPostalCode    string  `json:\"zip\"`\n\tCurrency      string  `json:\"currency\"`\n\tOrganization  string  `json:\"org\"`\n\tTimezone      string  `json:\"timezone\"`\n\tReverseDNS    string  `json:\"reverse\"`\n}\n\nvar argLookup = map[string]string{\n\t\"ip\":            \"IP Address\",\n\t\"isp\":           \"ISP\",\n\t\"as\":            \"AS\",\n\t\"asname\":        \"AS Name\",\n\t\"district\":      \"District\",\n\t\"city\":          \"City\",\n\t\"region\":        \"Region\",\n\t\"regionname\":    \"Region Name\",\n\t\"country\":       \"Country\",\n\t\"countrycode\":   \"Country Code\",\n\t\"continent\":     \"Continent\",\n\t\"continentcode\": \"Continent Code\",\n\t\"coordinates\":   \"Coordinates\",\n\t\"postalcode\":    \"Postal Code\",\n\t\"currency\":      \"Currency\",\n\t\"organization\":  \"Organization\",\n\t\"timezone\":      \"Timezone\",\n\t\"reversedns\":    \"Reverse DNS\",\n}\n\n// NewWidget constructor\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.View.SetWrap(false)\n\n\treturn &widget\n}\n\n// Refresh refresh the module\nfunc (widget *Widget) Refresh() {\n\twidget.ipinfo()\n\n\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, widget.result, false })\n}\n\n// this method reads the config and calls ipinfo for ip information\nfunc (widget *Widget) ipinfo() {\n\tclient := &http.Client{}\n\treq, err := http.NewRequest(\"GET\", \"http://ip-api.com/json?fields=66846719\", http.NoBody)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\treq.Header.Set(\"User-Agent\", \"curl\")\n\tresponse, err := client.Do(req)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\tdefer func() { _ = response.Body.Close() }()\n\tvar info ipinfo\n\terr = json.NewDecoder(response.Body).Decode(&info)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\n\twidget.setResult(&info)\n}\n\nfunc (widget *Widget) setResult(info *ipinfo) {\n\n\targs := utils.ToStrs(widget.settings.args)\n\n\t// if no arguments are defined set default\n\tif len(args) == 0 {\n\t\targs = []string{\"ip\", \"isp\", \"as\", \"city\", \"region\", \"country\", \"coordinates\", \"postalCode\", \"organization\", \"timezone\"}\n\t}\n\n\tformat := \"\"\n\n\tfor _, arg := range args {\n\t\tif val, ok := argLookup[strings.ToLower(arg)]; ok {\n\t\t\tformat = format + formatableText(val, strings.ToLower(arg))\n\t\t}\n\t}\n\n\tresultTemplate, _ := template.New(\"ipinfo_result\").Parse(format)\n\n\tresultBuffer := new(bytes.Buffer)\n\n\terr := resultTemplate.Execute(resultBuffer, map[string]string{\n\t\t\"nameColor\":     widget.settings.name,\n\t\t\"valueColor\":    widget.settings.value,\n\t\t\"ip\":            info.Query,\n\t\t\"isp\":           info.ISP,\n\t\t\"as\":            info.AS,\n\t\t\"asname\":        info.ASName,\n\t\t\"district\":      info.District,\n\t\t\"city\":          info.City,\n\t\t\"region\":        info.Region,\n\t\t\"regionname\":    info.RegionName,\n\t\t\"country\":       info.Country,\n\t\t\"countrycode\":   info.CountryCode,\n\t\t\"continent\":     info.Continent,\n\t\t\"continentcode\": info.ContinentCode,\n\t\t\"coordinates\":   strconv.FormatFloat(info.Latitude, 'f', 6, 64) + \",\" + strconv.FormatFloat(info.Longitude, 'f', 6, 64),\n\t\t\"postalcode\":    info.PostalCode,\n\t\t\"currency\":      info.Currency,\n\t\t\"organization\":  info.Organization,\n\t\t\"timezone\":      info.Timezone,\n\t\t\"reversedns\":    info.ReverseDNS,\n\t})\n\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t}\n\n\twidget.result = resultBuffer.String()\n}\n\nfunc formatableText(key, value string) string {\n\treturn fmt.Sprintf(\" [{{.nameColor}}]%s: [{{.valueColor}}]{{.%s}}\\n\", key, value)\n}\n"
  },
  {
    "path": "modules/ipaddresses/ipinfo/settings.go",
    "content": "package ipinfo\n\nimport (\n\t\"fmt\"\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\tlog \"github.com/wtfutil/wtf/logger\"\n)\n\nconst (\n\tdefaultFocusable                 = false\n\tdefaultTitle                     = \"IPInfo\"\n\tipV4             protocolVersion = \"v4\"\n\tipV6             protocolVersion = \"v6\"\n\tauto             protocolVersion = \"auto\"\n)\n\ntype protocolVersion string\n\nfunc (pv protocolVersion) String() string {\n\tswitch pv {\n\tcase ipV4:\n\t\treturn \"v4\"\n\tcase ipV6:\n\t\treturn \"v6\"\n\tdefault:\n\t\treturn \"auto\"\n\t}\n}\n\nfunc newProtocolVersion(str string) (protocolVersion, error) {\n\tswitch str {\n\tcase \"v4\":\n\t\treturn ipV4, nil\n\tcase \"v6\":\n\t\treturn ipV6, nil\n\tcase \"auto\":\n\t\treturn auto, nil\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"%s module: Unsupported protocol version: '%s'\", defaultTitle, str)\n\t}\n}\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiToken        string          `help:\"An api token\" optional:\"true\"`\n\tprotocolVersion protocolVersion `help:\"IP protocol version to display. Possible options are: 'v4' to show only IpV4 address, 'v6' to show only IpV6 address and 'auto' (default) to show the address preferred by OS.\" optional:\"true\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiToken:        ymlConfig.UString(\"apiToken\", \"\"),\n\t\tprotocolVersion: auto,\n\t}\n\n\tpv, err := newProtocolVersion(ymlConfig.UString(\"protocolVersion\", auto.String()))\n\tif err != nil {\n\t\tlog.Log(err.Error())\n\t\tlog.Log(fmt.Sprintf(\"%s module: Use '%s' protocol version as a default\", defaultTitle, auto))\n\t} else {\n\t\tsettings.protocolVersion = pv\n\t}\n\n\tsettings.SetDocumentationPath(\"ipaddress/ipinfo\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/ipaddresses/ipinfo/widget.go",
    "content": "package ipinfo\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\tlog \"github.com/wtfutil/wtf/logger\"\n\t\"net\"\n\t\"net/http\"\n\t\"text/template\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tresult   string\n\tsettings *Settings\n}\n\ntype ipinfo struct {\n\tIp           string `json:\"ip\"`\n\tHostname     string `json:\"hostname\"`\n\tCity         string `json:\"city\"`\n\tRegion       string `json:\"region\"`\n\tCountry      string `json:\"country\"`\n\tCoordinates  string `json:\"loc\"`\n\tPostalCode   string `json:\"postal\"`\n\tOrganization string `json:\"org\"`\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.View.SetWrap(false)\n\n\treturn &widget\n}\n\nfunc (widget *Widget) Refresh() {\n\twidget.ipinfo()\n\n\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, widget.result, false })\n}\n\n// this method reads the config and calls ipinfo for ip information\nfunc (widget *Widget) ipinfo() {\n\tclient := &http.Client{}\n\tvar url string\n\tip, ipv6 := getMyIP(widget.settings.protocolVersion)\n\tif ipv6 {\n\t\turl = fmt.Sprintf(\"https://ipinfo.io/%s\", ip.String())\n\t} else {\n\t\turl = \"https://ipinfo.io/\"\n\t}\n\n\treq, err := http.NewRequest(\"GET\", url, http.NoBody)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\treq.Header.Set(\"User-Agent\", \"curl\")\n\tif widget.settings.apiToken != \"\" {\n\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", widget.settings.apiToken))\n\t}\n\n\tresponse, err := client.Do(req)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\tdefer func() { _ = response.Body.Close() }()\n\n\tvar info ipinfo\n\terr = json.NewDecoder(response.Body).Decode(&info)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\n\twidget.setResult(&info)\n}\n\nfunc (widget *Widget) setResult(info *ipinfo) {\n\tresultTemplate, _ := template.New(\"ipinfo_result\").Parse(\n\t\tformatableText(\"IP\", \"Ip\") +\n\t\t\tformatableText(\"Hostname\", \"Hostname\") +\n\t\t\tformatableText(\"City\", \"City\") +\n\t\t\tformatableText(\"Region\", \"Region\") +\n\t\t\tformatableText(\"Country\", \"Country\") +\n\t\t\tformatableText(\"Loc\", \"Coordinates\") +\n\t\t\tformatableText(\"Org\", \"Organization\"),\n\t)\n\n\tresultBuffer := new(bytes.Buffer)\n\n\terr := resultTemplate.Execute(resultBuffer, map[string]string{\n\t\t\"subheadingColor\": widget.settings.Colors.Subheading,\n\t\t\"valueColor\":      widget.settings.Colors.Text,\n\t\t\"Ip\":              info.Ip,\n\t\t\"Hostname\":        info.Hostname,\n\t\t\"City\":            info.City,\n\t\t\"Region\":          info.Region,\n\t\t\"Country\":         info.Country,\n\t\t\"Coordinates\":     info.Coordinates,\n\t\t\"PostalCode\":      info.PostalCode,\n\t\t\"Organization\":    info.Organization,\n\t})\n\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t}\n\n\twidget.result = resultBuffer.String()\n}\n\nfunc formatableText(key, value string) string {\n\treturn fmt.Sprintf(\" [{{.subheadingColor}}]%8s[-:-:-] [{{.valueColor}}]{{.%s}}\\n\", key, value)\n}\n\n// getMyIP provides this system's default IPv4 or IPv6 IP address for routing WAN requests.\n// It does so by dialing out to a site known to have both an A and AAAA DNS records (IPv6)\n// The 'net' package is allowed to decide how to connect, connecting to both IPv4 or IPv6 address\n// depending on the availbility of IP protocols.\nfunc getMyIP(version protocolVersion) (ip net.IP, v6 bool) {\n\tlog.Log(fmt.Sprintf(\"Protocol version: %s\", version))\n\tlog.Log(fmt.Sprintf(\"Network: %s\", version.toNetwork()))\n\t//fmt.Println(\"Protocol version: \", version)\n\tconn, err := net.Dial(version.toNetwork(), \"fast.com:80\")\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer func() { _ = conn.Close() }()\n\n\taddr := conn.LocalAddr().(*net.TCPAddr)\n\tip = addr.IP\n\tv6 = ip.To4() == nil\n\n\treturn\n}\n\nfunc (pv protocolVersion) toNetwork() string {\n\tswitch pv {\n\tcase ipV4:\n\t\treturn \"tcp4\"\n\tcase ipV6:\n\t\treturn \"tcp6\"\n\tdefault:\n\t\treturn \"tcp\"\n\t}\n}\n"
  },
  {
    "path": "modules/jenkins/client.go",
    "content": "package jenkins\n\nimport (\n\t\"crypto/tls\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc (widget *Widget) Create(jenkinsURL string, username string, apiKey string) (*View, error) {\n\tconst apiSuffix = \"api/json?pretty=true\"\n\tview := &View{}\n\tparsedSuffix, err := url.Parse(apiSuffix)\n\tif err != nil {\n\t\treturn view, err\n\t}\n\n\tparsedJenkinsURL, err := url.Parse(ensureLastSlash(jenkinsURL))\n\tif err != nil {\n\t\treturn view, err\n\t}\n\tjenkinsAPIURL := parsedJenkinsURL.ResolveReference(parsedSuffix)\n\n\treq, _ := http.NewRequest(\"GET\", jenkinsAPIURL.String(), http.NoBody)\n\treq.SetBasicAuth(username, apiKey)\n\n\thttpClient := &http.Client{Transport: &http.Transport{\n\t\tTLSClientConfig: &tls.Config{\n\t\t\tInsecureSkipVerify: !widget.settings.verifyServerCertificate,\n\t\t},\n\t\tProxy: http.ProxyFromEnvironment,\n\t},\n\t}\n\tresp, err := httpClient.Do(req)\n\n\tif err != nil {\n\t\treturn view, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\terr = utils.ParseJSON(view, resp.Body)\n\tif err != nil {\n\t\treturn view, err\n\t}\n\n\trespJobs := make([]Job, 0, len(view.Jobs)+len(view.ActiveConfigurations))\n\trespJobs = append(append(respJobs, view.Jobs...), view.ActiveConfigurations...)\n\n\tjobs := make([]Job, 0)\n\n\tvar validID = regexp.MustCompile(widget.settings.jobNameRegex)\n\tfor _, job := range respJobs {\n\t\tif validID.MatchString(job.Name) {\n\t\t\tjobs = append(jobs, job)\n\t\t}\n\t}\n\n\tview.Jobs = jobs\n\n\treturn view, nil\n}\n\nfunc ensureLastSlash(url string) string {\n\treturn strings.TrimRight(url, \"/\") + \"/\"\n}\n"
  },
  {
    "path": "modules/jenkins/job.go",
    "content": "package jenkins\n\ntype Job struct {\n\tName  string `json:\"name\"`\n\tUrl   string `json:\"url\"`\n\tColor string `json:\"color\"`\n}\n"
  },
  {
    "path": "modules/jenkins/keyboard.go",
    "content": "package jenkins\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openJob, \"Open job in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openJob, \"Open job in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/jenkins/settings.go",
    "content": "package jenkins\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Jenkins\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey                  string `help:\"Your Jenkins API key.\"`\n\tjobNameRegex            string `help:\"A regex that filters the jobs shown in the widget.\" optional:\"true\"`\n\tsuccessBallColor        string `help:\"Changes the default color of successful Jenkins jobs to the color of your choosing.\" values:\"blue, green, purple, yellow, etc.\" optional:\"true\"`\n\turl                     string `help:\"The url to your Jenkins project or view.\"`\n\tuser                    string `help:\"Your Jenkins username.\"`\n\tverifyServerCertificate bool   `help:\"Determines whether or not the server’s certificate chain and host name are verified.\" values:\"true or false\" optional:\"true\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:                  ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_JENKINS_API_KEY\"))),\n\t\tjobNameRegex:            ymlConfig.UString(\"jobNameRegex\", \".*\"),\n\t\tsuccessBallColor:        ymlConfig.UString(\"successBallColor\", \"blue\"),\n\t\turl:                     ymlConfig.UString(\"url\"),\n\t\tuser:                    ymlConfig.UString(\"user\"),\n\t\tverifyServerCertificate: ymlConfig.UBool(\"verifyServerCertificate\", true),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).\n\t\tService(settings.url).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/jenkins/view.go",
    "content": "package jenkins\n\ntype View struct {\n\tDescription          string `json:\"description\"`\n\tJobs                 []Job  `json:\"jobs\"`\n\tActiveConfigurations []Job  `json:\"activeConfigurations\"`\n\tName                 string `json:\"name\"`\n\tUrl                  string `json:\"url\"`\n}\n"
  },
  {
    "path": "modules/jenkins/widget.go",
    "content": "package jenkins\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tsettings *Settings\n\tview     *View\n\terr      error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\tview, err := widget.Create(\n\t\twidget.settings.url,\n\t\twidget.settings.user,\n\t\twidget.settings.apiKey,\n\t)\n\twidget.view = view\n\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\twidget.SetItemCount(len(widget.view.Jobs))\n\t}\n\n\twidget.Render()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := fmt.Sprintf(\"%s: [red]%s\", widget.CommonSettings().Title, widget.view.Name)\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\tif widget.view == nil || len(widget.view.Jobs) == 0 {\n\t\treturn title, \"No content to display\", false\n\t}\n\n\tvar str string\n\tjobs := widget.view.Jobs\n\tfor idx, job := range jobs {\n\t\tjobName, _ := url.QueryUnescape(job.Name)\n\n\t\trow := fmt.Sprintf(\n\t\t\t`[%s] [%s]%-6s[white]`,\n\t\t\twidget.RowColor(idx),\n\t\t\twidget.jobColor(job),\n\t\t\tjobName,\n\t\t)\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(job.Name))\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) jobColor(job Job) string {\n\tswitch job.Color {\n\tcase \"blue\":\n\t\t// Override color if successBallColor boolean param provided in config\n\t\treturn widget.settings.successBallColor\n\tcase \"red\":\n\t\treturn \"red\"\n\tdefault:\n\t\treturn \"white\"\n\t}\n}\n\nfunc (widget *Widget) openJob() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.view != nil && sel < len(widget.view.Jobs) {\n\t\tjob := &widget.view.Jobs[sel]\n\t\tutils.OpenFile(job.Url)\n\t}\n}\n"
  },
  {
    "path": "modules/jira/client.go",
    "content": "package jira\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\n// UserIDCache represent a cached username to account ID mapping\ntype UserIDCache struct {\n\tAccountID string\n\tExpiresAt time.Time\n}\n\n// UserIDCacheMap holds the cache with thread safety\ntype UserIDCacheMap struct {\n\tcache map[string]UserIDCache\n\tmutex sync.RWMutex\n}\n\n// Global cache instance\nvar userIDCache = &UserIDCacheMap{\n\tcache: make(map[string]UserIDCache),\n}\n\n// JQLConversionRequest represents the request body for the JQL conversion API\ntype JQLConversionRequest struct {\n\tQueryStrings []string `json:\"queryStrings\"`\n}\n\n// JQLConversionResponse represents the response from the JQL conversion API\ntype JQLConversionResponse struct {\n\tQueryStrings []ConvertedQuery `json:\"queryStrings\"`\n}\n\n// ConvertedQuery represents a single converted JQL query\ntype ConvertedQuery struct {\n\tQuery          string        `json:\"query\"`\n\tConvertedQuery string        `json:\"convertedQuery\"`\n\tUserMessages   []UserMessage `json:\"userMessages\"`\n}\n\n// UserMessage represents messages about the conversion\ntype UserMessage struct {\n\tMessageKey  string            `json:\"messageKey\"`\n\tMessageArgs map[string]string `json:\"messageArgs\"`\n}\n\n// Get retrieves a cache account ID for a username\nfunc (c *UserIDCacheMap) Get(username string) (string, bool) {\n\tc.mutex.RLock()\n\tentry, exists := c.cache[username]\n\tif !exists {\n\t\tc.mutex.RUnlock()\n\t\treturn \"\", false\n\t}\n\n\t// Check if cache entry has expired\n\tif time.Now().After(entry.ExpiresAt) {\n\t\tc.mutex.RUnlock()\n\t\t// Remove expired entry - upgrade to write lock\n\t\tc.mutex.Lock()\n\t\tdelete(c.cache, username)\n\t\tc.mutex.Unlock()\n\t\treturn \"\", false\n\t}\n\n\taccountID := entry.AccountID\n\tc.mutex.RUnlock()\n\treturn accountID, true\n}\n\n// Set stores a username to account ID mapping with expiration\nfunc (c *UserIDCacheMap) Set(username, accountID string, duration time.Duration) {\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\n\tc.cache[username] = UserIDCache{\n\t\tAccountID: accountID,\n\t\tExpiresAt: time.Now().Add(duration),\n\t}\n}\n\n// Clear removes all expired entries from the cache\nfunc (c *UserIDCacheMap) Clear() {\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\n\tnow := time.Now()\n\tfor username, entry := range c.cache {\n\t\tif now.After(entry.ExpiresAt) {\n\t\t\tdelete(c.cache, username)\n\t\t}\n\t}\n}\n\n// ConvertJQLWithUsername converts a JQL query containing username to account ID\nfunc (widget *Widget) ConvertJQLWithUsername(username string) (string, error) {\n\t// Check cache first\n\tif accountID, found := userIDCache.Get(username); found {\n\t\treturn fmt.Sprintf(\"assignee = \\\"%s\\\"\", accountID), nil\n\t}\n\n\t// Create a JQL query with the username that needs conversion\n\toriginalJQL := fmt.Sprintf(\"assignee = \\\"%s\\\"\", username)\n\n\t// Prepare the request body\n\trequestBody := JQLConversionRequest{\n\t\tQueryStrings: []string{originalJQL},\n\t}\n\n\t// Convert to JSON\n\tjsonData, err := json.Marshal(requestBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal request: %v\", err)\n\t}\n\n\t// Make the POST request to the JQL conversion API\n\tresp, err := widget.jiraPostRequest(\"/rest/api/3/jql/pdcleaner\", jsonData)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar conversionResult JQLConversionResponse\n\terr = utils.ParseJSON(&conversionResult, bytes.NewReader(resp))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(conversionResult.QueryStrings) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no conversion result for username: %s\", username)\n\t}\n\n\t// Return the converted JQL query part (just the assignee part)\n\tconvertedQuery := conversionResult.QueryStrings[0].ConvertedQuery\n\n\t// Extract account ID properly\n\taccountID := extractAccountIDFromJQL(convertedQuery)\n\tif accountID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"failed to extract account ID from converted query: %s\", convertedQuery)\n\t}\n\n\t// Cache the result for 10 minutes\n\tuserIDCache.Set(username, accountID, 10*time.Minute)\n\n\treturn convertedQuery, nil\n}\n\n// extractAccountIDFromJQL extracts the account ID from a converted JQL query\nfunc extractAccountIDFromJQL(jql string) string {\n\t// Example: \"assignee = \\\"account:5b10ac8d82e05b22cc7d4ef5\\\"\"\n\t// We want to extract: \"account:5b10ac8d82e05b22cc7d4ef5\"\n\n\tstart := strings.Index(jql, \"\\\"\")\n\tif start == -1 {\n\t\treturn \"\"\n\t}\n\n\tend := strings.LastIndex(jql, \"\\\"\")\n\tif end == -1 || end <= start {\n\t\treturn \"\"\n\t}\n\n\treturn jql[start+1 : end]\n}\n\n// IssuesFor returns a collection of issues for a given collection of projects.\n// If username is provided, it scopes the issues to that person\nfunc (widget *Widget) IssuesFor(username string, projects []string, jql string) (*SearchResult, error) {\n\tquery := []string{}\n\n\tvar projQuery = getProjectQuery(projects)\n\tif projQuery != \"\" {\n\t\tquery = append(query, projQuery)\n\t}\n\n\tif username != \"\" {\n\t\t// Convert JQL with username to account ID\n\t\tconvertedJQL, err := widget.ConvertJQLWithUsername(username)\n\t\tif err != nil {\n\t\t\treturn &SearchResult{}, fmt.Errorf(\"failed to convert username %s to account ID: %v\", username, err)\n\t\t}\n\t\tquery = append(query, convertedJQL)\n\t}\n\n\tif jql != \"\" {\n\t\tquery = append(query, jql)\n\t}\n\n\t// Try the new API v3 search/jql endpoint\n\tjqlQuery := strings.Join(query, \" AND \")\n\tsearchResult, err := widget.searchWithNewAPI(jqlQuery)\n\tif err != nil {\n\t\t// If new API fails, return the error\n\t\treturn &SearchResult{}, fmt.Errorf(\"JIRA search failed: %v\", err)\n\t}\n\n\treturn searchResult, nil\n}\n\n// searchWithNewAPI uses the new /rest/api/3/search/jql endpoint\nfunc (widget *Widget) searchWithNewAPI(jql string) (*SearchResult, error) {\n\t// First, get issue IDs using the new endpoint\n\tv := url.Values{}\n\tv.Set(\"jql\", jql)\n\tv.Set(\"maxResults\", \"20\") // Limit to avoid too many API calls\n\n\tjqlURL := fmt.Sprintf(\"/rest/api/3/search/jql?%s\", v.Encode())\n\n\tresp, err := widget.jiraRequest(jqlURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Parse the JQL response which contains issue IDs\n\ttype JQLSearchResult struct {\n\t\tIssues []struct {\n\t\t\tID string `json:\"id\"`\n\t\t} `json:\"issues\"`\n\t}\n\n\tjqlResult := &JQLSearchResult{}\n\terr = utils.ParseJSON(jqlResult, bytes.NewReader(resp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse JQL search response: %v\", err)\n\t}\n\n\tif len(jqlResult.Issues) == 0 {\n\t\t// Return empty result if no issues found\n\t\treturn &SearchResult{Issues: []Issue{}}, nil\n\t}\n\n\t// Now get full issue details for each ID\n\tsearchResult := &SearchResult{Issues: []Issue{}}\n\n\tfor i, issue := range jqlResult.Issues {\n\t\t// Limit to prevent too many API calls\n\t\tif i >= 20 {\n\t\t\tbreak\n\t\t}\n\n\t\tfullIssue, err := widget.getIssueByID(issue.ID)\n\t\tif err != nil {\n\t\t\t// Log error but continue with other issues\n\t\t\tfmt.Printf(\"Error fetching issue %s: %v\\n\", issue.ID, err)\n\t\t\tcontinue\n\t\t}\n\t\tsearchResult.Issues = append(searchResult.Issues, *fullIssue)\n\t}\n\n\treturn searchResult, nil\n} // getIssueByID fetches full issue details by ID\nfunc (widget *Widget) getIssueByID(issueID string) (*Issue, error) {\n\turl := fmt.Sprintf(\"/rest/api/3/issue/%s\", issueID)\n\n\tresp, err := widget.jiraRequest(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tissue := &Issue{}\n\terr = utils.ParseJSON(issue, bytes.NewReader(resp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse issue %s: %v\", issueID, err)\n\t}\n\n\treturn issue, nil\n}\n\nfunc buildJql(key string, value string) string {\n\treturn fmt.Sprintf(\"%s = \\\"%s\\\"\", key, value)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) jiraRequest(path string) ([]byte, error) {\n\turl := fmt.Sprintf(\"%s%s\", widget.settings.domain, path)\n\n\treq, err := http.NewRequest(\"GET\", url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif widget.settings.personalAccessToken != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+widget.settings.personalAccessToken)\n\t} else {\n\t\treq.SetBasicAuth(widget.settings.email, widget.settings.apiKey)\n\t}\n\n\thttpClient := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tInsecureSkipVerify: !widget.settings.verifyServerCertificate,\n\t\t\t},\n\t\t\tProxy: http.ProxyFromEnvironment,\n\t\t},\n\t}\n\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"JIRA API error - %s: %s (URL: %s)\", resp.Status, string(body), url)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn body, nil\n}\n\nfunc (widget *Widget) jiraPostRequest(path string, data []byte) ([]byte, error) {\n\turl := fmt.Sprintf(\"%s%s\", widget.settings.domain, path)\n\n\treq, err := http.NewRequest(\"POST\", url, bytes.NewBuffer(data))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tif widget.settings.personalAccessToken != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+widget.settings.personalAccessToken)\n\t} else {\n\t\treq.SetBasicAuth(widget.settings.email, widget.settings.apiKey)\n\t}\n\n\thttpClient := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tInsecureSkipVerify: !widget.settings.verifyServerCertificate,\n\t\t\t},\n\t\t\tProxy: http.ProxyFromEnvironment,\n\t\t},\n\t}\n\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"JIRA API POST error - %s: %s (URL: %s)\", resp.Status, string(body), url)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn body, nil\n}\n\nfunc getProjectQuery(projects []string) string {\n\tsingleEmptyProject := len(projects) == 1 && projects[0] == \"\"\n\tif len(projects) == 0 || singleEmptyProject {\n\t\treturn \"\"\n\t} else if len(projects) == 1 {\n\t\treturn buildJql(\"project\", projects[0])\n\t}\n\n\tquoted := make([]string, len(projects))\n\tfor i := range projects {\n\t\tquoted[i] = fmt.Sprintf(\"\\\"%s\\\"\", projects[i])\n\t}\n\treturn fmt.Sprintf(\"project in (%s)\", strings.Join(quoted, \", \"))\n}\n"
  },
  {
    "path": "modules/jira/client_test.go",
    "content": "package jira\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gotest.tools/assert\"\n)\n\nfunc TestUserIDCacheMap_SetAndGet(t *testing.T) {\n\tcache := &UserIDCacheMap{\n\t\tcache: make(map[string]UserIDCache),\n\t}\n\n\t// Test setting and getting a value\n\tusername := \"testuser\"\n\taccountID := \"account:123456789\"\n\tduration := 5 * time.Minute\n\n\tcache.Set(username, accountID, duration)\n\n\t// Test successful retrieval\n\tretrievedID, found := cache.Get(username)\n\tassert.Equal(t, true, found)\n\tassert.Equal(t, accountID, retrievedID)\n}\n\nfunc TestUserIDCacheMap_GetNonExistent(t *testing.T) {\n\tcache := &UserIDCacheMap{\n\t\tcache: make(map[string]UserIDCache),\n\t}\n\n\t// Test getting non-existent value\n\tretrievedID, found := cache.Get(\"nonexistent\")\n\tassert.Equal(t, false, found)\n\tassert.Equal(t, \"\", retrievedID)\n}\n\nfunc TestUserIDCacheMap_GetExpired(t *testing.T) {\n\tcache := &UserIDCacheMap{\n\t\tcache: make(map[string]UserIDCache),\n\t}\n\n\t// Set an entry that expires immediately\n\tusername := \"expireduser\"\n\taccountID := \"account:987654321\"\n\tcache.Set(username, accountID, -1*time.Second) // Already expired\n\n\t// Test that expired entry is not returned and is cleaned up\n\tretrievedID, found := cache.Get(username)\n\tassert.Equal(t, false, found)\n\tassert.Equal(t, \"\", retrievedID)\n\n\t// Verify the expired entry was removed from cache\n\tcache.mutex.RLock()\n\t_, exists := cache.cache[username]\n\tcache.mutex.RUnlock()\n\tassert.Equal(t, false, exists)\n}\n\nfunc TestUserIDCacheMap_Clear(t *testing.T) {\n\tcache := &UserIDCacheMap{\n\t\tcache: make(map[string]UserIDCache),\n\t}\n\n\t// Add a valid entry and an expired entry\n\tcache.Set(\"validuser\", \"account:111\", 5*time.Minute)\n\tcache.Set(\"expireduser\", \"account:222\", -1*time.Second)\n\n\t// Clear expired entries\n\tcache.Clear()\n\n\t// Valid entry should still exist\n\t_, found := cache.Get(\"validuser\")\n\tassert.Equal(t, true, found)\n\n\t// Expired entry should be gone\n\tcache.mutex.RLock()\n\t_, exists := cache.cache[\"expireduser\"]\n\tcache.mutex.RUnlock()\n\tassert.Equal(t, false, exists)\n}\n\nfunc TestExtractAccountIDFromJQL(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tjql      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"valid account ID\",\n\t\t\tjql:      `assignee = \"account:5b10ac8d82e05b22cc7d4ef5\"`,\n\t\t\texpected: \"account:5b10ac8d82e05b22cc7d4ef5\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single quotes\",\n\t\t\tjql:      `assignee = 'account:123456789'`,\n\t\t\texpected: \"\", // Our function only handles double quotes\n\t\t},\n\t\t{\n\t\t\tname:     \"no quotes\",\n\t\t\tjql:      `assignee = account:123456789`,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tjql:      \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"malformed JQL\",\n\t\t\tjql:      `assignee = \"incomplete`,\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := extractAccountIDFromJQL(tt.jql)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestConvertJQLWithUsername_CacheHit(t *testing.T) {\n\t// Setup a mock widget (minimal setup for testing)\n\twidget := &Widget{}\n\n\t// Clear and setup cache\n\tuserIDCache = &UserIDCacheMap{\n\t\tcache: make(map[string]UserIDCache),\n\t}\n\n\t// Pre-populate cache\n\tusername := \"cacheduser\"\n\taccountID := \"account:cached123\"\n\tuserIDCache.Set(username, accountID, 5*time.Minute)\n\n\t// Test that cached value is returned without API call\n\tresult, err := widget.ConvertJQLWithUsername(username)\n\n\tassert.NilError(t, err)\n\tassert.Equal(t, `assignee = \"account:cached123\"`, result)\n}\n\nfunc TestConvertJQLWithUsername_APICalls(t *testing.T) {\n\t// Create a mock JIRA server\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// Verify it's a POST request to the right endpoint\n\t\tassert.Equal(t, \"POST\", r.Method)\n\t\tassert.Equal(t, \"/rest/api/3/jql/pdcleaner\", r.URL.Path)\n\t\tassert.Equal(t, \"application/json\", r.Header.Get(\"Content-Type\"))\n\n\t\t// Mock response\n\t\tresponse := JQLConversionResponse{\n\t\t\tQueryStrings: []ConvertedQuery{\n\t\t\t\t{\n\t\t\t\t\tQuery:          `assignee = \"testuser\"`,\n\t\t\t\t\tConvertedQuery: `assignee = \"account:5b10ac8d82e05b22cc7d4ef5\"`,\n\t\t\t\t\tUserMessages:   []UserMessage{},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_ = json.NewEncoder(w).Encode(response)\n\t}))\n\tdefer server.Close()\n\n\t// Setup widget with mock server\n\twidget := &Widget{\n\t\tsettings: &Settings{\n\t\t\tdomain: server.URL,\n\t\t},\n\t}\n\n\t// Clear cache\n\tuserIDCache = &UserIDCacheMap{\n\t\tcache: make(map[string]UserIDCache),\n\t}\n\n\t// Test API call\n\tresult, err := widget.ConvertJQLWithUsername(\"testuser\")\n\n\tassert.NilError(t, err)\n\tassert.Equal(t, `assignee = \"account:5b10ac8d82e05b22cc7d4ef5\"`, result)\n\n\t// Verify it was cached\n\tcachedID, found := userIDCache.Get(\"testuser\")\n\tassert.Equal(t, true, found)\n\tassert.Equal(t, \"account:5b10ac8d82e05b22cc7d4ef5\", cachedID)\n}\n\nfunc TestConvertJQLWithUsername_APIError(t *testing.T) {\n\t// Create a mock server that returns an error\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t_, _ = w.Write([]byte(\"Internal Server Error\"))\n\t}))\n\tdefer server.Close()\n\n\t// Setup widget with mock server\n\twidget := &Widget{\n\t\tsettings: &Settings{\n\t\t\tdomain: server.URL,\n\t\t},\n\t}\n\n\t// Clear cache\n\tuserIDCache = &UserIDCacheMap{\n\t\tcache: make(map[string]UserIDCache),\n\t}\n\n\t// Test API error handling\n\tresult, err := widget.ConvertJQLWithUsername(\"testuser\")\n\n\tassert.ErrorContains(t, err, \"500 Internal Server Error\")\n\tassert.Equal(t, \"\", result)\n}\n\nfunc TestConvertJQLWithUsername_EmptyResponse(t *testing.T) {\n\t// Create a mock server that returns empty response\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tresponse := JQLConversionResponse{\n\t\t\tQueryStrings: []ConvertedQuery{},\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_ = json.NewEncoder(w).Encode(response)\n\t}))\n\tdefer server.Close()\n\n\t// Setup widget with mock server\n\twidget := &Widget{\n\t\tsettings: &Settings{\n\t\t\tdomain: server.URL,\n\t\t},\n\t}\n\n\t// Clear cache\n\tuserIDCache = &UserIDCacheMap{\n\t\tcache: make(map[string]UserIDCache),\n\t}\n\n\t// Test empty response handling\n\tresult, err := widget.ConvertJQLWithUsername(\"testuser\")\n\n\tassert.Error(t, err, \"no conversion result for username: testuser\")\n\tassert.Equal(t, \"\", result)\n}\n\nfunc TestConvertJQLWithUsername_InvalidAccountID(t *testing.T) {\n\t// Create a mock server that returns malformed JQL\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tresponse := JQLConversionResponse{\n\t\t\tQueryStrings: []ConvertedQuery{\n\t\t\t\t{\n\t\t\t\t\tQuery:          `assignee = \"testuser\"`,\n\t\t\t\t\tConvertedQuery: `assignee = malformed_without_quotes`,\n\t\t\t\t\tUserMessages:   []UserMessage{},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_ = json.NewEncoder(w).Encode(response)\n\t}))\n\tdefer server.Close()\n\n\t// Setup widget with mock server\n\twidget := &Widget{\n\t\tsettings: &Settings{\n\t\t\tdomain: server.URL,\n\t\t},\n\t}\n\n\t// Clear cache\n\tuserIDCache = &UserIDCacheMap{\n\t\tcache: make(map[string]UserIDCache),\n\t}\n\n\t// Test invalid account ID handling\n\tresult, err := widget.ConvertJQLWithUsername(\"testuser\")\n\n\tassert.ErrorContains(t, err, \"failed to extract account ID from converted query\")\n\tassert.Equal(t, \"\", result)\n}\n"
  },
  {
    "path": "modules/jira/issues.go",
    "content": "package jira\n\ntype Issue struct {\n\tExpand string `json:\"expand\"`\n\tID     string `json:\"id\"`\n\tSelf   string `json:\"self\"`\n\tKey    string `json:\"key\"`\n\n\tIssueFields *IssueFields `json:\"fields\"`\n}\n\ntype IssueFields struct {\n\tSummary string `json:\"summary\"`\n\n\tIssueType   *IssueType   `json:\"issuetype\"`\n\tIssueStatus *IssueStatus `json:\"status\"`\n}\n\ntype IssueType struct {\n\tSelf        string `json:\"self\"`\n\tID          string `json:\"id\"`\n\tDescription string `json:\"description\"`\n\tIconURL     string `json:\"iconUrl\"`\n\tName        string `json:\"name\"`\n\tSubtask     bool   `json:\"subtask\"`\n}\n\ntype IssueStatus struct {\n\tISelf        string `json:\"self\"`\n\tIDescription string `json:\"description\"`\n\tIName        string `json:\"name\"`\n}\n"
  },
  {
    "path": "modules/jira/keyboard.go",
    "content": "package jira\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openItem, \"Open item in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openItem, \"Open item in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/jira/search_result.go",
    "content": "package jira\n\ntype SearchResult struct {\n\tStartAt    int     `json:\"startAt\"`\n\tMaxResults int     `json:\"maxResults\"`\n\tTotal      int     `json:\"total\"`\n\tIssues     []Issue `json:\"issues\"`\n}\n"
  },
  {
    "path": "modules/jira/settings.go",
    "content": "package jira\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Jira\"\n)\n\ntype colors struct {\n\trows struct {\n\t\teven string\n\t\todd  string\n\t}\n}\n\ntype Settings struct {\n\tcolors\n\t*cfg.Common\n\n\tapiKey                  string   `help:\"Your Jira API key (or password for basic auth).\"`\n\tpersonalAccessToken     string   `help:\"Access Token to use instead of username / password auth\"`\n\tdomain                  string   `help:\"Your Jira corporate domain.\"`\n\temail                   string   `help:\"The email address associated with your Jira account (or username for basic auth).\"`\n\tjql                     string   `help:\"Custom JQL to be appended to the search query.\" values:\"See Search Jira like a boss with JQL for details.\" optional:\"true\"`\n\tprojects                []string `help:\"An array of projects to get data from\"`\n\tusername                string   `help:\"Your Jira username. If provided, will filter issues by this username.\" optional:\"true\"`\n\tverifyServerCertificate bool     `help:\"Determines whether or not the server’s certificate chain and host name are verified.\" values:\"true or false\" optional:\"true\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:                  ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_JIRA_API_KEY\"))),\n\t\tpersonalAccessToken:     ymlConfig.UString(\"personalAccessToken\"),\n\t\tdomain:                  ymlConfig.UString(\"domain\"),\n\t\temail:                   ymlConfig.UString(\"email\"),\n\t\tjql:                     ymlConfig.UString(\"jql\"),\n\t\tusername:                ymlConfig.UString(\"username\"),\n\t\tverifyServerCertificate: ymlConfig.UBool(\"verifyServerCertificate\", true),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).\n\t\tService(settings.domain).Load()\n\n\tsettings.rows.even = ymlConfig.UString(\"colors.even\", \"lightblue\")\n\tsettings.rows.odd = ymlConfig.UString(\"colors.odd\", \"white\")\n\n\tsettings.projects = settings.arrayifyProjects(ymlConfig)\n\n\treturn &settings\n}\n\n/* -------------------- Unexported functions -------------------- */\n\n// arrayifyProjects figures out if we're dealing with a single project or an array of projects\nfunc (settings *Settings) arrayifyProjects(ymlConfig *config.Config) []string {\n\tprojects := []string{}\n\n\t// Single project\n\tproject, err := ymlConfig.String(\"project\")\n\tif err == nil {\n\t\tprojects = append(projects, project)\n\t\treturn projects\n\t}\n\n\t// Array of projects\n\tprojectList := ymlConfig.UList(\"project\")\n\tfor _, projectName := range projectList {\n\t\tif project, ok := projectName.(string); ok {\n\t\t\tprojects = append(projects, project)\n\t\t}\n\t}\n\n\treturn projects\n}\n"
  },
  {
    "path": "modules/jira/widget.go",
    "content": "package jira\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tresult   *SearchResult\n\tsettings *Settings\n\terr      error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tsearchResult, err := widget.IssuesFor(\n\t\twidget.settings.username,\n\t\twidget.settings.projects,\n\t\twidget.settings.jql,\n\t)\n\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.result = nil\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\twidget.err = nil\n\t\twidget.result = searchResult\n\t\twidget.SetItemCount(len(searchResult.Issues))\n\t}\n\twidget.Render()\n}\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) openItem() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.result != nil && sel < len(widget.result.Issues) {\n\t\tissue := &widget.result.Issues[sel]\n\t\tutils.OpenFile(widget.settings.domain + \"/browse/\" + issue.Key)\n\t}\n}\n\nconst MaxIssueTypeLength = 7\nconst MaxStatusNameLength = 14\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tif widget.err != nil {\n\t\treturn widget.CommonSettings().Title, widget.err.Error(), true\n\t}\n\n\ttitle := widget.CommonSettings().Title\n\n\tstr := fmt.Sprintf(\" [%s]Assigned Issues[white]\\n\", widget.settings.Colors.Subheading)\n\n\tif widget.result == nil || len(widget.result.Issues) == 0 {\n\t\treturn title, \"No results to display\", false\n\t}\n\n\tlongestIssueTypeLength, longestKeyLength, longestStatusNameLength := getLongestColumnLengths(widget.result.Issues)\n\n\tfor idx, issue := range widget.result.Issues {\n\t\trow := fmt.Sprintf(\n\t\t\t`[%s] [%s]%-*s[white] [green]%-*s[white] [yellow]%-*s[white] [%s]%s`,\n\t\t\twidget.RowColor(idx),\n\t\t\twidget.issueTypeColor(&issue),\n\t\t\tlongestIssueTypeLength+1,\n\t\t\ttrimToMaxLength(issue.IssueFields.IssueType.Name, MaxIssueTypeLength),\n\t\t\tlongestKeyLength+1,\n\t\t\tissue.Key,\n\t\t\tlongestStatusNameLength+1,\n\t\t\ttrimToMaxLength(issue.IssueFields.IssueStatus.IName, MaxStatusNameLength),\n\t\t\twidget.RowColor(idx),\n\t\t\ttview.Escape(issue.IssueFields.Summary),\n\t\t)\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(issue.IssueFields.Summary))\n\t}\n\n\treturn title, str, false\n}\n\nfunc getLongestColumnLengths(issues []Issue) (int, int, int) {\n\tlongestIssueTypeLength := 0\n\tlongestKeyLength := 0\n\tlongestStatusNameLength := 0\n\tfor _, issue := range issues {\n\t\tissueTypeLength := len(issue.IssueFields.IssueType.Name)\n\t\tif issueTypeLength > longestIssueTypeLength {\n\t\t\tlongestIssueTypeLength = issueTypeLength\n\t\t}\n\n\t\tissueKeyLength := len(issue.Key)\n\t\tif issueKeyLength > longestKeyLength {\n\t\t\tlongestKeyLength = len(\"WTF-XXX\") // issueKeyLength\n\t\t}\n\n\t\tstatusNameLength := len(issue.IssueFields.IssueStatus.IName)\n\t\tif statusNameLength > longestStatusNameLength {\n\t\t\tlongestStatusNameLength = statusNameLength\n\t\t}\n\t}\n\n\tif longestIssueTypeLength > MaxIssueTypeLength {\n\t\tlongestIssueTypeLength = MaxIssueTypeLength\n\t}\n\n\tif longestStatusNameLength > MaxStatusNameLength {\n\t\tlongestStatusNameLength = MaxStatusNameLength\n\t}\n\n\treturn longestIssueTypeLength, longestKeyLength, longestStatusNameLength\n}\n\nfunc (*Widget) issueTypeColor(issue *Issue) string {\n\tswitch issue.IssueFields.IssueType.Name {\n\tcase \"Bug\":\n\t\treturn \"red\"\n\tcase \"Story\":\n\t\treturn \"blue\"\n\tcase \"Task\":\n\t\treturn \"orange\"\n\tdefault:\n\t\treturn \"white\"\n\t}\n}\n\nfunc trimToMaxLength(text string, maxLength int) string {\n\tif len(text) <= maxLength {\n\t\treturn text\n\t} else {\n\t\treturn text[:maxLength]\n\t}\n}\n"
  },
  {
    "path": "modules/krisinformation/client.go",
    "content": "package krisinformation\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/wtfutil/wtf/logger\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tkrisinformationAPI = \"https://api.krisinformation.se/v2/feed?format=json\"\n)\n\ntype Krisinformation []struct {\n\tIdentifier  string    `json:\"Identifier\"`\n\tPushMessage string    `json:\"PushMessage\"`\n\tUpdated     time.Time `json:\"Updated\"`\n\tPublished   time.Time `json:\"Published\"`\n\tHeadline    string    `json:\"Headline\"`\n\tPreamble    string    `json:\"Preamble\"`\n\tBodyText    string    `json:\"BodyText\"`\n\tArea        []struct {\n\t\tType                string      `json:\"Type\"`\n\t\tDescription         string      `json:\"Description\"`\n\t\tCoordinate          string      `json:\"Coordinate\"`\n\t\tGeometryInformation interface{} `json:\"GeometryInformation\"`\n\t} `json:\"Area\"`\n\tWeb        string        `json:\"Web\"`\n\tLanguage   string        `json:\"Language\"`\n\tEvent      string        `json:\"Event\"`\n\tSenderName string        `json:\"SenderName\"`\n\tPush       bool          `json:\"Push\"`\n\tBodyLinks  []interface{} `json:\"BodyLinks\"`\n\tSourceID   int           `json:\"SourceID\"`\n\tIsVma      bool          `json:\"IsVma\"`\n\tIsTestVma  bool          `json:\"IsTestVma\"`\n}\n\n// Client holds or configuration\ntype Client struct {\n\tlatitude  float64\n\tlongitude float64\n\tradius    int\n\tcounty    string\n\tcountry   bool\n}\n\n// Item holds the interesting parts\ntype Item struct {\n\tPushMessage string\n\tHeadLine    string\n\tSenderName  string\n\tCountry     bool\n\tCounty      bool\n\tDistance    float64\n\tUpdated     time.Time\n}\n\n// NewClient returns a new Client\nfunc NewClient(latitude, longitude float64, radius int, county string, country bool) *Client {\n\treturn &Client{\n\t\tlatitude:  latitude,\n\t\tlongitude: longitude,\n\t\tradius:    radius,\n\t\tcounty:    county,\n\t\tcountry:   country,\n\t}\n\n}\n\n// getKrisinformation - return items that match either country, county or a radius\n// Priority:\n//   - Country\n//   - County\n//   - Region\nfunc (c *Client) getKrisinformation() (items []Item, err error) {\n\tresp, err := http.Get(krisinformationAPI)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tvar data Krisinformation\n\terr = utils.ParseJSON(&data, resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor i := range data {\n\t\tfor a := range data[i].Area {\n\t\t\t// Country wide events\n\t\t\tif c.country && data[i].Area[a].Type == \"Country\" {\n\t\t\t\titem := Item{\n\t\t\t\t\tPushMessage: data[i].PushMessage,\n\t\t\t\t\tHeadLine:    data[i].Headline,\n\t\t\t\t\tSenderName:  data[i].SenderName,\n\t\t\t\t\tCountry:     true,\n\t\t\t\t\tUpdated:     data[i].Updated,\n\t\t\t\t}\n\t\t\t\titems = append(items, item)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// County specific events\n\t\t\tif c.county != \"\" && data[i].Area[a].Type == \"County\" {\n\t\t\t\t// We look for county in description\n\t\t\t\tif strings.Contains(\n\t\t\t\t\tstrings.ToLower(data[i].Area[a].Description),\n\t\t\t\t\tstrings.ToLower(c.county),\n\t\t\t\t) {\n\t\t\t\t\titem := Item{\n\t\t\t\t\t\tPushMessage: data[i].PushMessage,\n\t\t\t\t\t\tHeadLine:    data[i].Headline,\n\t\t\t\t\t\tSenderName:  data[i].SenderName,\n\t\t\t\t\t\tCounty:      true,\n\t\t\t\t\t\tUpdated:     data[i].Updated,\n\t\t\t\t\t}\n\t\t\t\t\titems = append(items, item)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif c.radius != -1 {\n\t\t\t\tcoords := data[i].Area[a].Coordinate\n\t\t\t\tif coords == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tbuf := strings.Split(coords, \" \")\n\t\t\t\tlatlon := strings.Split(buf[0], \",\")\n\t\t\t\tkris_latitude, err := strconv.ParseFloat(latlon[0], 32)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tkris_longitude, err := strconv.ParseFloat(latlon[1], 32)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdistance := DistanceInMeters(kris_latitude, kris_longitude, c.latitude, c.longitude)\n\t\t\t\tlogger.Log(fmt.Sprintf(\"Distance: %f\", distance/1000)) // KM\n\t\t\t\tif distance < float64(c.radius) {\n\t\t\t\t\titem := Item{\n\t\t\t\t\t\tPushMessage: data[i].PushMessage,\n\t\t\t\t\t\tHeadLine:    data[i].Headline,\n\t\t\t\t\t\tSenderName:  data[i].SenderName,\n\t\t\t\t\t\tDistance:    distance,\n\t\t\t\t\t\tUpdated:     data[i].Updated,\n\t\t\t\t\t}\n\t\t\t\t\titems = append(items, item)\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n\n\treturn items, nil\n}\n\n// haversin(θ) function\nfunc hsin(theta float64) float64 {\n\treturn math.Pow(math.Sin(theta/2), 2)\n}\n\n// Distance function returns the distance (in meters) between two points of\n//\n//\ta given longitude and latitude relatively accurately (using a spherical\n//\tapproximation of the Earth) through the Haversin Distance Formula for\n//\tgreat arc distance on a sphere with accuracy for small distances\n//\n// point coordinates are supplied in degrees and converted into rad. in the func\n//\n// http://en.wikipedia.org/wiki/Haversine_formula\nfunc DistanceInMeters(lat1, lon1, lat2, lon2 float64) float64 {\n\t// convert to radians\n\t// must cast radius as float to multiply later\n\tvar la1, lo1, la2, lo2, r float64\n\tla1 = lat1 * math.Pi / 180\n\tlo1 = lon1 * math.Pi / 180\n\tla2 = lat2 * math.Pi / 180\n\tlo2 = lon2 * math.Pi / 180\n\n\tr = 6378100 // Earth radius in METERS\n\n\t// calculate\n\th := hsin(la2-la1) + math.Cos(la1)*math.Cos(la2)*hsin(lo2-lo1)\n\n\treturn 2 * r * math.Asin(math.Sqrt(h))\n}\n"
  },
  {
    "path": "modules/krisinformation/settings.go",
    "content": "package krisinformation\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Krisinformation\"\n\tdefaultRadius    = -1\n\tdefaultCountry   = true\n\tdefaultCounty    = \"\"\n\tdefaultMaxItems  = -1\n\tdefaultMaxAge    = 720\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\tcommon    *cfg.Common\n\tlatitude  float64 `help:\"The latitude of the position from which the widget should look for messages.\" optional:\"true\"`\n\tlongitude float64 `help:\"The longitude of the position from which the widget should look for messages.\" optional:\"true\"`\n\tradius    int     `help:\"The radius in km from your position that the widget should look for messages. need latitude/longitude setting,Default 10\" optional:\"true\"`\n\tcounty    string  `help:\"The county from where to display messages\" optional:\"true\"`\n\tcountry   bool    `help:\"Only display country wide messages\" optional:\"true\"`\n\tmaxitems  int     `help:\"Only display X number of latest messages\" optional:\"true\"`\n\tmaxage    int     `help:\"Only show messages younger than maxage\" optional:\"true\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tcommon:    cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t\tlatitude:  ymlConfig.UFloat64(\"latitude\", -1),\n\t\tlongitude: ymlConfig.UFloat64(\"longitude\", -1),\n\t\tradius:    ymlConfig.UInt(\"radius\", defaultRadius),\n\t\tcountry:   ymlConfig.UBool(\"country\", defaultCountry),\n\t\tcounty:    ymlConfig.UString(\"county\", defaultCounty),\n\t\tmaxitems:  ymlConfig.UInt(\"maxitems\", defaultMaxItems),\n\t\tmaxage:    ymlConfig.UInt(\"maxages\", defaultMaxAge),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/krisinformation/widget.go",
    "content": "package krisinformation\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget is the container for your module's data\ntype Widget struct {\n\tview.TextWidget\n\n\tapp      *tview.Application\n\tsettings *Settings\n\terr      error\n\tclient   *Client\n}\n\n// NewWidget creates and returns an instance of Widget\nfunc NewWidget(app *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(app, redrawChan, nil, settings.common),\n\t\tapp:        app,\n\t\tsettings:   settings,\n\t\tclient: NewClient(\n\t\t\tsettings.latitude,\n\t\t\tsettings.longitude,\n\t\t\tsettings.radius,\n\t\t\tsettings.county,\n\t\t\tsettings.country),\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\t// The last call should always be to the display function\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tvar title = defaultTitle\n\tif widget.CommonSettings().Title != \"\" {\n\t\ttitle = widget.CommonSettings().Title\n\t}\n\tnow := time.Now()\n\tkriser, err := widget.client.getKrisinformation()\n\tif err != nil {\n\t\thandleError(widget, err)\n\t}\n\n\tvar str string\n\ti := 0\n\tfor k := range kriser {\n\t\tdiff := now.Sub(kriser[k].Updated)\n\t\tif widget.settings.maxage != -1 {\n\t\t\t// Skip if message is too old\n\t\t\tif int(diff.Hours()) > widget.settings.maxage {\n\t\t\t\t//logger.Log(fmt.Sprintf(\"Article to old: (%s) Days: %d\", kriser[k].HeadLine, int(diff.Hours())))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\ti++\n\t\tif i > widget.settings.maxitems && widget.settings.maxitems != -1 {\n\t\t\tbreak\n\t\t}\n\t\tstr += fmt.Sprintf(\"- %s\\n\", kriser[k].HeadLine)\n\t}\n\treturn title, str, true\n}\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(func() (string, string, bool) {\n\t\treturn widget.content()\n\t})\n}\n\nfunc handleError(widget *Widget, err error) {\n\twidget.err = err\n}\n"
  },
  {
    "path": "modules/kubernetes/client.go",
    "content": "package kubernetes\n\nimport (\n\t\"k8s.io/client-go/kubernetes\"\n\t// Includes authentication modules for various Kubernetes providers\n\t_ \"k8s.io/client-go/plugin/pkg/client/auth\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n)\n\ntype clientInstance struct {\n\tClient kubernetes.Interface\n}\n\n// getInstance returns a Kubernetes interface for a clientset\nfunc (widget *Widget) getInstance() (*clientInstance, error) {\n\tvar err error\n\n\twidget.clientOnce.Do(func() {\n\t\twidget.client = &clientInstance{}\n\t\twidget.client.Client, err = widget.getKubeClient()\n\t})\n\n\treturn widget.client, err\n}\n\n// getKubeClient returns a kubernetes clientset for the kubeconfig provided\nfunc (widget *Widget) getKubeClient() (kubernetes.Interface, error) {\n\tvar overrides *clientcmd.ConfigOverrides\n\tif widget.context != \"\" {\n\t\toverrides = &clientcmd.ConfigOverrides{\n\t\t\tCurrentContext: widget.context,\n\t\t}\n\t}\n\n\tconfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(\n\t\t&clientcmd.ClientConfigLoadingRules{ExplicitPath: widget.kubeconfig},\n\t\toverrides).ClientConfig()\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclientset, err := kubernetes.NewForConfig(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset, nil\n}\n"
  },
  {
    "path": "modules/kubernetes/settings.go",
    "content": "package kubernetes\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Kubernetes\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tobjects    []string `help:\"Kubernetes objects to show. Options are: [nodes, pods, deployments].\"`\n\ttitle      string   `help:\"Override the title of widget.\"`\n\tkubeconfig string   `help:\"Location of a kubeconfig file.\"`\n\tnamespaces []string `help:\"List of namespaces to watch. If blank, defaults to all namespaces.\"`\n\tcontext    string   `help:\"Kubernetes context to use. If blank, uses default context\"`\n}\n\nfunc NewSettingsFromYAML(name string, moduleConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, moduleConfig, globalConfig),\n\n\t\tobjects:    utils.ToStrs(moduleConfig.UList(\"objects\")),\n\t\ttitle:      moduleConfig.UString(\"title\"),\n\t\tkubeconfig: moduleConfig.UString(\"kubeconfig\"),\n\t\tnamespaces: utils.ToStrs(moduleConfig.UList(\"namespaces\")),\n\t\tcontext:    moduleConfig.UString(\"context\"),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/kubernetes/widget.go",
    "content": "package kubernetes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// Widget contains all the config for the widget\ntype Widget struct {\n\tview.TextWidget\n\n\tclient     *clientInstance\n\tclientOnce sync.Once\n\n\tobjects    []string\n\ttitle      string\n\tkubeconfig string\n\tnamespaces []string\n\tcontext    string\n\tsettings   *Settings\n}\n\n// NewWidget creates a new instance of the widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tobjects:    settings.objects,\n\t\ttitle:      settings.title,\n\t\tkubeconfig: settings.kubeconfig,\n\t\tnamespaces: settings.namespaces,\n\t\tsettings:   settings,\n\t\tcontext:    settings.context,\n\t}\n\n\twidget.View.SetWrap(true)\n\n\treturn &widget\n}\n\n// Refresh executes the command and updates the view with the results\nfunc (widget *Widget) Refresh() {\n\ttitle := widget.generateTitle()\n\tclient, err := widget.getInstance()\n\n\tif err != nil {\n\t\twidget.Redraw(func() (string, string, bool) { return title, err.Error(), true })\n\t\treturn\n\t}\n\n\tvar content string\n\n\tif utils.Includes(widget.objects, \"nodes\") {\n\t\tnodeList, nodeError := client.getNodes()\n\t\tif nodeError != nil {\n\t\t\twidget.Redraw(func() (string, string, bool) { return title, \"[red] Error getting node data [white]\\n\", true })\n\t\t\treturn\n\t\t}\n\t\tcontent += fmt.Sprintf(\"[%s]Nodes[white]\\n\", widget.settings.Colors.Subheading)\n\t\tfor _, node := range nodeList {\n\t\t\tcontent += fmt.Sprintf(\"%s\\n\", node)\n\t\t}\n\t\tcontent += \"\\n\"\n\t}\n\n\tif utils.Includes(widget.objects, \"deployments\") {\n\t\tdeploymentList, deploymentError := client.getDeployments(widget.namespaces)\n\t\tif deploymentError != nil {\n\t\t\twidget.Redraw(func() (string, string, bool) { return title, \"[red] Error getting deployment data [white]\\n\", true })\n\t\t\treturn\n\t\t}\n\t\tcontent += fmt.Sprintf(\"[%s]Deployments[white]\\n\", widget.settings.Colors.Subheading)\n\t\tfor _, deployment := range deploymentList {\n\t\t\tcontent += fmt.Sprintf(\"%s\\n\", deployment)\n\t\t}\n\t\tcontent += \"\\n\"\n\t}\n\n\tif utils.Includes(widget.objects, \"pods\") {\n\t\tpodList, podError := client.getPods(widget.namespaces)\n\t\tif podError != nil {\n\t\t\twidget.Redraw(func() (string, string, bool) { return title, \"[red] Error getting pod data [white]\\n\", false })\n\t\t\treturn\n\t\t}\n\t\tcontent += fmt.Sprintf(\"[%s]Pods[white]\\n\", widget.settings.Colors.Subheading)\n\t\tfor _, pod := range podList {\n\t\t\tcontent += fmt.Sprintf(\"%s\\n\", pod)\n\t\t}\n\t\tcontent += \"\\n\"\n\t}\n\n\twidget.Redraw(func() (string, string, bool) { return title, content, false })\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// generateTitle generates a title for the widget\nfunc (widget *Widget) generateTitle() string {\n\tif widget.title != \"\" {\n\t\treturn widget.title\n\t}\n\ttitle := \"Kube\"\n\n\tif widget.context != \"\" {\n\t\ttitle = fmt.Sprintf(\"%s (%s)\", title, widget.context)\n\t}\n\n\tif len(widget.namespaces) == 1 {\n\t\ttitle += fmt.Sprintf(\" - Namespace: %s\", widget.namespaces[0])\n\t} else if len(widget.namespaces) > 1 {\n\t\ttitle += fmt.Sprintf(\" - Namespaces: %q\", widget.namespaces)\n\t}\n\treturn title\n}\n\n// getPods returns a slice of pod strings\nfunc (client *clientInstance) getPods(namespaces []string) ([]string, error) {\n\tvar podList []string\n\tif len(namespaces) != 0 {\n\t\tfor _, namespace := range namespaces {\n\t\t\tpods, err := client.Client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tfor _, pod := range pods.Items {\n\t\t\t\tvar podString string\n\t\t\t\tstatus := pod.Status.Phase\n\t\t\t\tname := pod.Name\n\t\t\t\tif len(namespaces) == 1 {\n\t\t\t\t\tpodString = fmt.Sprintf(\"%-50s %s\", name, status)\n\t\t\t\t} else {\n\t\t\t\t\tpodString = fmt.Sprintf(\"%-20s %-50s %s\", namespace, name, status)\n\t\t\t\t}\n\t\t\t\tpodList = append(podList, podString)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tpods, err := client.Client.CoreV1().Pods(\"\").List(context.Background(), metav1.ListOptions{})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, pod := range pods.Items {\n\t\t\tpodString := fmt.Sprintf(\"%-20s %-50s %s\", pod.Namespace, pod.Name, pod.Status.Phase)\n\t\t\tpodList = append(podList, podString)\n\t\t}\n\t}\n\n\treturn podList, nil\n}\n\n// get Deployments returns a string slice of pod strings\nfunc (client *clientInstance) getDeployments(namespaces []string) ([]string, error) {\n\tvar deploymentList []string\n\tif len(namespaces) != 0 {\n\t\tfor _, namespace := range namespaces {\n\t\t\tdeployments, err := client.Client.AppsV1().Deployments(namespace).List(context.Background(), metav1.ListOptions{})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tfor _, deployment := range deployments.Items {\n\t\t\t\tvar deployString string\n\t\t\t\tif len(namespaces) == 1 {\n\t\t\t\t\tdeployString = fmt.Sprintf(\"%-50s (%d/%d)\", deployment.Name, deployment.Status.ReadyReplicas, deployment.Status.Replicas)\n\t\t\t\t} else {\n\t\t\t\t\tdeployString = fmt.Sprintf(\"%-20s %-50s (%d/%d)\", deployment.Namespace, deployment.Name, deployment.Status.ReadyReplicas, deployment.Status.Replicas)\n\t\t\t\t}\n\t\t\t\tdeploymentList = append(deploymentList, deployString)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tdeployments, err := client.Client.AppsV1().Deployments(\"\").List(context.Background(), metav1.ListOptions{})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, deployment := range deployments.Items {\n\t\t\tdeployString := fmt.Sprintf(\"%-20s %-50s (%d/%d)\", deployment.Namespace, deployment.Name, deployment.Status.ReadyReplicas, deployment.Status.Replicas)\n\t\t\tdeploymentList = append(deploymentList, deployString)\n\t\t}\n\t}\n\treturn deploymentList, nil\n}\n\n// getNodes returns a string slice of nodes\nfunc (client *clientInstance) getNodes() ([]string, error) {\n\tvar nodeList []string\n\n\tnodes, err := client.Client.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, node := range nodes.Items {\n\t\tvar nodeStatus string\n\t\tfor _, condition := range node.Status.Conditions {\n\t\t\tif condition.Reason == \"KubeletReady\" {\n\t\t\t\tswitch {\n\t\t\t\tcase condition.Status == \"True\":\n\t\t\t\t\tnodeStatus = \"Ready\"\n\t\t\t\tcase condition.Reason == \"False\":\n\t\t\t\t\tnodeStatus = \"NotReady\"\n\t\t\t\tdefault:\n\t\t\t\t\tnodeStatus = \"Unknown\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tnodeString := fmt.Sprintf(\"%-50s %s\", node.Name, nodeStatus)\n\t\tnodeList = append(nodeList, nodeString)\n\t}\n\treturn nodeList, nil\n}\n"
  },
  {
    "path": "modules/kubernetes/widget_test.go",
    "content": "package kubernetes\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_generateTitle(t *testing.T) {\n\ttype fields struct {\n\t\ttitle      string\n\t\tnamespaces []string\n\t\tcontext    string\n\t}\n\n\ttestCases := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   string\n\t}{\n\t\t{\n\t\t\tname: \"No Namespaces\",\n\t\t\tfields: fields{\n\t\t\t\tnamespaces: []string{},\n\t\t\t},\n\t\t\twant: \"Kube\",\n\t\t},\n\t\t{\n\t\t\tname: \"One Namespace\",\n\t\t\tfields: fields{\n\t\t\t\tnamespaces: []string{\"some-namespace\"},\n\t\t\t},\n\t\t\twant: \"Kube - Namespace: some-namespace\",\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple Namespaces\",\n\t\t\tfields: fields{\n\t\t\t\tnamespaces: []string{\"ns1\", \"ns2\"},\n\t\t\t},\n\t\t\twant: `Kube - Namespaces: [\"ns1\" \"ns2\"]`,\n\t\t},\n\t\t{\n\t\t\tname: \"Explicit Title Set\",\n\t\t\tfields: fields{\n\t\t\t\tnamespaces: []string{},\n\t\t\t\ttitle:      \"Test Explicit Title\",\n\t\t\t},\n\t\t\twant: \"Test Explicit Title\",\n\t\t},\n\t\t{\n\t\t\tname: \"Context set\",\n\t\t\tfields: fields{\n\t\t\t\tnamespaces: []string{},\n\t\t\t\tcontext:    \"test-context\",\n\t\t\t},\n\t\t\twant: \"Kube (test-context)\",\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\twidget := &Widget{\n\t\t\t\ttitle:      tt.fields.title,\n\t\t\t\tnamespaces: tt.fields.namespaces,\n\t\t\t\tcontext:    tt.fields.context,\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, widget.generateTitle())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/logger/settings.go",
    "content": "package logger\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Logger\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/logger/widget.go",
    "content": "package logger\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\tlog \"github.com/wtfutil/wtf/logger\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst (\n\tmaxBufferSize int64 = 1024\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tfilePath string\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tfilePath: log.LogFilePath(),\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tif log.LogFileMissing() {\n\t\treturn widget.CommonSettings().Title, \"File missing\", false\n\t}\n\n\tlogLines := widget.tailFile()\n\tstr := \"\"\n\n\tfor _, line := range logLines {\n\t\tchunks := strings.Split(line, \" \")\n\n\t\tif len(chunks) >= 4 {\n\t\t\tstr += fmt.Sprintf(\n\t\t\t\t\"[green]%s[white] [yellow]%s[white] %s\\n\",\n\t\t\t\tchunks[0],\n\t\t\t\tchunks[1],\n\t\t\t\tstrings.Join(chunks[3:], \" \"),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn widget.CommonSettings().Title, str, false\n}\n\nfunc (widget *Widget) tailFile() []string {\n\tfile, err := os.Open(widget.filePath)\n\tif err != nil {\n\t\treturn []string{}\n\t}\n\tdefer func() { _ = file.Close() }()\n\n\tstat, err := file.Stat()\n\tif err != nil {\n\t\treturn []string{}\n\t}\n\n\tbufferSize := maxBufferSize\n\tif maxBufferSize > stat.Size() {\n\t\tbufferSize = stat.Size()\n\t}\n\n\tstartPos := stat.Size() - bufferSize\n\n\tbuff := make([]byte, bufferSize)\n\t_, err = file.ReadAt(buff, startPos)\n\tif err != nil {\n\t\treturn []string{}\n\t}\n\n\tlogLines := strings.Split(string(buff), \"\\n\")\n\n\t// Reverse the array of lines\n\t// Offset by two to account for the blank line at the end\n\tlast := len(logLines) - 2\n\tfor i := 0; i < len(logLines)/2; i++ {\n\t\tlogLines[i], logLines[last-i] = logLines[last-i], logLines[i]\n\t}\n\n\treturn logLines\n}\n"
  },
  {
    "path": "modules/lunarphase/keyboard.go",
    "content": "package lunarphase\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"n\", widget.NextDay, \"Show next day lunar phase\")\n\twidget.SetKeyboardChar(\"p\", widget.PrevDay, \"Show previous day lunar phase\")\n\twidget.SetKeyboardChar(\"t\", widget.Today, \"Show today lunar phase\")\n\twidget.SetKeyboardChar(\"N\", widget.NextWeek, \"Show next week lunar phase\")\n\twidget.SetKeyboardChar(\"P\", widget.PrevWeek, \"Show previous week lunar phase\")\n\twidget.SetKeyboardChar(\"o\", widget.OpenMoonPhase, \"Open 'Moon Phase for Today' in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevDay, \"Show previous day lunar phase\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextDay, \"Show next day lunar phase\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.NextWeek, \"Show next week lunar phase\")\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.PrevWeek, \"Show previous week lunar phase\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.OpenMoonPhase, \"Open 'Moon Phase for Today' in browser\")\n\twidget.SetKeyboardKey(tcell.KeyCtrlD, widget.DisableWidget, \"Disable/Enable this widget instance\")\n}\n"
  },
  {
    "path": "modules/lunarphase/settings.go",
    "content": "package lunarphase\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Phase of the Moon\"\n\tdateFormat       = \"2006-01-02\"\n\tphaseFormat      = \"01-02-2006\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tlanguage       string\n\trequestTimeout int\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tlanguage:       ymlConfig.UString(\"language\", \"en\"),\n\t\trequestTimeout: ymlConfig.UInt(\"timeout\", 30),\n\t}\n\n\tsettings.SetDocumentationPath(\"lunarphase\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/lunarphase/widget.go",
    "content": "package lunarphase\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tcurrent   bool\n\tday       string\n\tdate      time.Time\n\tlast      string\n\tresult    string\n\tsettings  *Settings\n\ttimeout   time.Duration\n\ttitleBase string\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\t\tsettings:         settings,\n\t}\n\n\twidget.current = true\n\twidget.date = time.Now()\n\twidget.day = widget.date.Format(dateFormat)\n\twidget.last = \"\"\n\twidget.timeout = time.Duration(widget.settings.requestTimeout) * time.Second\n\twidget.titleBase = widget.settings.Title\n\n\twidget.SetRenderFunction(widget.Refresh)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\nfunc (widget *Widget) Refresh() {\n\tif widget.current {\n\t\twidget.date = time.Now()\n\t\twidget.day = widget.date.Format(dateFormat)\n\t}\n\tif widget.day != widget.last {\n\t\twidget.lunarPhase()\n\t}\n\n\tif !widget.settings.Enabled {\n\t\twidget.settings.Title = widget.titleBase + \" \" + widget.day + \" [ Disabled ]\"\n\t\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, \"\", false })\n\t\twidget.View.Clear()\n\t\treturn\n\t}\n\twidget.settings.Title = widget.titleBase + \" \" + widget.day\n\n\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, widget.result, false })\n}\n\nfunc (widget *Widget) RefreshTitle() {\n\tif !widget.settings.Enabled {\n\t\twidget.settings.Title = widget.titleBase + \" \" + widget.day + \" [ Disabled ]\"\n\t\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, \"\", false })\n\t\twidget.View.Clear()\n\t\treturn\n\t}\n\twidget.settings.Title = widget.titleBase + \" [\" + widget.day + \"]\"\n\n\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, widget.result, false })\n}\n\n// this method reads the config and calls wttr.in for lunar phase\nfunc (widget *Widget) lunarPhase() {\n\tclient := &http.Client{\n\t\tTimeout: widget.timeout,\n\t}\n\n\tlanguage := widget.settings.language\n\n\treq, err := http.NewRequest(\"GET\", \"https://wttr.in/Moon@\"+widget.day+\"?AF&lang=\"+language, http.NoBody)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\n\treq.Header.Set(\"Accept-Language\", widget.settings.language)\n\treq.Header.Set(\"User-Agent\", \"curl\")\n\tresponse, err := client.Do(req)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\tdefer func() { _ = response.Body.Close() }()\n\n\tcontents, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\n\twidget.last = widget.day\n\twidget.result = strings.TrimSpace(wtf.ASCIItoTviewColors(string(contents)))\n}\n\n// NextDay shows the next day's lunar phase (KeyRight / 'n')\nfunc (widget *Widget) NextDay() {\n\twidget.current = false\n\ttomorrow := widget.date.AddDate(0, 0, 1)\n\twidget.setDay(tomorrow)\n}\n\n// NextWeek shows the next week's lunar phase (KeyUp / 'N')\nfunc (widget *Widget) NextWeek() {\n\twidget.current = false\n\tnextweek := widget.date.AddDate(0, 0, 7)\n\twidget.setDay(nextweek)\n}\n\n// PrevDay shows the previous day's lunar phase (KeyLeft / 'p')\nfunc (widget *Widget) PrevDay() {\n\twidget.current = false\n\tyesterday := widget.date.AddDate(0, 0, -1)\n\twidget.setDay(yesterday)\n}\n\n// Today shows the current day's lunar phase ('t')\nfunc (widget *Widget) Today() {\n\twidget.current = true\n\twidget.Refresh()\n}\n\n// PrevWeek shows the previous week's lunar phase (KeyDown / 'P')\nfunc (widget *Widget) PrevWeek() {\n\twidget.current = false\n\tlastweek := widget.date.AddDate(0, 0, -7)\n\twidget.setDay(lastweek)\n}\n\nfunc (widget *Widget) setDay(ts time.Time) {\n\twidget.date = ts\n\twidget.day = widget.date.Format(dateFormat)\n\twidget.RefreshTitle()\n}\n\n// Open nineplanets.org in a browser (Enter / 'o')\nfunc (widget *Widget) OpenMoonPhase() {\n\tphasedate := widget.date.Format(phaseFormat)\n\tutils.OpenFile(\"https://nineplanets.org/moon/phase/\" + phasedate + \"/\")\n}\n\n// Disable/Enable the widget (Ctrl-D)\nfunc (widget *Widget) DisableWidget() {\n\tif widget.settings.Enabled {\n\t\twidget.settings.Enabled = false\n\t\twidget.RefreshTitle()\n\t} else {\n\t\twidget.settings.Enabled = true\n\t\twidget.Refresh()\n\t}\n}\n"
  },
  {
    "path": "modules/mercurial/display.go",
    "content": "package mercurial\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\trepoData := widget.currentData()\n\tif repoData == nil {\n\t\treturn widget.CommonSettings().Title, \" Mercurial repo data is unavailable \", false\n\t}\n\n\ttitle := fmt.Sprintf(\n\t\t\"%s - %s[white]\",\n\t\twidget.settings.Colors.Title,\n\t\trepoData.Repository,\n\t)\n\n\t_, _, width, _ := widget.View.GetRect()\n\tstr := widget.settings.PaginationMarker(len(widget.Data), widget.Idx, width) + \"\\n\"\n\tstr += fmt.Sprintf(\" [%s]Branch:Bookmark[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += fmt.Sprintf(\" %s:%s\\n\", repoData.Branch, repoData.Bookmark)\n\tstr += \"\\n\"\n\tstr += widget.formatChanges(repoData.ChangedFiles)\n\tstr += \"\\n\"\n\tstr += widget.formatCommits(repoData.Commits)\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) formatChanges(data []string) string {\n\tstr := fmt.Sprintf(\" [%s]Changed Files[white]\\n\", widget.settings.Colors.Subheading)\n\n\tif len(data) == 1 {\n\t\tstr += \" [grey]none[white]\\n\"\n\t} else {\n\t\tfor _, line := range data {\n\t\t\tstr += widget.formatChange(line)\n\t\t}\n\t}\n\n\treturn str\n}\n\nfunc (widget *Widget) formatChange(line string) string {\n\tif line == \"\" {\n\t\treturn \"\"\n\t}\n\n\tline = strings.TrimSpace(line)\n\tfirstChar, _ := utf8.DecodeRuneInString(line)\n\n\t// Revisit this and kill the ugly duplication\n\tswitch firstChar {\n\tcase 'A':\n\t\tline = strings.Replace(line, \"A\", \"[green]A[white]\", 1)\n\tcase 'D':\n\t\tline = strings.Replace(line, \"D\", \"[red]D[white]\", 1)\n\tcase 'M':\n\t\tline = strings.Replace(line, \"M\", \"[yellow]M[white]\", 1)\n\tcase 'R':\n\t\tline = strings.Replace(line, \"R\", \"[purple]R[white]\", 1)\n\t}\n\n\treturn fmt.Sprintf(\" %s\\n\", strings.ReplaceAll(line, \"\\\"\", \"\"))\n}\n\nfunc (widget *Widget) formatCommits(data []string) string {\n\tstr := fmt.Sprintf(\" [%s]Recent Commits[white]\\n\", widget.settings.Colors.Subheading)\n\n\tfor _, line := range data {\n\t\tstr += widget.formatCommit(line)\n\t}\n\n\treturn str\n}\n\nfunc (widget *Widget) formatCommit(line string) string {\n\treturn fmt.Sprintf(\" %s\\n\", strings.ReplaceAll(line, \"\\\"\", \"\"))\n}\n"
  },
  {
    "path": "modules/mercurial/hg_repo.go",
    "content": "package mercurial\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\ntype MercurialRepo struct {\n\tBranch       string\n\tBookmark     string\n\tChangedFiles []string\n\tCommits      []string\n\tRepository   string\n\tPath         string\n}\n\nfunc NewMercurialRepo(repoPath string, commitCount int, commitFormat string) *MercurialRepo {\n\trepo := MercurialRepo{Path: repoPath}\n\n\trepo.Branch = strings.TrimSpace(repo.branch())\n\trepo.Bookmark = strings.TrimSpace(repo.bookmark())\n\trepo.ChangedFiles = repo.changedFiles()\n\trepo.Commits = repo.commits(commitCount, commitFormat)\n\trepo.Repository = strings.TrimSpace(repo.Path)\n\n\treturn &repo\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (repo *MercurialRepo) branch() string {\n\targ := []string{\"branch\", repo.repoPath()}\n\n\tcmd := exec.Command(\"hg\", arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\n\treturn str\n}\n\nfunc (repo *MercurialRepo) bookmark() string {\n\tbookmark, err := os.ReadFile(path.Join(repo.Path, \".hg\", \"bookmarks.current\"))\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn string(bookmark)\n}\n\nfunc (repo *MercurialRepo) changedFiles() []string {\n\targ := []string{\"status\", repo.repoPath()}\n\n\tcmd := exec.Command(\"hg\", arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\n\tdata := strings.Split(str, \"\\n\")\n\n\treturn data\n}\n\nfunc (repo *MercurialRepo) commits(commitCount int, commitFormat string) []string {\n\tnumStr := fmt.Sprintf(\"-l %d\", commitCount)\n\tcommitStr := fmt.Sprintf(\"--template=\\\"%s\\n\\\"\", commitFormat)\n\n\targ := []string{\"log\", repo.repoPath(), numStr, commitStr}\n\n\tcmd := exec.Command(\"hg\", arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\n\tdata := strings.Split(str, \"\\n\")\n\n\treturn data\n}\n\nfunc (repo *MercurialRepo) pull() string {\n\targ := []string{\"pull\", repo.repoPath()}\n\tcmd := exec.Command(\"hg\", arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\treturn str\n}\n\nfunc (repo *MercurialRepo) checkout(branch string) string {\n\targ := []string{\"checkout\", repo.repoPath(), branch}\n\tcmd := exec.Command(\"hg\", arg...)\n\tstr := utils.ExecuteCommand(cmd)\n\treturn str\n}\n\nfunc (repo *MercurialRepo) repoPath() string {\n\treturn fmt.Sprintf(\"--repository=%s\", repo.Path)\n}\n"
  },
  {
    "path": "modules/mercurial/keyboard.go",
    "content": "package mercurial\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardChar(\"p\", widget.Pull, \"Pull repo\")\n\twidget.SetKeyboardChar(\"c\", widget.Checkout, \"Checkout branch\")\n\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous source\")\n}\n"
  },
  {
    "path": "modules/mercurial/settings.go",
    "content": "package mercurial\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Mercurial\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcommitCount  int           `help:\"The number of past commits to display.\" optional:\"true\"`\n\tcommitFormat string        `help:\"The string format for the commit message.\" optional:\"true\"`\n\trepositories []interface{} `help:\"Defines which mercurial repositories to watch.\" values:\"A list of zero or more local file paths pointing to valid mercurial repositories.\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tcommitCount:  ymlConfig.UInt(\"commitCount\", 10),\n\t\tcommitFormat: ymlConfig.UString(\"commitFormat\", \"[forestgreen]{rev}:{phase} [white]{desc|firstline|strip} [grey]{author|person} {date|age}[white]\"),\n\t\trepositories: ymlConfig.UList(\"repositories\"),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/mercurial/widget.go",
    "content": "package mercurial\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst (\n\tmodalHeight = 7\n\tmodalWidth  = 80\n\toffscreen   = -1000\n)\n\n// A Widget represents a Mercurial widget\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tData     []*MercurialRepo\n\tpages    *tview.Pages\n\tsettings *Settings\n\ttviewApp *tview.Application\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"repository\", \"repositories\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\ttviewApp: tviewApp,\n\t\tpages:    pages,\n\t\tsettings: settings,\n\t}\n\n\twidget.SetDisplayFunction(widget.display)\n\n\twidget.initializeKeyboardControls()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Checkout() {\n\tform := widget.modalForm(\"Branch to checkout:\", \"\")\n\n\tcheckoutFctn := func() {\n\t\ttext := form.GetFormItem(0).(*tview.InputField).GetText()\n\t\trepoToCheckout := widget.Data[widget.Idx]\n\t\trepoToCheckout.checkout(text)\n\t\twidget.pages.RemovePage(\"modal\")\n\t\twidget.tviewApp.SetFocus(widget.View)\n\n\t\twidget.display()\n\n\t\twidget.Refresh()\n\t}\n\n\twidget.addButtons(form, checkoutFctn)\n\twidget.modalFocus(form)\n}\n\nfunc (widget *Widget) Pull() {\n\trepoToPull := widget.Data[widget.Idx]\n\trepoToPull.pull()\n\twidget.Refresh()\n}\n\nfunc (widget *Widget) Refresh() {\n\trepoPaths := utils.ToStrs(widget.settings.repositories)\n\n\twidget.Data = widget.mercurialRepos(repoPaths)\n\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) addCheckoutButton(form *tview.Form, fctn func()) {\n\tform.AddButton(\"Checkout\", fctn)\n}\n\nfunc (widget *Widget) addButtons(form *tview.Form, checkoutFctn func()) {\n\twidget.addCheckoutButton(form, checkoutFctn)\n\twidget.addCancelButton(form)\n}\n\nfunc (widget *Widget) addCancelButton(form *tview.Form) {\n\tcancelFn := func() {\n\t\twidget.pages.RemovePage(\"modal\")\n\t\twidget.tviewApp.SetFocus(widget.View)\n\t\twidget.display()\n\t}\n\n\tform.AddButton(\"Cancel\", cancelFn)\n\tform.SetCancelFunc(cancelFn)\n}\n\nfunc (widget *Widget) modalFocus(form *tview.Form) {\n\tframe := widget.modalFrame(form)\n\twidget.pages.AddPage(\"modal\", frame, false, true)\n\twidget.tviewApp.SetFocus(frame)\n}\n\nfunc (widget *Widget) modalForm(lbl, text string) *tview.Form {\n\tform := tview.NewForm()\n\tform.SetButtonsAlign(tview.AlignCenter)\n\tform.SetButtonTextColor(tview.Styles.PrimaryTextColor)\n\n\tform.AddInputField(lbl, text, 60, nil, nil)\n\n\treturn form\n}\n\nfunc (widget *Widget) modalFrame(form *tview.Form) *tview.Frame {\n\tframe := tview.NewFrame(form)\n\tframe.SetBorders(0, 0, 0, 0, 0, 0)\n\tframe.SetRect(offscreen, offscreen, modalWidth, modalHeight)\n\tframe.SetBorder(true)\n\tframe.SetBorders(1, 1, 0, 0, 1, 1)\n\n\tdrawFunc := func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {\n\t\tw, h := screen.Size()\n\t\tframe.SetRect((w/2)-(width/2), (h/2)-(height/2), width, height)\n\t\treturn x, y, width, height\n\t}\n\n\tframe.SetDrawFunc(drawFunc)\n\n\treturn frame\n}\n\nfunc (widget *Widget) currentData() *MercurialRepo {\n\tif len(widget.Data) == 0 {\n\t\treturn nil\n\t}\n\n\tif widget.Idx < 0 || widget.Idx >= len(widget.Data) {\n\t\treturn nil\n\t}\n\n\treturn widget.Data[widget.Idx]\n}\n\nfunc (widget *Widget) mercurialRepos(repoPaths []string) []*MercurialRepo {\n\trepos := []*MercurialRepo{}\n\n\tfor _, repoPath := range repoPaths {\n\t\trepo := NewMercurialRepo(repoPath, widget.settings.commitCount, widget.settings.commitFormat)\n\t\trepos = append(repos, repo)\n\t}\n\n\treturn repos\n}\n"
  },
  {
    "path": "modules/nbascore/keyboard.go",
    "content": "package nbascore\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"l\", widget.next, \"Select next item\")\n\twidget.SetKeyboardChar(\"h\", widget.prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"c\", widget.center, \"Center on item\")\n\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.prev, \"Select previous item\")\n}\n\nfunc (widget *Widget) center() {\n\toffset = 0\n\twidget.Refresh()\n}\n\nfunc (widget *Widget) next() {\n\toffset++\n\twidget.Refresh()\n}\n\nfunc (widget *Widget) prev() {\n\toffset--\n\twidget.Refresh()\n}\n"
  },
  {
    "path": "modules/nbascore/settings.go",
    "content": "package nbascore\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"NBA Score\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\tsettings.SetDocumentationPath(\"sports/nbascore\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/nbascore/widget.go",
    "content": "package nbascore\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nvar offset = 0\n\n// A Widget represents an NBA Score  widget\ntype Widget struct {\n\tview.TextWidget\n\n\tlanguage string\n\tsettings *Settings\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\n\twidget.View.SetScrollable(true)\n\n\treturn &widget\n}\n\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.nbascore)\n}\n\nfunc (widget *Widget) nbascore() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\tcur := time.Now().AddDate(0, 0, offset) // Go back/forward offset days\n\tcurString := cur.Format(\"20060102\")     // Need 20060102 format to feed to api\n\tclient := &http.Client{}\n\treq, err := http.NewRequest(\"GET\", \"http://data.nba.net/10s/prod/v1/\"+curString+\"/scoreboard.json\", http.NoBody)\n\tif err != nil {\n\t\treturn title, err.Error(), true\n\t}\n\n\treq.Header.Set(\"Accept-Language\", widget.language)\n\treq.Header.Set(\"User-Agent\", \"curl\")\n\tresponse, err := client.Do(req)\n\tif err != nil {\n\t\treturn title, err.Error(), true\n\t}\n\tdefer func() { _ = response.Body.Close() }()\n\tif response.StatusCode != 200 {\n\t\treturn title, err.Error(), true\n\t} // Get data from data.nba.net and check if successful\n\n\tcontents, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn title, err.Error(), true\n\t}\n\tresult := map[string]interface{}{}\n\terr = json.Unmarshal(contents, &result)\n\tif err != nil {\n\t\treturn title, err.Error(), true\n\t}\n\n\tallGame := fmt.Sprintf(\" [%s]\", widget.settings.Colors.Subheading) + (cur.Format(utils.FriendlyDateFormat) + \"\\n\\n\") + \"[white]\"\n\n\tfor _, game := range result[\"games\"].([]interface{}) {\n\t\tvTeam, hTeam, vScore, hScore := \"\", \"\", \"\", \"\"\n\t\tquarter := 0.\n\t\tactivate := false\n\t\tfor keyGame, team := range game.(map[string]interface{}) { // assertion\n\t\t\tswitch keyGame {\n\t\t\tcase \"vTeam\", \"hTeam\":\n\t\t\t\tfor keyTeam, stat := range team.(map[string]interface{}) {\n\t\t\t\t\tswitch keyTeam {\n\t\t\t\t\tcase \"triCode\":\n\t\t\t\t\t\tif keyGame == \"vTeam\" {\n\t\t\t\t\t\t\tvTeam = stat.(string)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\thTeam = stat.(string)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase \"score\":\n\t\t\t\t\t\tif keyGame == \"vTeam\" {\n\t\t\t\t\t\t\tvScore = stat.(string)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\thScore = stat.(string)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase \"period\":\n\t\t\t\tfor keyTeam, stat := range team.(map[string]interface{}) {\n\t\t\t\t\tif keyTeam == \"current\" {\n\t\t\t\t\t\tquarter = stat.(float64)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase \"isGameActivated\":\n\t\t\t\tactivate = team.(bool)\n\t\t\t}\n\t\t}\n\t\tvNum, _ := strconv.Atoi(vScore)\n\t\thNum, _ := strconv.Atoi(hScore)\n\t\thColor := \"\"\n\t\tif quarter != 0 { // Compare the score\n\t\t\tswitch {\n\t\t\tcase vNum > hNum:\n\t\t\t\tvTeam = \"[orange]\" + vTeam\n\t\t\tcase hNum > vNum:\n\t\t\t\t// hScore = \"[orange]\" + hScore\n\t\t\t\thColor = \"[orange]\" // For correct padding\n\t\t\t\thTeam += \"[white]\"\n\t\t\tdefault:\n\t\t\t\tvTeam = \"[orange]\" + vTeam\n\t\t\t\thColor = \"[orange]\"\n\t\t\t\thTeam += \"[white]\"\n\t\t\t}\n\t\t}\n\t\tqColor := \"[white]\"\n\t\tif activate {\n\t\t\tqColor = \"[sandybrown]\"\n\t\t}\n\t\tallGame += fmt.Sprintf(\"%s%5s%v[white] %s %3s [white]vs %s%-3s %s\\n\", qColor, \"Q\", quarter, vTeam, vScore, hColor, hScore, hTeam) // Format the score and store in allgame\n\t}\n\treturn title, allGame, false\n}\n"
  },
  {
    "path": "modules/newrelic/client/README.md",
    "content": "[![GoDoc](http://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/yfronto/newrelic)\n[![Build\nstatus](https://travis-ci.org/yfronto/newrelic.svg)](https://travis-ci.org/yfronto/newrelic)\n\n# New Relic API library for Go\n\nThis is a Go library that wraps the [New Relic][1] REST\nAPI. It provides the needed types to interact with the New Relic REST API.\n\nIt's still in progress and I haven't finished the entirety of the API, yet. I\nplan to finish all GET (read) operations before any POST (create) operations,\nand then PUT (update) operations, and, finally, the DELETE operations.\n\nThe API documentation can be found from [New Relic][1],\nand you'll need an API key (for some operations, an Admin API key is\nrequired).\n\n## USAGE\n\nThis library will provide a client object and any operations can be performed\nthrough it. Simply import this library and create a client to get started:\n\n```go\npackage main\n\nimport (\n  \"github.com/yfronto/newrelic\"\n)\n\nvar api_key = \"...\" // Required\n\nfunc main() {\n  // Create the client object\n  client := newrelic.NewClient(api_key)\n\n  // Get the applciation with ID 12345\n  myApp, err := client.GetApplication(12345)\n  if err != nil {\n    // Handle error\n  }\n\n  // Work with the object\n  fmt.Println(myApp.Name)\n\n  // Some operations accept options\n  opts := &newrelic.AlertEventOptions{\n    // Only events with \"MyProduct\" as the product name\n    Filter: newRelic.AlertEventFilter{\n      Product: \"MyProduct\",\n    },\n  }\n  // Get a list of recent events for my product\n  events, err := client.GetAlertEvents(opts)\n  if err != nil {\n    // Handle error\n  }\n  // Display each event with some information\n  for _, e := range events {\n    fmt.Printf(\"%d -- %d (%s): %s\\n\", e.Timestamp, e.Id, e.Priority, e.Description)\n  }\n}\n```\n\n## Contributing\n\nAs I work to populate all actions, bugs are bound to come up. Feel free to\nsend me a pull request or just file an issue. Staying up to date with an API\nis hard work and I'm happy to accept contributors.\n\n**DISCLAIMER:** *I am in no way affiliated with New Relic and this work is\nmerely a convenience project for myself with no guarantees. It should be\nconsidered \"as-is\" with no implication of responsibility. See the included\nLICENSE for more details.*\n\n[1]: http://www.newrelic.com"
  },
  {
    "path": "modules/newrelic/client/alert_conditions.go",
    "content": "package newrelic\n\n// AlertCondition describes what triggers an alert for a specific policy.\ntype AlertCondition struct {\n\tEnabled     bool                 `json:\"enabled,omitempty\"`\n\tEntities    []string             `json:\"entities,omitempty\"`\n\tID          int                  `json:\"id,omitempty\"`\n\tMetric      string               `json:\"metric,omitempty\"`\n\tName        string               `json:\"name,omitempty\"`\n\tRunbookURL  string               `json:\"runbook_url,omitempty\"`\n\tTerms       []AlertConditionTerm `json:\"terms,omitempty\"`\n\tType        string               `json:\"type,omitempty\"`\n\tUserDefined AlertUserDefined     `json:\"user_defined,omitempty\"`\n}\n\n// AlertConditionTerm defines thresholds that trigger an AlertCondition.\ntype AlertConditionTerm struct {\n\tDuration     string `json:\"duration,omitempty\"`\n\tOperator     string `json:\"operator,omitempty\"`\n\tPriority     string `json:\"priority,omitempty\"`\n\tThreshold    string `json:\"threshold,omitempty\"`\n\tTimeFunction string `json:\"time_function,omitempty\"`\n}\n\n// AlertUserDefined describes user-defined behavior for an AlertCondition.\ntype AlertUserDefined struct {\n\tMetric        string `json:\"metric,omitempty\"`\n\tValueFunction string `json:\"value_function,omitempty\"`\n}\n\n// AlertConditionOptions define filters for GetAlertConditions.\ntype AlertConditionOptions struct {\n\tpolicyID int\n\tPage     int\n}\n\nfunc (o *AlertConditionOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"policy_id\": o.policyID,\n\t\t\"page\":      o.Page,\n\t})\n}\n\n// GetAlertConditions will return any AlertCondition defined for a given\n// policy, optionally filtered by AlertConditionOptions.\nfunc (c *Client) GetAlertConditions(policy int, options *AlertConditionOptions) ([]AlertCondition, error) {\n\tresp := &struct {\n\t\tConditions []AlertCondition `json:\"conditions,omitempty\"`\n\t}{}\n\toptions.policyID = policy\n\terr := c.doGet(\"alerts_conditions.json\", options, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Conditions, nil\n}\n"
  },
  {
    "path": "modules/newrelic/client/alert_events.go",
    "content": "package newrelic\n\n// AlertEvent describes a triggered event.\ntype AlertEvent struct {\n\tID            int    `json:\"id,omitempty\"`\n\tEventType     string `json:\"event_type,omitempty\"`\n\tProduct       string `json:\"product,omitempty\"`\n\tEntityType    string `json:\"entity_type,omitempty\"`\n\tEntityGroupID int    `json:\"entity_group_id,omitempty\"`\n\tEntityID      int    `json:\"entity_id,omitempty\"`\n\tPriority      string `json:\"priority,omitempty\"`\n\tDescription   string `json:\"description,omitempty\"`\n\tTimestamp     int64  `json:\"timestamp,omitempty\"`\n\tIncidentID    int    `json:\"incident_id\"`\n}\n\n// AlertEventFilter provides filters for AlertEventOptions when calling\n// GetAlertEvents.\ntype AlertEventFilter struct {\n\t// TODO: New relic restricts these options\n\tProduct       string\n\tEntityType    string\n\tEntityGroupID int\n\tEntityID      int\n\tEventType     string\n}\n\n// AlertEventOptions is an optional means of filtering AlertEvents when\n// calling GetAlertEvents.\ntype AlertEventOptions struct {\n\tFilter AlertEventFilter\n\tPage   int\n}\n\nfunc (o *AlertEventOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"filter[product]\":         o.Filter.Product,\n\t\t\"filter[entity_type]\":     o.Filter.EntityType,\n\t\t\"filter[entity_group_id]\": o.Filter.EntityGroupID,\n\t\t\"filter[entity_id]\":       o.Filter.EntityID,\n\t\t\"filter[event_type]\":      o.Filter.EventType,\n\t\t\"page\":                    o.Page,\n\t})\n}\n\n// GetAlertEvents will return a slice of recent AlertEvent items triggered,\n// optionally filtering by AlertEventOptions.\nfunc (c *Client) GetAlertEvents(options *AlertEventOptions) ([]AlertEvent, error) {\n\tresp := &struct {\n\t\tRecentEvents []AlertEvent `json:\"recent_events,omitempty\"`\n\t}{}\n\terr := c.doGet(\"alerts_events.json\", options, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.RecentEvents, nil\n}\n"
  },
  {
    "path": "modules/newrelic/client/application_deployments.go",
    "content": "package newrelic\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\n// ApplicationDeploymentLinks represents links that apply to an\n// ApplicationDeployment.\ntype ApplicationDeploymentLinks struct {\n\tApplication int `json:\"application,omitempty\"`\n}\n\n// ApplicationDeploymentOptions provide a means to filter when calling\n// GetApplicationDeployments.\ntype ApplicationDeploymentOptions struct {\n\tPage int\n}\n\n// ApplicationDeployment contains information about a New Relic Application\n// Deployment.\ntype ApplicationDeployment struct {\n\tID          int                        `json:\"id,omitempty\"`\n\tRevision    string                     `json:\"revision,omitempty\"`\n\tChangelog   string                     `json:\"changelog,omitempty\"`\n\tDescription string                     `json:\"description,omitempty\"`\n\tUser        string                     `json:\"user,omitempty\"`\n\tTimestamp   time.Time                  `json:\"timestamp,omitempty\"`\n\tLinks       ApplicationDeploymentLinks `json:\"links,omitempty\"`\n}\n\n// GetApplicationDeployments returns a slice of New Relic Application\n// Deployments.\nfunc (c *Client) GetApplicationDeployments(id int, opt *ApplicationDeploymentOptions) ([]ApplicationDeployment, error) {\n\tresp := &struct {\n\t\tDeployments []ApplicationDeployment `json:\"deployments,omitempty\"`\n\t}{}\n\tpath := \"applications/\" + strconv.Itoa(id) + \"/deployments.json\"\n\terr := c.doGet(path, opt, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Deployments, nil\n}\n\nfunc (o *ApplicationDeploymentOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"page\": o.Page,\n\t})\n}\n"
  },
  {
    "path": "modules/newrelic/client/application_host_metrics.go",
    "content": "package newrelic\n\nimport (\n\t\"fmt\"\n)\n\n// GetApplicationHostMetrics will return a slice of Metric items for a\n// particular Application ID's Host ID, optionally filtering by\n// MetricsOptions.\nfunc (c *Client) GetApplicationHostMetrics(appID, hostID int, options *MetricsOptions) ([]Metric, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetrics(\n\t\tfmt.Sprintf(\n\t\t\t\"applications/%d/hosts/%d/metrics.json\",\n\t\t\tappID,\n\t\t\thostID,\n\t\t),\n\t\toptions,\n\t)\n}\n\n// GetApplicationHostMetricData will return all metric data for a particular\n// application's host and slice of metric names, optionally filtered by\n// MetricDataOptions.\nfunc (c *Client) GetApplicationHostMetricData(appID, hostID int, names []string, options *MetricDataOptions) (*MetricDataResponse, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetricData(\n\t\tfmt.Sprintf(\n\t\t\t\"applications/%d/hosts/%d/metrics/data.json\",\n\t\t\tappID,\n\t\t\thostID,\n\t\t),\n\t\tnames,\n\t\toptions,\n\t)\n}\n"
  },
  {
    "path": "modules/newrelic/client/application_hosts.go",
    "content": "package newrelic\n\nimport (\n\t\"strconv\"\n)\n\n// ApplicationHostSummary describes an Application's host.\ntype ApplicationHostSummary struct {\n\tApdexScore    float64 `json:\"apdex_score,omitempty\"`\n\tErrorRate     float64 `json:\"error_rate,omitempty\"`\n\tInstanceCount int     `json:\"instance_count,omitempty\"`\n\tResponseTime  float64 `json:\"response_time,omitempty\"`\n\tThroughput    float64 `json:\"throughput,omitempty\"`\n}\n\n// ApplicationHostEndUserSummary describes the end user summary component of\n// an ApplicationHost.\ntype ApplicationHostEndUserSummary struct {\n\tResponseTime float64 `json:\"response_time,omitempty\"`\n\tThroughput   float64 `json:\"throughput,omitempty\"`\n\tApdexScore   float64 `json:\"apdex_score,omitempty\"`\n}\n\n// ApplicationHostLinks list IDs associated with an ApplicationHost.\ntype ApplicationHostLinks struct {\n\tApplication          int   `json:\"application,omitempty\"`\n\tApplicationInstances []int `json:\"application_instances,omitempty\"`\n\tServer               int   `json:\"server,omitempty\"`\n}\n\n// ApplicationHost describes a New Relic Application Host.\ntype ApplicationHost struct {\n\tApplicationName    string                        `json:\"application_name,omitempty\"`\n\tApplicationSummary ApplicationHostSummary        `json:\"application_summary,omitempty\"`\n\tHealthStatus       string                        `json:\"health_status,omitempty\"`\n\tHost               string                        `json:\"host,omitempty\"`\n\tID                 int                           `json:\"idomitempty\"`\n\tLanguage           string                        `json:\"language,omitempty\"`\n\tLinks              ApplicationHostLinks          `json:\"links,omitempty\"`\n\tEndUserSummary     ApplicationHostEndUserSummary `json:\"end_user_summary,omitempty\"`\n}\n\n// ApplicationHostsFilter provides a means to filter requests through\n// ApplicationHostsOptions when calling GetApplicationHosts.\ntype ApplicationHostsFilter struct {\n\tHostname string\n\tIDs      []int\n}\n\n// ApplicationHostsOptions provide a means to filter results when calling\n// GetApplicationHosts.\ntype ApplicationHostsOptions struct {\n\tFilter ApplicationHostsFilter\n\tPage   int\n}\n\n// GetApplicationHosts returns a slice of New Relic Application Hosts,\n// optionally filtering by ApplicationHostOptions.\nfunc (c *Client) GetApplicationHosts(id int, options *ApplicationHostsOptions) ([]ApplicationHost, error) {\n\tresp := &struct {\n\t\tApplicationHosts []ApplicationHost `json:\"application_hosts,omitempty\"`\n\t}{}\n\tpath := \"applications/\" + strconv.Itoa(id) + \"/hosts.json\"\n\terr := c.doGet(path, options, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.ApplicationHosts, nil\n}\n\n// GetApplicationHost returns a single Application Host associated with the\n// given application host ID and host ID.\nfunc (c *Client) GetApplicationHost(appID, hostID int) (*ApplicationHost, error) {\n\tresp := &struct {\n\t\tApplicationHost ApplicationHost `json:\"application_host,omitempty\"`\n\t}{}\n\tpath := \"applications/\" + strconv.Itoa(appID) + \"/hosts/\" + strconv.Itoa(hostID) + \".json\"\n\terr := c.doGet(path, nil, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp.ApplicationHost, nil\n}\n\nfunc (o *ApplicationHostsOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"filter[hostname]\": o.Filter.Hostname,\n\t\t\"filter[ids]\":      o.Filter.IDs,\n\t\t\"page\":             o.Page,\n\t})\n}\n"
  },
  {
    "path": "modules/newrelic/client/application_instance_metrics.go",
    "content": "package newrelic\n\nimport (\n\t\"fmt\"\n)\n\n// GetApplicationInstanceMetrics will return a slice of Metric items for a\n// particular Application ID's instance ID, optionally filtering by\n// MetricsOptions.\nfunc (c *Client) GetApplicationInstanceMetrics(appID, instanceID int, options *MetricsOptions) ([]Metric, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetrics(\n\t\tfmt.Sprintf(\n\t\t\t\"applications/%d/instances/%d/metrics.json\",\n\t\t\tappID,\n\t\t\tinstanceID,\n\t\t),\n\t\toptions,\n\t)\n}\n\n// GetApplicationInstanceMetricData will return all metric data for a\n// particular application's instance and slice of metric names, optionally\n// filtered by MetricDataOptions.\nfunc (c *Client) GetApplicationInstanceMetricData(appID, instanceID int, names []string, options *MetricDataOptions) (*MetricDataResponse, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetricData(\n\t\tfmt.Sprintf(\n\t\t\t\"applications/%d/instances/%d/metrics/data.json\",\n\t\t\tappID,\n\t\t\tinstanceID,\n\t\t),\n\t\tnames,\n\t\toptions,\n\t)\n}\n"
  },
  {
    "path": "modules/newrelic/client/application_instances.go",
    "content": "package newrelic\n\nimport (\n\t\"strconv\"\n)\n\n// ApplicationInstanceSummary describes an Application's instance.\ntype ApplicationInstanceSummary struct {\n\tResponseTime  float64 `json:\"response_time,omitempty\"`\n\tThroughput    float64 `json:\"throughput,omitempty\"`\n\tErrorRate     float64 `json:\"error_rate,omitempty\"`\n\tApdexScore    float64 `json:\"apdex_score,omitempty\"`\n\tInstanceCount int     `json:\"instance_count,omitempty\"`\n}\n\n// ApplicationInstanceEndUserSummary describes the end user summary component\n// of an ApplicationInstance.\ntype ApplicationInstanceEndUserSummary struct {\n\tResponseTime float64 `json:\"response_time,omitempty\"`\n\tThroughput   float64 `json:\"throughput,omitempty\"`\n\tApdexScore   float64 `json:\"apdex_score,omitempty\"`\n}\n\n// ApplicationInstanceLinks lists IDs associated with an ApplicationInstances.\ntype ApplicationInstanceLinks struct {\n\tApplication     int `json:\"application,omitempty\"`\n\tApplicationHost int `json:\"application_host,omitempty\"`\n\tServer          int `json:\"server,omitempty\"`\n}\n\n// ApplicationInstance describes a New Relic Application instance.\ntype ApplicationInstance struct {\n\tID                 int                               `json:\"id,omitempty\"`\n\tApplicationName    string                            `json:\"application_name,omitempty\"`\n\tHost               string                            `json:\"host,omitempty\"`\n\tPort               int                               `json:\"port,omitempty\"`\n\tLanguage           string                            `json:\"language,omitempty\"`\n\tHealthStatus       string                            `json:\"health_status,omitempty\"`\n\tApplicationSummary ApplicationInstanceSummary        `json:\"application_summary,omitempty\"`\n\tEndUserSummary     ApplicationInstanceEndUserSummary `json:\"end_user_summary,omitempty\"`\n\tLinks              ApplicationInstanceLinks          `json:\"links,omitempty\"`\n}\n\n// ApplicationInstancesFilter provides a means to filter requests through\n// ApplicationInstancesOptions when calling GetApplicationInstances.\ntype ApplicationInstancesFilter struct {\n\tHostname string\n\tIDs      []int\n}\n\n// ApplicationInstancesOptions provides a means to filter results when calling\n// GetApplicationInstances.\ntype ApplicationInstancesOptions struct {\n\tFilter ApplicationInstancesFilter\n\tPage   int\n}\n\n// GetApplicationInstances returns a slice of New Relic Application Instances,\n// optionall filtering by ApplicationInstancesOptions.\nfunc (c *Client) GetApplicationInstances(appID int, options *ApplicationInstancesOptions) ([]ApplicationInstance, error) {\n\tresp := &struct {\n\t\tApplicationInstances []ApplicationInstance `json:\"application_instances,omitempty\"`\n\t}{}\n\tpath := \"applications/\" + strconv.Itoa(appID) + \"/instances.json\"\n\terr := c.doGet(path, options, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.ApplicationInstances, nil\n}\n\n// GetApplicationInstance returns a single Application Instance associated\n// with the given application ID and instance ID\nfunc (c *Client) GetApplicationInstance(appID, instanceID int) (*ApplicationInstance, error) {\n\tresp := &struct {\n\t\tApplicationInstance ApplicationInstance `json:\"application_instance,omitempty\"`\n\t}{}\n\tpath := \"applications/\" + strconv.Itoa(appID) + \"/instances/\" + strconv.Itoa(instanceID) + \".json\"\n\terr := c.doGet(path, nil, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp.ApplicationInstance, nil\n}\nfunc (o *ApplicationInstancesOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"filter[hostname]\": o.Filter.Hostname,\n\t\t\"filter[ids]\":      o.Filter.IDs,\n\t\t\"page\":             o.Page,\n\t})\n}\n"
  },
  {
    "path": "modules/newrelic/client/application_metrics.go",
    "content": "package newrelic\n\nimport (\n\t\"fmt\"\n)\n\n// GetApplicationMetrics will return a slice of Metric items for a\n// particular Application ID, optionally filtering by\n// MetricsOptions.\nfunc (c *Client) GetApplicationMetrics(id int, options *MetricsOptions) ([]Metric, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetrics(\n\t\tfmt.Sprintf(\n\t\t\t\"applications/%d/metrics.json\",\n\t\t\tid,\n\t\t),\n\t\toptions,\n\t)\n}\n\n// GetApplicationMetricData will return all metric data for a particular\n// application and slice of metric names, optionally filtered by\n// MetricDataOptions.\nfunc (c *Client) GetApplicationMetricData(id int, names []string, options *MetricDataOptions) (*MetricDataResponse, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetricData(\n\t\tfmt.Sprintf(\n\t\t\t\"applications/%d/metrics/data.json\",\n\t\t\tid,\n\t\t),\n\t\tnames,\n\t\toptions,\n\t)\n}\n"
  },
  {
    "path": "modules/newrelic/client/applications.go",
    "content": "package newrelic\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\n// ApplicationSummary describes the brief summary component of an Application.\ntype ApplicationSummary struct {\n\tResponseTime            float64 `json:\"response_time,omitempty\"`\n\tThroughput              float64 `json:\"throughput,omitempty\"`\n\tErrorRate               float64 `json:\"error_rate,omitempty\"`\n\tApdexTarget             float64 `json:\"apdex_target,omitempty\"`\n\tApdexScore              float64 `json:\"apdex_score,omitempty\"`\n\tHostCount               int     `json:\"host_count,omitempty\"`\n\tInstanceCount           int     `json:\"instance_count,omitempty\"`\n\tConcurrentInstanceCount int     `json:\"concurrent_instance_count,omitempty\"`\n}\n\n// EndUserSummary describes the end user summary component of an Application.\ntype EndUserSummary struct {\n\tResponseTime float64 `json:\"response_time,omitempty\"`\n\tThroughput   float64 `json:\"throughput,omitempty\"`\n\tApdexTarget  float64 `json:\"apdex_target,omitempty\"`\n\tApdexScore   float64 `json:\"apdex_score,omitempty\"`\n}\n\n// Settings describe settings for an Application.\ntype Settings struct {\n\tAppApdexThreshold        float64 `json:\"app_apdex_threshold,omitempty\"`\n\tEndUserApdexThreshold    float64 `json:\"end_user_apdex_threshold,omitempty\"`\n\tEnableRealUserMonitoring bool    `json:\"enable_real_user_monitoring,omitempty\"`\n\tUseServerSideConfig      bool    `json:\"use_server_side_config,omitempty\"`\n}\n\n// Links list IDs associated with an Application.\ntype Links struct {\n\tServers              []int `json:\"servers,omitempty\"`\n\tApplicationHosts     []int `json:\"application_hosts,omitempty\"`\n\tApplicationInstances []int `json:\"application_instances,omitempty\"`\n\tAlertPolicy          int   `json:\"alert_policy,omitempty\"`\n}\n\n// Application describes a New Relic Application.\ntype Application struct {\n\tID                 int                `json:\"id,omitempty\"`\n\tName               string             `json:\"name,omitempty\"`\n\tLanguage           string             `json:\"language,omitempty\"`\n\tHealthStatus       string             `json:\"health_status,omitempty\"`\n\tReporting          bool               `json:\"reporting,omitempty\"`\n\tLastReportedAt     time.Time          `json:\"last_reported_at,omitempty\"`\n\tApplicationSummary ApplicationSummary `json:\"application_summary,omitempty\"`\n\tEndUserSummary     EndUserSummary     `json:\"end_user_summary,omitempty\"`\n\tSettings           Settings           `json:\"settings,omitempty\"`\n\tLinks              Links              `json:\"links,omitempty\"`\n}\n\n// ApplicationFilter provides a means to filter requests through\n// ApplicaitonOptions when calling GetApplications.\ntype ApplicationFilter struct {\n\tName     string\n\tHost     string\n\tIDs      []int\n\tLanguage string\n}\n\n// ApplicationOptions provides a means to filter results when calling\n// GetApplicaitons.\ntype ApplicationOptions struct {\n\tFilter ApplicationFilter\n\tPage   int\n}\n\nfunc (o *ApplicationOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"filter[name]\":     o.Filter.Name,\n\t\t\"filter[host]\":     o.Filter.Host,\n\t\t\"filter[ids]\":      o.Filter.IDs,\n\t\t\"filter[language]\": o.Filter.Language,\n\t\t\"page\":             o.Page,\n\t})\n}\n\n// GetApplications returns a slice of New Relic Applications, optionally\n// filtering by ApplicationOptions.\nfunc (c *Client) GetApplications(options *ApplicationOptions) ([]Application, error) {\n\tresp := &struct {\n\t\tApplications []Application `json:\"applications,omitempty\"`\n\t}{}\n\terr := c.doGet(\"applications.json\", options, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Applications, nil\n}\n\n// GetApplication returns a single Application associated with a given ID.\nfunc (c *Client) GetApplication(id int) (*Application, error) {\n\tresp := &struct {\n\t\tApplication Application `json:\"application,omitempty\"`\n\t}{}\n\tpath := \"applications/\" + strconv.Itoa(id) + \".json\"\n\terr := c.doGet(path, nil, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp.Application, nil\n}\n"
  },
  {
    "path": "modules/newrelic/client/array.go",
    "content": "package newrelic\n\n// An Array is a type expected by the NewRelic API that differs from a comma-\n// separated list. When passing GET params that expect an 'Array' type with\n// one to many values, the expected format is \"key=val1&key=val2\" but an\n// argument with zero to many values is of the form \"key=val1,val2\", and\n// neither can be used in the other's place, so we have to differentiate\n// somehow.\ntype Array struct {\n\tarr []string\n}\n"
  },
  {
    "path": "modules/newrelic/client/browser_applications.go",
    "content": "package newrelic\n\n// BrowserApplicationsFilter is the filtering component of\n// BrowserApplicationsOptions\ntype BrowserApplicationsFilter struct {\n\tName string\n\tIDs  []int\n}\n\n// BrowserApplicationsOptions provides a filtering mechanism for\n// GetBrowserApplications.\ntype BrowserApplicationsOptions struct {\n\tFilter BrowserApplicationsFilter\n\tPage   int\n}\n\n// BrowserApplication describes a New Relic Browser Application.\ntype BrowserApplication struct {\n\tID                   int    `json:\"id,omitempty\"`\n\tName                 string `json:\"name,omitempty\"`\n\tBrowserMonitoringKey string `json:\"browser_monitoring_key,omitempty\"`\n\tLoaderScript         string `json:\"loader_script,omitempty\"`\n}\n\n// GetBrowserApplications will return a slice of New Relic Browser\n// Applications, optionally filtered by BrowserApplicationsOptions.\nfunc (c *Client) GetBrowserApplications(opt *BrowserApplicationsOptions) ([]BrowserApplication, error) {\n\tresp := &struct {\n\t\tBrowserApplications []BrowserApplication `json:\"browser_applications,omitempty\"`\n\t}{}\n\tpath := \"browser_applications.json\"\n\terr := c.doGet(path, opt, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.BrowserApplications, nil\n}\n\nfunc (o *BrowserApplicationsOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"filter[name]\": o.Filter.Name,\n\t\t\"filter[ids]\":  o.Filter.IDs,\n\t\t\"page\":         o.Page,\n\t})\n}\n"
  },
  {
    "path": "modules/newrelic/client/component_metrics.go",
    "content": "package newrelic\n\nimport (\n\t\"fmt\"\n)\n\n// GetComponentMetrics will return a slice of Metric items for a\n// particular Component ID, optionally filtered by MetricsOptions.\nfunc (c *Client) GetComponentMetrics(id int, options *MetricsOptions) ([]Metric, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetrics(\n\t\tfmt.Sprintf(\n\t\t\t\"components/%d/metrics.json\",\n\t\t\tid,\n\t\t),\n\t\toptions,\n\t)\n}\n\n// GetComponentMetricData will return all metric data for a particular\n// component, optionally filtered by MetricDataOptions.\nfunc (c *Client) GetComponentMetricData(id int, names []string, options *MetricDataOptions) (*MetricDataResponse, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetricData(\n\t\tfmt.Sprintf(\n\t\t\t\"components/%d/metrics/data.json\",\n\t\t\tid,\n\t\t),\n\t\tnames,\n\t\toptions,\n\t)\n}\n"
  },
  {
    "path": "modules/newrelic/client/http_helper.go",
    "content": "package newrelic\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc (c *Client) doGet(path string, params fmt.Stringer, out interface{}) error {\n\tvar s string\n\tif params != nil {\n\t\ts = params.String()\n\t}\n\tr := strings.NewReader(s)\n\treq, err := http.NewRequest(\"GET\", c.url.String()+path, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Add(\"X-Api-Key\", c.apiKey)\n\treq.Header.Add(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treturn c.doRequest(req, out)\n}\n\nfunc (c *Client) doRequest(req *http.Request, out interface{}) error {\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"newrelic http error (%s): %s\", resp.Status, b)\n\t}\n\tif len(b) == 0 {\n\t\tb = []byte{'{', '}'}\n\t}\n\terr = json.Unmarshal(b, &out)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc encodeGetParams(params map[string]interface{}) string {\n\ts := url.Values{}\n\tfor k, v := range params {\n\t\tswitch val := v.(type) {\n\t\tcase string:\n\t\t\tif val != \"\" {\n\t\t\t\ts.Add(k, val)\n\t\t\t}\n\t\tcase int:\n\t\t\tif val != 0 {\n\t\t\t\ts.Add(k, strconv.Itoa(val))\n\t\t\t}\n\t\tcase []string:\n\t\t\tif len(val) != 0 {\n\t\t\t\ts.Add(k, strings.Join(val, \",\"))\n\t\t\t}\n\t\tcase []int:\n\t\t\tarr := []string{}\n\t\t\tfor _, v := range val {\n\t\t\t\tarr = append(arr, strconv.Itoa(v))\n\t\t\t}\n\t\t\tif len(arr) != 0 {\n\t\t\t\ts.Add(k, strings.Join(arr, \",\"))\n\t\t\t}\n\t\tcase time.Time:\n\t\t\tif !val.IsZero() {\n\t\t\t\ts.Add(k, val.String())\n\t\t\t}\n\t\tcase Array:\n\t\t\tfor _, v := range val.arr {\n\t\t\t\ts.Add(k, v)\n\t\t\t}\n\t\tcase bool:\n\t\t\ts.Add(k, \"true\")\n\t\tdefault:\n\t\t\ts.Add(k, fmt.Sprintf(\"%v\", v))\n\t\t}\n\t}\n\treturn s.Encode()\n}\n"
  },
  {
    "path": "modules/newrelic/client/key_transactions.go",
    "content": "package newrelic\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\n// KeyTransactionsFilter is the filtering component of KeyTransactionsOptions.\ntype KeyTransactionsFilter struct {\n\tName string\n\tIDs  []int\n}\n\n// KeyTransactionsOptions provides a filtering mechanism for GetKeyTransactions.\ntype KeyTransactionsOptions struct {\n\tFilter KeyTransactionsFilter\n\tPage   int\n}\n\n// KeyTransactionLinks link KeyTransactions to the objects to which they\n// pertain.\ntype KeyTransactionLinks struct {\n\tApplication int `json:\"application,omitempty\"`\n}\n\n// KeyTransaction represents a New Relic Key Transaction.\ntype KeyTransaction struct {\n\tID                 int                 `json:\"id,omitempty\"`\n\tName               string              `json:\"name,omitempty\"`\n\tTransactionName    string              `json:\"transaction_name,omitempty\"`\n\tHealthStatus       string              `json:\"health_status,omitempty\"`\n\tReporting          bool                `json:\"reporting,omitempty\"`\n\tLastReportedAt     time.Time           `json:\"last_reported_at,omitempty\"`\n\tApplicationSummary ApplicationSummary  `json:\"application_summary,omitempty\"`\n\tEndUserSummary     EndUserSummary      `json:\"end_user_summary,omitempty\"`\n\tLinks              KeyTransactionLinks `json:\"links,omitempty\"`\n}\n\n// GetKeyTransactions will return a slice of New Relic Key Transactions,\n// optionally filtered by KeyTransactionsOptions.\nfunc (c *Client) GetKeyTransactions(opt *KeyTransactionsOptions) ([]KeyTransaction, error) {\n\tresp := &struct {\n\t\tKeyTransactions []KeyTransaction `json:\"key_transactions,omitempty\"`\n\t}{}\n\tpath := \"key_transactions.json\"\n\terr := c.doGet(path, opt, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.KeyTransactions, nil\n}\n\n// GetKeyTransaction will return a single New Relic Key Transaction for the\n// given id.\nfunc (c *Client) GetKeyTransaction(id int) (*KeyTransaction, error) {\n\tresp := &struct {\n\t\tKeyTransaction *KeyTransaction `json:\"key_transaction,omitempty\"`\n\t}{}\n\tpath := \"key_transactions/\" + strconv.Itoa(id) + \".json\"\n\terr := c.doGet(path, nil, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.KeyTransaction, nil\n}\n\nfunc (o *KeyTransactionsOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"filter[name]\": o.Filter.Name,\n\t\t\"filter[ids]\":  o.Filter.IDs,\n\t\t\"page\":         o.Page,\n\t})\n}\n"
  },
  {
    "path": "modules/newrelic/client/legacy_alert_policies.go",
    "content": "package newrelic\n\nimport (\n\t\"strconv\"\n)\n\n// LegacyAlertPolicyLinks describes object links for Alert Policies.\ntype LegacyAlertPolicyLinks struct {\n\tNotificationChannels []int `json:\"notification_channels,omitempty\"`\n\tServers              []int `json:\"servers,omitempty\"`\n}\n\n// LegacyAlertPolicyCondition describes conditions that trigger an LegacyAlertPolicy.\ntype LegacyAlertPolicyCondition struct {\n\tID             int     `json:\"id,omitempty\"`\n\tEnabled        bool    `json:\"enabled,omitempty\"`\n\tSeverity       string  `json:\"severity,omitempty\"`\n\tThreshold      float64 `json:\"threshold,omitempty\"`\n\tTriggerMinutes int     `json:\"trigger_minutes,omitempty\"`\n\tType           string  `json:\"type,omitempty\"`\n}\n\n// LegacyAlertPolicy describes a New Relic alert policy.\ntype LegacyAlertPolicy struct {\n\tConditions         []LegacyAlertPolicyCondition `json:\"conditions,omitempty\"`\n\tEnabled            bool                         `json:\"enabled,omitempty\"`\n\tID                 int                          `json:\"id,omitempty\"`\n\tLinks              LegacyAlertPolicyLinks       `json:\"links,omitempty\"`\n\tIncidentPreference string                       `json:\"incident_preference,omitempty\"`\n\tName               string                       `json:\"name,omitempty\"`\n}\n\n// LegacyAlertPolicyFilter provides filters for LegacyAlertPolicyOptions.\ntype LegacyAlertPolicyFilter struct {\n\tName string\n}\n\n// LegacyAlertPolicyOptions is an optional means of filtering when calling\n// GetLegacyAlertPolicies.\ntype LegacyAlertPolicyOptions struct {\n\tFilter LegacyAlertPolicyFilter\n\tPage   int\n}\n\nfunc (o *LegacyAlertPolicyOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"filter[name]\": o.Filter.Name,\n\t\t\"page\":         o.Page,\n\t})\n}\n\n// GetLegacyAlertPolicy will return the LegacyAlertPolicy with  particular ID.\nfunc (c *Client) GetLegacyAlertPolicy(id int) (*LegacyAlertPolicy, error) {\n\tresp := &struct {\n\t\tLegacyAlertPolicy *LegacyAlertPolicy `json:\"alert_policy,omitempty\"`\n\t}{}\n\terr := c.doGet(\"alert_policies/\"+strconv.Itoa(id)+\".json\", nil, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.LegacyAlertPolicy, nil\n}\n\n// GetLegacyAlertPolicies will return a slice of LegacyAlertPolicy items,\n// optionally filtering by LegacyAlertPolicyOptions.\nfunc (c *Client) GetLegacyAlertPolicies(options *LegacyAlertPolicyOptions) ([]LegacyAlertPolicy, error) {\n\tresp := &struct {\n\t\tLegacyAlertPolicies []LegacyAlertPolicy `json:\"alert_policies,omitempty\"`\n\t}{}\n\terr := c.doGet(\"alert_policies.json\", options, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.LegacyAlertPolicies, nil\n}\n"
  },
  {
    "path": "modules/newrelic/client/main.go",
    "content": "/*\n * NewRelic API for Go\n *\n * Please see the included LICENSE file for licensing information.\n *\n * Copyright 2016 by authors and contributors.\n */\n\npackage newrelic\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n)\n\nconst (\n\t// defaultAPIURL is the default base URL for New Relic's latest API.\n\tdefaultAPIURL = \"https://api.newrelic.com/v2/\"\n\t// defaultTimeout is the default timeout for the http.Client used.\n\tdefaultTimeout = 5 * time.Second\n)\n\n// Client provides a set of methods to interact with the New Relic API.\ntype Client struct {\n\tapiKey     string\n\thttpClient *http.Client\n\turl        *url.URL\n}\n\n// NewWithHTTPClient returns a new Client object for interfacing with the New\n// Relic API, allowing for override of the http.Client object.\nfunc NewWithHTTPClient(apiKey string, client *http.Client) *Client {\n\tu, err := url.Parse(defaultAPIURL)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\thttpClient: client,\n\t\turl:        u,\n\t}\n}\n\n// NewClient returns a new Client object for interfacing with the New Relic API.\nfunc NewClient(apiKey string) *Client {\n\treturn NewWithHTTPClient(apiKey, &http.Client{Timeout: defaultTimeout})\n}\n"
  },
  {
    "path": "modules/newrelic/client/metrics.go",
    "content": "package newrelic\n\nimport (\n\t\"time\"\n)\n\n// Metric describes a New Relic metric.\ntype Metric struct {\n\tName   string   `json:\"name,omitempty\"`\n\tValues []string `json:\"values,omitempty\"`\n}\n\n// MetricsOptions options allow filtering when getting lists of metric names\n// associated with an entity.\ntype MetricsOptions struct {\n\tName string\n\tPage int\n}\n\n// MetricTimeslice describes the period to which a Metric pertains.\ntype MetricTimeslice struct {\n\tFrom   time.Time          `json:\"from,omitempty\"`\n\tTo     time.Time          `json:\"to,omitempty\"`\n\tValues map[string]float64 `json:\"values,omitempty\"`\n}\n\n// MetricData describes the data for a particular metric.\ntype MetricData struct {\n\tName       string            `json:\"name,omitempty\"`\n\tTimeslices []MetricTimeslice `json:\"timeslices,omitempty\"`\n}\n\n// MetricDataOptions allow filtering when getting data about a particular set\n// of New Relic metrics.\ntype MetricDataOptions struct {\n\tNames     Array\n\tValues    Array\n\tFrom      time.Time\n\tTo        time.Time\n\tPeriod    int\n\tSummarize bool\n\tRaw       bool\n}\n\n// MetricDataResponse is the response received from New Relic for any request\n// for metric data.\ntype MetricDataResponse struct {\n\tFrom            time.Time    `json:\"from,omitempty\"`\n\tTo              time.Time    `json:\"to,omitempty\"`\n\tMetricsNotFound []string     `json:\"metrics_not_found,omitempty\"`\n\tMetricsFound    []string     `json:\"metrics_found,omitempty\"`\n\tMetrics         []MetricData `json:\"metrics,omitempty\"`\n}\n\nfunc (o *MetricsOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"name\": o.Name,\n\t\t\"page\": o.Page,\n\t})\n}\n\nfunc (o *MetricDataOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"names[]\":   o.Names,\n\t\t\"values[]\":  o.Values,\n\t\t\"from\":      o.From,\n\t\t\"to\":        o.To,\n\t\t\"period\":    o.Period,\n\t\t\"summarize\": o.Summarize,\n\t\t\"raw\":       o.Raw,\n\t})\n}\n\n// MetricClient implements a generic New Relic metrics client.\n// This is used as a general client for fetching metric names and data.\ntype MetricClient struct {\n\tnewRelicClient *Client\n}\n\n// NewMetricClient creates and returns a new MetricClient.\nfunc NewMetricClient(newRelicClient *Client) *MetricClient {\n\treturn &MetricClient{\n\t\tnewRelicClient: newRelicClient,\n\t}\n}\n\n// GetMetrics is a generic function for fetching a list of available metrics\n// from different parts of New Relic.\n// Example: Application metrics, Component metrics, etc.\nfunc (mc *MetricClient) GetMetrics(path string, options *MetricsOptions) ([]Metric, error) {\n\tresp := &struct {\n\t\tMetrics []Metric `json:\"metrics,omitempty\"`\n\t}{}\n\n\terr := mc.newRelicClient.doGet(path, options, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn resp.Metrics, nil\n}\n\n// GetMetricData is a generic function for fetching data for a specific metric.\n// from different parts of New Relic.\n// Example: Application metric data, Component metric data, etc.\nfunc (mc *MetricClient) GetMetricData(path string, names []string, options *MetricDataOptions) (*MetricDataResponse, error) {\n\tresp := &struct {\n\t\tMetricData MetricDataResponse `json:\"metric_data,omitempty\"`\n\t}{}\n\n\tif options == nil {\n\t\toptions = &MetricDataOptions{}\n\t}\n\n\toptions.Names = Array{names}\n\terr := mc.newRelicClient.doGet(path, options, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.MetricData, nil\n}\n"
  },
  {
    "path": "modules/newrelic/client/mobile_application_metrics.go",
    "content": "package newrelic\n\nimport (\n\t\"fmt\"\n)\n\n// GetMobileApplicationMetrics will return a slice of Metric items for a\n// particular MobileAplication ID, optionally filtering by\n// MetricsOptions.\nfunc (c *Client) GetMobileApplicationMetrics(id int, options *MetricsOptions) ([]Metric, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetrics(\n\t\tfmt.Sprintf(\n\t\t\t\"mobile_applications/%d/metrics.json\",\n\t\t\tid,\n\t\t),\n\t\toptions,\n\t)\n}\n\n// GetMobileApplicationMetricData will return all metric data for a particular\n// MobileAplication and slice of metric names, optionally filtered by\n// MetricDataOptions.\nfunc (c *Client) GetMobileApplicationMetricData(id int, names []string, options *MetricDataOptions) (*MetricDataResponse, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetricData(\n\t\tfmt.Sprintf(\n\t\t\t\"mobile_applications/%d/metrics/data.json\",\n\t\t\tid,\n\t\t),\n\t\tnames,\n\t\toptions,\n\t)\n}\n"
  },
  {
    "path": "modules/newrelic/client/mobile_applications.go",
    "content": "package newrelic\n\nimport (\n\t\"strconv\"\n)\n\n// MobileApplicationSummary describes an Application's host.\ntype MobileApplicationSummary struct {\n\tActiveUsers     int     `json:\"active_users,omitempty\"`\n\tLaunchCount     int     `json:\"launch_count,omitempty\"`\n\tThroughput      float64 `json:\"throughput,omitempty\"`\n\tResponseTime    float64 `json:\"response_time,omitempty\"`\n\tCallsPerSession float64 `json:\"calls_per_session,omitempty\"`\n\tInteractionTime float64 `json:\"interaction_time,omitempty\"`\n\tFailedCallRate  float64 `json:\"failed_call_rate,omitempty\"`\n\tRemoteErrorRate float64 `json:\"remote_error_rate\"`\n}\n\n// MobileApplicationCrashSummary describes a MobileApplication's crash data.\ntype MobileApplicationCrashSummary struct {\n\tSupportsCrashData    bool    `json:\"supports_crash_data,omitempty\"`\n\tUnresolvedCrashCount int     `json:\"unresolved_crash_count,omitempty\"`\n\tCrashCount           int     `json:\"crash_count,omitempty\"`\n\tCrashRate            float64 `json:\"crash_rate,omitempty\"`\n}\n\n// MobileApplication describes a New Relic Application Host.\ntype MobileApplication struct {\n\tID            int                           `json:\"id,omitempty\"`\n\tName          string                        `json:\"name,omitempty\"`\n\tHealthStatus  string                        `json:\"health_status,omitempty\"`\n\tReporting     bool                          `json:\"reporting,omitempty\"`\n\tMobileSummary MobileApplicationSummary      `json:\"mobile_summary,omitempty\"`\n\tCrashSummary  MobileApplicationCrashSummary `json:\"crash_summary,omitempty\"`\n}\n\n// GetMobileApplications returns a slice of New Relic Mobile Applications.\nfunc (c *Client) GetMobileApplications() ([]MobileApplication, error) {\n\tresp := &struct {\n\t\tApplications []MobileApplication `json:\"applications,omitempty\"`\n\t}{}\n\tpath := \"mobile_applications.json\"\n\terr := c.doGet(path, nil, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Applications, nil\n}\n\n// GetMobileApplication returns a single Mobile Application with the id.\nfunc (c *Client) GetMobileApplication(id int) (*MobileApplication, error) {\n\tresp := &struct {\n\t\tApplication MobileApplication `json:\"application,omitempty\"`\n\t}{}\n\tpath := \"mobile_applications/\" + strconv.Itoa(id) + \".json\"\n\terr := c.doGet(path, nil, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp.Application, nil\n}\n"
  },
  {
    "path": "modules/newrelic/client/notification_channels.go",
    "content": "package newrelic\n\nimport (\n\t\"strconv\"\n)\n\n// NotificationChannelLinks describes object links for notification channels.\ntype NotificationChannelLinks struct {\n\tNotificationChannels []int `json:\"notification_channels,omitempty\"`\n\tUser                 int   `json:\"user,omitempty\"`\n}\n\n// NotificationChannel describes a New Relic notification channel.\ntype NotificationChannel struct {\n\tID           int                      `json:\"id,omitempty\"`\n\tType         string                   `json:\"type,omitempty\"`\n\tDowntimeOnly bool                     `json:\"downtime_only,omitempty\"`\n\tURL          string                   `json:\"url,omitempty\"`\n\tName         string                   `json:\"name,omitempty\"`\n\tDescription  string                   `json:\"description,omitempty\"`\n\tEmail        string                   `json:\"email,omitempty\"`\n\tSubdomain    string                   `json:\"subdomain,omitempty\"`\n\tService      string                   `json:\"service,omitempty\"`\n\tMobileAlerts bool                     `json:\"mobile_alerts,omitempty\"`\n\tEmailAlerts  bool                     `json:\"email_alerts,omitempty\"`\n\tRoom         string                   `json:\"room,omitempty\"`\n\tLinks        NotificationChannelLinks `json:\"links,omitempty\"`\n}\n\n// NotificationChannelsFilter provides filters for\n// NotificationChannelsOptions.\ntype NotificationChannelsFilter struct {\n\tType []string\n\tIDs  []int\n}\n\n// NotificationChannelsOptions is an optional means of filtering when calling\n// GetNotificationChannels.\ntype NotificationChannelsOptions struct {\n\tFilter NotificationChannelsFilter\n\tPage   int\n}\n\nfunc (o *NotificationChannelsOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"filter[type]\": o.Filter.Type,\n\t\t\"filter[ids]\":  o.Filter.IDs,\n\t\t\"page\":         o.Page,\n\t})\n}\n\n// GetNotificationChannel will return the NotificationChannel with  particular ID.\nfunc (c *Client) GetNotificationChannel(id int) (*NotificationChannel, error) {\n\tresp := &struct {\n\t\tNotificationChannel *NotificationChannel `json:\"notification_channel,omitempty\"`\n\t}{}\n\terr := c.doGet(\"notification_channels/\"+strconv.Itoa(id)+\".json\", nil, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.NotificationChannel, nil\n}\n\n// GetNotificationChannels will return a slice of NotificationChannel items,\n// optionally filtering by NotificationChannelsOptions.\nfunc (c *Client) GetNotificationChannels(options *NotificationChannelsOptions) ([]NotificationChannel, error) {\n\tresp := &struct {\n\t\tNotificationChannels []NotificationChannel `json:\"notification_channels,omitempty\"`\n\t}{}\n\terr := c.doGet(\"notification_channels.json\", options, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.NotificationChannels, nil\n}\n"
  },
  {
    "path": "modules/newrelic/client/server_metrics.go",
    "content": "package newrelic\n\nimport (\n\t\"fmt\"\n)\n\n// GetServerMetrics will return a slice of Metric items for a particular\n// Server ID, optionally filtering by MetricsOptions.\nfunc (c *Client) GetServerMetrics(id int, options *MetricsOptions) ([]Metric, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetrics(\n\t\tfmt.Sprintf(\n\t\t\t\"servers/%d/metrics.json\",\n\t\t\tid,\n\t\t),\n\t\toptions,\n\t)\n}\n\n// GetServerMetricData will return all metric data for a particular Server and\n// slice of metric names, optionally filtered by MetricDataOptions.\nfunc (c *Client) GetServerMetricData(id int, names []string, options *MetricDataOptions) (*MetricDataResponse, error) {\n\tmc := NewMetricClient(c)\n\n\treturn mc.GetMetricData(\n\t\tfmt.Sprintf(\n\t\t\t\"servers/%d/metrics/data.json\",\n\t\t\tid,\n\t\t),\n\t\tnames,\n\t\toptions,\n\t)\n}\n"
  },
  {
    "path": "modules/newrelic/client/servers.go",
    "content": "package newrelic\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\n// ServersFilter is the filtering component of ServersOptions.\ntype ServersFilter struct {\n\tName     string\n\tHost     string\n\tIDs      []int\n\tLabels   []string\n\tReported bool\n}\n\n// ServersOptions provides a filtering mechanism for GetServers.\ntype ServersOptions struct {\n\tFilter ServersFilter\n\tPage   int\n}\n\n// ServerSummary describes the summary component of a Server.\ntype ServerSummary struct {\n\tCPU             float64 `json:\"cpu,omitempty\"`\n\tCPUStolen       float64 `json:\"cpu_stolen,omitempty\"`\n\tDiskIO          float64 `json:\"disk_io,omitempty\"`\n\tMemory          float64 `json:\"memory,omitempty\"`\n\tMemoryUsed      int64   `json:\"memory_used,omitempty\"`\n\tMemoryTotal     int64   `json:\"memory_total,omitempty\"`\n\tFullestDisk     float64 `json:\"fullest_disk,omitempty\"`\n\tFullestDiskFree int64   `json:\"fullest_disk_free,omitempty\"`\n}\n\n// ServerLinks link Servers to the objects to which they pertain.\ntype ServerLinks struct {\n\tAlertPolicy int `json:\"alert_policy,omitempty\"`\n}\n\n// Server represents a New Relic Server.\ntype Server struct {\n\tID             int           `json:\"id,omitempty\"`\n\tAccountID      int           `json:\"account_id,omitempty\"`\n\tName           string        `json:\"name,omitempty\"`\n\tHost           string        `json:\"host,omitempty\"`\n\tHealthStatus   string        `json:\"health_status,omitempty\"`\n\tReporting      bool          `json:\"reporting,omitempty\"`\n\tLastReportedAt time.Time     `json:\"last_reported_at,omitempty\"`\n\tSummary        ServerSummary `json:\"summary,omitempty\"`\n\tLinks          ServerLinks   `json:\"links,omitempty\"`\n}\n\n// GetServers will return a slice of New Relic Servers, optionally filtered by\n// ServerOptions.\nfunc (c *Client) GetServers(opt *ServersOptions) ([]Server, error) {\n\tresp := &struct {\n\t\tServers []Server `json:\"servers,omitempty\"`\n\t}{}\n\tpath := \"servers.json\"\n\terr := c.doGet(path, opt, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Servers, nil\n}\n\n// GetServer will return a single New Relic Server for the given id.\nfunc (c *Client) GetServer(id int) (*Server, error) {\n\tresp := &struct {\n\t\tServer *Server `json:\"server,omitempty\"`\n\t}{}\n\tpath := \"servers/\" + strconv.Itoa(id) + \".json\"\n\terr := c.doGet(path, nil, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Server, nil\n}\n\nfunc (o *ServersOptions) String() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"filter[name]\":     o.Filter.Name,\n\t\t\"filter[host]\":     o.Filter.Host,\n\t\t\"filter[ids]\":      o.Filter.IDs,\n\t\t\"filter[labels]\":   o.Filter.Labels,\n\t\t\"filter[reported]\": o.Filter.Reported,\n\t\t\"page\":             o.Page,\n\t})\n}\n"
  },
  {
    "path": "modules/newrelic/client/usages.go",
    "content": "package newrelic\n\nimport (\n\t\"time\"\n)\n\n// Usage describes usage over a single time period.\ntype Usage struct {\n\tFrom  time.Time `json:\"from,omitempty\"`\n\tTo    time.Time `json:\"to,omitempty\"`\n\tUsage int       `json:\"usage,omitempty\"`\n}\n\n// UsageData represents usage data for a product over a time frame, including\n// a slice of Usages.\ntype UsageData struct {\n\tProduct string    `json:\"product,omitempty\"`\n\tFrom    time.Time `json:\"from,omitempty\"`\n\tTo      time.Time `json:\"to,omitempty\"`\n\tUnit    string    `json:\"unit,omitempty\"`\n\tUsages  []Usage   `json:\"usages,omitempty\"`\n}\n\ntype usageParams struct {\n\tStart             time.Time\n\tEnd               time.Time\n\tIncludeSubaccount bool\n}\n\nfunc (o *usageParams) String() string {\n\treturn encodeGetParams(map[string]interface{}{\n\t\t\"start_date\":          o.Start.Format(\"2006-01-02\"),\n\t\t\"end_date\":            o.End.Format(\"2006-01-02\"),\n\t\t\"include_subaccounts\": o.IncludeSubaccount,\n\t})\n}\n\n// GetUsages will return usage for a product in a given time frame.\nfunc (c *Client) GetUsages(product string, start, end time.Time, includeSubaccounts bool) (*UsageData, error) {\n\tresp := &struct {\n\t\tUsageData *UsageData `json:\"usage_data,omitempty\"`\n\t}{}\n\toptions := &usageParams{start, end, includeSubaccounts}\n\terr := c.doGet(\"usages/\"+product+\".json\", options, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.UsageData, nil\n}\n"
  },
  {
    "path": "modules/newrelic/client.go",
    "content": "package newrelic\n\nimport (\n\tnr \"github.com/wtfutil/wtf/modules/newrelic/client\"\n)\n\ntype Client2 struct {\n\tapplicationId int\n\tnrClient      *nr.Client\n}\n\nfunc NewClient(apiKey string, applicationId int) *Client2 {\n\treturn &Client2{\n\t\tapplicationId: applicationId,\n\t\tnrClient:      nr.NewClient(apiKey),\n\t}\n\n}\n\nfunc (client *Client2) Application() (*nr.Application, error) {\n\n\tapplication, err := client.nrClient.GetApplication(client.applicationId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn application, nil\n}\n\nfunc (client *Client2) Deployments() ([]nr.ApplicationDeployment, error) {\n\n\topts := &nr.ApplicationDeploymentOptions{Page: 1}\n\tdeployments, err := client.nrClient.GetApplicationDeployments(client.applicationId, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn deployments, nil\n}\n"
  },
  {
    "path": "modules/newrelic/display.go",
    "content": "package newrelic\n\nimport (\n\t\"fmt\"\n\n\tnr \"github.com/wtfutil/wtf/modules/newrelic/client\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tclient := widget.currentData()\n\tif client == nil {\n\t\treturn widget.CommonSettings().Title, \" NewRelic data unavailable \", false\n\t}\n\tapp, appErr := client.Application()\n\tdeploys, depErr := client.Deployments()\n\n\tappName := \"error\"\n\tif appErr == nil {\n\t\tappName = app.Name\n\t}\n\n\tvar content string\n\ttitle := fmt.Sprintf(\"%s - [green]%s[white]\", widget.CommonSettings().Title, appName)\n\twrap := false\n\tif depErr != nil {\n\t\twrap = true\n\t\tcontent = depErr.Error()\n\t} else {\n\t\tcontent = widget.contentFrom(deploys)\n\t}\n\n\treturn title, content, wrap\n}\n\nfunc (widget *Widget) contentFrom(deploys []nr.ApplicationDeployment) string {\n\tstr := fmt.Sprintf(\n\t\t\" %s\\n\",\n\t\tfmt.Sprintf(\n\t\t\t\"[%s]Latest Deploys[white]\",\n\t\t\twidget.settings.Colors.Subheading,\n\t\t),\n\t)\n\n\trevisions := []string{}\n\n\tfor _, deploy := range deploys {\n\t\tif (deploy.Revision != \"\") && utils.DoesNotInclude(revisions, deploy.Revision) {\n\t\t\tlineColor := \"white\"\n\t\t\tif wtf.IsToday(deploy.Timestamp) {\n\t\t\t\tlineColor = \"lightblue\"\n\t\t\t}\n\n\t\t\trevLen := 8\n\t\t\tif revLen > len(deploy.Revision) {\n\t\t\t\trevLen = len(deploy.Revision)\n\t\t\t}\n\n\t\t\tstr += fmt.Sprintf(\n\t\t\t\t\" [green]%s[%s] %s %-.16s[white]\\n\",\n\t\t\t\tdeploy.Revision[0:revLen],\n\t\t\t\tlineColor,\n\t\t\t\tdeploy.Timestamp.Format(\"Jan 02 15:04 MST\"),\n\t\t\t\tutils.NameFromEmail(deploy.User),\n\t\t\t)\n\n\t\t\trevisions = append(revisions, deploy.Revision)\n\n\t\t\tif len(revisions) == widget.settings.deployCount {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn str\n}\n"
  },
  {
    "path": "modules/newrelic/keyboard.go",
    "content": "package newrelic\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous application\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next application\")\n}\n"
  },
  {
    "path": "modules/newrelic/settings.go",
    "content": "package newrelic\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"NewRelic\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey         string        `help:\"Your New Relic API token.\"`\n\tdeployCount    int           `help:\"The number of past deploys to display on screen.\" optional:\"true\"`\n\tapplicationIDs []interface{} `help:\"The integer ID of the New Relic application you wish to report on.\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:         ymlConfig.UString(\"apiKey\", os.Getenv(\"WTF_NEW_RELIC_API_KEY\")),\n\t\tdeployCount:    ymlConfig.UInt(\"deployCount\", 5),\n\t\tapplicationIDs: ymlConfig.UList(\"applicationIDs\"),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/newrelic/widget.go",
    "content": "package newrelic\n\nimport (\n\t\"sort\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tClients []*Client2\n\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"applicationID\", \"applicationIDs\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\n\tfor _, id := range utils.ToInts(widget.settings.applicationIDs) {\n\t\twidget.Clients = append(widget.Clients, NewClient(widget.settings.apiKey, id))\n\t}\n\n\tsort.Slice(widget.Clients, func(i, j int) bool {\n\t\treturn widget.Clients[i].applicationId < widget.Clients[j].applicationId\n\t})\n\n\twidget.SetDisplayFunction(widget.Refresh)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) currentData() *Client2 {\n\tif len(widget.Clients) == 0 {\n\t\treturn nil\n\t}\n\n\tif widget.Idx < 0 || widget.Idx >= len(widget.Clients) {\n\t\treturn nil\n\t}\n\n\treturn widget.Clients[widget.Idx]\n}\n"
  },
  {
    "path": "modules/nextbus/settings.go",
    "content": "package nextbus\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"nextbus\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\tcommon *cfg.Common\n\n\troute  string `help:\"Route Number of your bus\"`\n\tagency string `help:\"Transit agency of your bus\"`\n\tstopID string `help:\"Your bus stop number\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tcommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\troute:  ymlConfig.UString(\"route\"),\n\t\tagency: ymlConfig.UString(\"agency\"),\n\t\tstopID: ymlConfig.UString(\"stopID\"),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/nextbus/widget.go",
    "content": "package nextbus\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/logger\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget is the container for your module's data\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n}\n\n// NewWidget creates and returns an instance of Widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.common),\n\n\t\tsettings: settings,\n\t}\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\t// The last call should always be to the display function\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() string {\n\treturn getNextBus(widget.settings.agency, widget.settings.route, widget.settings.stopID)\n}\n\ntype AutoGenerated struct {\n\tCopyright   string      `json:\"copyright\"`\n\tPredictions Predictions `json:\"predictions\"`\n}\n\ntype Prediction struct {\n\tAffectedByLayover string `json:\"affectedByLayover\"`\n\tSeconds           string `json:\"seconds\"`\n\tTripTag           string `json:\"tripTag\"`\n\tMinutes           string `json:\"minutes\"`\n\tIsDeparture       string `json:\"isDeparture\"`\n\tBlock             string `json:\"block\"`\n\tDirTag            string `json:\"dirTag\"`\n\tBranch            string `json:\"branch\"`\n\tEpochTime         string `json:\"epochTime\"`\n\tVehicle           string `json:\"vehicle\"`\n}\n\ntype Direction struct {\n\tPredictionRaw json.RawMessage `json:\"prediction\"`\n\tTitle         string          `json:\"title\"`\n}\n\ntype Predictions struct {\n\tRouteTag    string    `json:\"routeTag\"`\n\tStopTag     string    `json:\"stopTag\"`\n\tRouteTitle  string    `json:\"routeTitle\"`\n\tAgencyTitle string    `json:\"agencyTitle\"`\n\tStopTitle   string    `json:\"stopTitle\"`\n\tDirection   Direction `json:\"direction\"`\n}\n\nfunc getNextBus(agency string, route string, stopID string) string {\n\turl := fmt.Sprintf(\"https://webservices.umoiq.com/service/publicJSONFeed?command=predictions&a=%s&r=%s&stopId=%s\", agency, route, stopID)\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\tlogger.Log(fmt.Sprintf(\"[nextbus] Error: Failed to make requests to umoiq for next bus predictions. Reason: %s\", err))\n\t\treturn \"[nextbus] error calling umoiq\"\n\t}\n\n\tbody, readErr := io.ReadAll(resp.Body)\n\tif readErr != nil {\n\t\tlogger.Log(fmt.Sprintf(\"[nextbus] Error: Failed to parse response body from umoiq. Reason: %s\", err))\n\t\treturn \"[nextbus] error parsing response body\"\n\t}\n\tdefer resp.Body.Close()\n\n\tvar parsedResponse AutoGenerated\n\n\t// partial unmarshal, we don't have r.Predictions.Direction.PredictionRaw <- YET\n\tunmarshalError := json.Unmarshal(body, &parsedResponse)\n\tif unmarshalError != nil {\n\t\tlogger.Log(fmt.Sprintf(\"[nextbus] Error: Failed to unmarshal body from umoiq. Reason: %s\", err))\n\t\treturn \"[nextbus] error unmarshalling response body\"\n\t}\n\n\tparseType := \"\"\n\t// hacky, try object parse first\n\tnextBusObject := Prediction{}\n\tif err := json.Unmarshal(parsedResponse.Predictions.Direction.PredictionRaw, &nextBusObject); err == nil {\n\t\tparseType = \"object\"\n\t}\n\n\t// if object parse failed, it probably means we have an array\n\tnextBuses := []Prediction{}\n\tif err := json.Unmarshal(parsedResponse.Predictions.Direction.PredictionRaw, &nextBuses); err == nil {\n\t\tparseType = \"array\"\n\t}\n\n\t// build the final string\n\tfinalStr := \"\"\n\tif parseType == \"array\" {\n\t\tfor _, nextBus := range nextBuses {\n\t\t\tfinalStr += fmt.Sprintf(\"%s | ETA [%s]\\n\", parsedResponse.Predictions.RouteTitle, strTimeToInt(nextBus.Minutes, nextBus.Seconds))\n\t\t}\n\t} else {\n\t\tfinalStr += fmt.Sprintf(\"%s | ETA [%s]\\n\", parsedResponse.Predictions.RouteTitle, strTimeToInt(nextBusObject.Minutes, nextBusObject.Seconds))\n\t}\n\n\treturn finalStr\n}\n\n// takes minutes and seconds from the API, does math to find the remainder seconds\n// since the API only gives whole minutes\nfunc strTimeToInt(sourceMinutes string, sourceSeconds string) string {\n\tmin, _ := strconv.Atoi(sourceMinutes)\n\tsec, _ := strconv.Atoi(sourceSeconds)\n\tsec = sec % 60\n\treturn fmt.Sprintf(\"%02d:%02d\", min, sec)\n}\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(func() (string, string, bool) {\n\t\treturn widget.CommonSettings().Title, widget.content(), false\n\t})\n}\n"
  },
  {
    "path": "modules/opsgenie/client.go",
    "content": "package opsgenie\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype OnCallResponse struct {\n\tOnCallData OnCallData `json:\"data\"`\n\tMessage    string     `json:\"message\"`\n\tRequestID  string     `json:\"requestId\"`\n\tTook       float32    `json:\"took\"`\n}\n\ntype OnCallData struct {\n\tRecipients []string `json:\"onCallRecipients\"`\n\tParent     Parent   `json:\"_parent\"`\n}\n\ntype Parent struct {\n\tID      string `json:\"id\"`\n\tName    string `json:\"name\"`\n\tEnabled bool   `json:\"enabled\"`\n}\n\nvar opsGenieAPIUrl = map[string]string{\n\t\"us\": \"https://api.opsgenie.com\",\n\t\"eu\": \"https://api.eu.opsgenie.com\",\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Fetch(scheduleIdentifierType string, schedules []string) ([]*OnCallResponse, error) {\n\tagregatedResponses := []*OnCallResponse{}\n\n\tif regionURL, regionErr := opsGenieAPIUrl[widget.settings.region]; regionErr {\n\t\tfor _, sched := range schedules {\n\t\t\tscheduleURL := fmt.Sprintf(\"%s/v2/schedules/%s/on-calls?scheduleIdentifierType=%s&flat=true\", regionURL, sched, scheduleIdentifierType)\n\t\t\tresponse, err := opsGenieRequest(scheduleURL, widget.settings.apiKey)\n\t\t\tagregatedResponses = append(agregatedResponses, response)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn agregatedResponses, nil\n\t} else {\n\t\treturn nil, fmt.Errorf(\"you specified wrong region. Possible options are only 'us' and 'eu'\")\n\t}\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc opsGenieRequest(url string, apiKey string) (*OnCallResponse, error) {\n\treq, err := http.NewRequest(\"GET\", url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"GenieKey %s\", apiKey))\n\n\tclient := &http.Client{}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tresponse := &OnCallResponse{}\n\tif err := json.NewDecoder(resp.Body).Decode(response); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn response, nil\n}\n"
  },
  {
    "path": "modules/opsgenie/settings.go",
    "content": "package opsgenie\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"OpsGenie\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey                 string   `help:\"Your OpsGenie API token.\"`\n\tregion                 string   `help:\"Defines region to use. Possible options: us (by default), eu.\" optional:\"true\"`\n\tdisplayEmpty           bool     `help:\"Whether schedules with no assigned person on-call should be displayed.\" optional:\"true\"`\n\tschedule               []string `help:\"A list of names of the schedule(s) to retrieve.\"`\n\tscheduleIdentifierType string   `help:\"Type of the schedule identifier.\" values:\"id or name\" optional:\"true\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:                 ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_OPS_GENIE_API_KEY\"))),\n\t\tregion:                 ymlConfig.UString(\"region\", \"us\"),\n\t\tdisplayEmpty:           ymlConfig.UBool(\"displayEmpty\", true),\n\t\tscheduleIdentifierType: ymlConfig.UString(\"scheduleIdentifierType\", \"id\"),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\tsettings.schedule = settings.arrayifySchedules(ymlConfig)\n\n\treturn &settings\n}\n\n// arrayifySchedules figures out if we're dealing with a single project or an array of projects\nfunc (settings *Settings) arrayifySchedules(ymlConfig *config.Config) []string {\n\tschedules := []string{}\n\n\t// Single schedule\n\tschedule, err := ymlConfig.String(\"schedule\")\n\tif err == nil {\n\t\tschedules = append(schedules, schedule)\n\t\treturn schedules\n\t}\n\n\t// Array of schedules\n\tscheduleList := ymlConfig.UList(\"schedule\")\n\tfor _, scheduleName := range scheduleList {\n\t\tif schedule, ok := scheduleName.(string); ok {\n\t\t\tschedules = append(schedules, schedule)\n\t\t}\n\t}\n\n\treturn schedules\n}\n"
  },
  {
    "path": "modules/opsgenie/widget.go",
    "content": "package opsgenie\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tonCallResponses, err := widget.Fetch(\n\t\twidget.settings.scheduleIdentifierType,\n\t\twidget.settings.schedule,\n\t)\n\ttitle := widget.CommonSettings().Title\n\n\tvar content string\n\twrap := false\n\tif err != nil {\n\t\twrap = true\n\t\tcontent = err.Error()\n\t} else {\n\n\t\tfor _, data := range onCallResponses {\n\t\t\tif (len(data.OnCallData.Recipients) == 0) && !widget.settings.displayEmpty {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar msg string\n\t\t\tif len(data.OnCallData.Recipients) == 0 {\n\t\t\t\tmsg = \" [gray]no one[white]\\n\\n\"\n\t\t\t} else {\n\t\t\t\tmsg = fmt.Sprintf(\" %s\\n\\n\", strings.Join(utils.NamesFromEmails(data.OnCallData.Recipients), \", \"))\n\t\t\t}\n\n\t\t\tcontent += widget.cleanScheduleName(data.OnCallData.Parent.Name)\n\t\t\tcontent += msg\n\t\t}\n\t}\n\n\treturn title, content, wrap\n}\n\nfunc (widget *Widget) cleanScheduleName(schedule string) string {\n\tcleanedName := strings.ReplaceAll(schedule, \"_\", \" \")\n\treturn fmt.Sprintf(\" [green]%s[white]\\n\", cleanedName)\n}\n"
  },
  {
    "path": "modules/pagerduty/client.go",
    "content": "package pagerduty\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/PagerDuty/go-pagerduty\"\n)\n\nconst (\n\tqueryTimeFmt = \"2006-01-02T15:04:05Z07:00\"\n)\n\n// GetOnCalls returns a list of people currently on call\nfunc GetOnCalls(apiKey string, scheduleIDs []string) ([]pagerduty.OnCall, error) {\n\tclient := pagerduty.NewClient(apiKey)\n\n\tvar results []pagerduty.OnCall\n\tvar queryOpts pagerduty.ListOnCallOptions\n\n\tqueryOpts.ScheduleIDs = scheduleIDs\n\tqueryOpts.Since = time.Now().Format(queryTimeFmt)\n\tqueryOpts.Until = time.Now().Format(queryTimeFmt)\n\n\toncalls, err := client.ListOnCallsWithContext(context.Background(), queryOpts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults = append(results, oncalls.OnCalls...)\n\n\tfor oncalls.More {\n\t\tqueryOpts.Offset = oncalls.Offset\n\t\toncalls, err = client.ListOnCallsWithContext(context.Background(), queryOpts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresults = append(results, oncalls.OnCalls...)\n\t}\n\n\treturn results, nil\n}\n\n// GetIncidents returns a list of unresolved incidents\nfunc GetIncidents(apiKey string, teamIDs []string, userIDs []string) ([]pagerduty.Incident, error) {\n\tclient := pagerduty.NewClient(apiKey)\n\n\tvar results []pagerduty.Incident\n\n\tvar queryOpts pagerduty.ListIncidentsOptions\n\tqueryOpts.DateRange = \"all\"\n\tqueryOpts.Statuses = []string{\"triggered\", \"acknowledged\"}\n\tqueryOpts.TeamIDs = teamIDs\n\tqueryOpts.UserIDs = userIDs\n\n\titems, err := client.ListIncidentsWithContext(context.Background(), queryOpts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresults = append(results, items.Incidents...)\n\n\tfor items.More {\n\t\tqueryOpts.Offset = items.Offset\n\t\titems, err = client.ListIncidentsWithContext(context.Background(), queryOpts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresults = append(results, items.Incidents...)\n\t}\n\n\treturn results, nil\n}\n"
  },
  {
    "path": "modules/pagerduty/settings.go",
    "content": "package pagerduty\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"PagerDuty\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey           string        `help:\"Your PagerDuty API key.\"`\n\tescalationFilter []interface{} `help:\"An array of schedule names you want to filter the OnCalls on.\"`\n\tmyName           string        `help:\"The name to highlight when on-call in PagerDuty.\"`\n\tscheduleIDs      []interface{} `help:\"An array of schedule IDs you want to restrict the OnCalls query to.\"`\n\tshowIncidents    bool          `help:\"Whether or not to list incidents.\" optional:\"true\"`\n\tshowOnCallEnd    bool          `help:\"Whether or not to display the date the oncall schedule ends.\" optional:\"true\"`\n\tshowSchedules    bool          `help:\"Whether or not to show schedules.\" optional:\"true\"`\n\tteamIDs          []interface{} `help:\"An array of team IDs to restrict the incidents query to\" optional:\"true\"`\n\tuserIDs          []interface{} `help:\"An array of user IDs to restrict the incidents query to\" optional:\"true\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:           ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_PAGERDUTY_API_KEY\"))),\n\t\tescalationFilter: ymlConfig.UList(\"escalationFilter\"),\n\t\tmyName:           ymlConfig.UString(\"myName\"),\n\t\tscheduleIDs:      ymlConfig.UList(\"scheduleIDs\", []interface{}{}),\n\t\tshowIncidents:    ymlConfig.UBool(\"showIncidents\", true),\n\t\tshowOnCallEnd:    ymlConfig.UBool(\"showOnCallEnd\", false),\n\t\tshowSchedules:    ymlConfig.UBool(\"showSchedules\", true),\n\t\tteamIDs:          ymlConfig.UList(\"teamIDs\", []interface{}{}),\n\t\tuserIDs:          ymlConfig.UList(\"userIDs\", []interface{}{}),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/pagerduty/sort.go",
    "content": "package pagerduty\n\nimport \"github.com/PagerDuty/go-pagerduty\"\n\ntype ByEscalationLevel []pagerduty.OnCall\n\nfunc (s ByEscalationLevel) Len() int      { return len(s) }\nfunc (s ByEscalationLevel) Swap(i, j int) { s[i], s[j] = s[j], s[i] }\n\nfunc (s ByEscalationLevel) Less(i, j int) bool {\n\treturn s[i].EscalationLevel < s[j].EscalationLevel\n}\n"
  },
  {
    "path": "modules/pagerduty/widget.go",
    "content": "package pagerduty\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/PagerDuty/go-pagerduty\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst (\n\tonCallTimeAPILayout     = \"2006-01-02T15:04:05Z\"\n\tonCallTimeDisplayLayout = \"Jan 2, 2006\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n}\n\n// NewWidget creates and returns an instance of PagerDuty widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tvar onCalls []pagerduty.OnCall\n\tvar incidents []pagerduty.Incident\n\n\tvar err1 error\n\tvar err2 error\n\n\tif widget.settings.showIncidents {\n\t\tteamIDs := utils.ToStrs(widget.settings.teamIDs)\n\t\tuserIDs := utils.ToStrs(widget.settings.userIDs)\n\t\tincidents, err2 = GetIncidents(widget.settings.apiKey, teamIDs, userIDs)\n\t}\n\n\tif widget.settings.showSchedules {\n\t\tscheduleIDs := utils.ToStrs(widget.settings.scheduleIDs)\n\t\tonCalls, err1 = GetOnCalls(widget.settings.apiKey, scheduleIDs)\n\t}\n\n\tvar content string\n\twrap := false\n\tif err1 != nil || err2 != nil {\n\t\twrap = true\n\t\tif err1 != nil {\n\t\t\tcontent += err1.Error()\n\t\t}\n\t\tif err2 != nil {\n\t\t\tcontent += err2.Error()\n\t\t}\n\t} else {\n\t\twidget.View.SetWrap(false)\n\t\tcontent = widget.contentFrom(onCalls, incidents)\n\t}\n\n\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, content, wrap })\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) contentFrom(onCalls []pagerduty.OnCall, incidents []pagerduty.Incident) string {\n\tvar str string\n\n\t// Incidents\n\n\tif widget.settings.showIncidents {\n\t\tstr += fmt.Sprintf(\"[%s] Incidents[white]\\n\", widget.settings.Colors.Subheading)\n\n\t\tif len(incidents) > 0 {\n\t\t\tfor _, incident := range incidents {\n\t\t\t\tstr += fmt.Sprintf(\"\\n [%s]%s[white]\\n\", widget.settings.Colors.Label, tview.Escape(incident.Summary))\n\t\t\t\tstr += fmt.Sprintf(\"     Status: %s\\n\", incident.Status)\n\t\t\t\tstr += fmt.Sprintf(\"    Service: %s\\n\", incident.Service.Summary)\n\t\t\t\tstr += fmt.Sprintf(\" Escalation: %s\\n\", incident.EscalationPolicy.Summary)\n\t\t\t\tstr += fmt.Sprintf(\"       Link: %s\\n\", incident.HTMLURL)\n\t\t\t}\n\t\t} else {\n\t\t\tstr += \"\\n No open incidents\\n\"\n\t\t}\n\n\t\tstr += \"\\n\"\n\t}\n\n\tonCallTree := make(map[string][]pagerduty.OnCall)\n\n\tfilter := make(map[string]bool)\n\tfor _, item := range widget.settings.escalationFilter {\n\t\tfilter[item.(string)] = true\n\t}\n\n\t// OnCalls\n\n\tfor _, onCall := range onCalls {\n\t\tsummary := onCall.EscalationPolicy.Summary\n\t\tif len(widget.settings.escalationFilter) == 0 || filter[summary] {\n\t\t\tonCallTree[summary] = append(onCallTree[summary], onCall)\n\t\t}\n\t}\n\n\t// We want to sort our escalation policies for predictability/ease of finding\n\tkeys := make([]string, 0, len(onCallTree))\n\tfor k := range onCallTree {\n\t\tkeys = append(keys, k)\n\t}\n\n\tsort.Strings(keys)\n\n\tif len(keys) > 0 {\n\t\tstr += fmt.Sprintf(\"[%s] Schedules[white]\\n\", widget.settings.Colors.Subheading)\n\n\t\t// Print out policies, and escalation order of users\n\t\tfor _, key := range keys {\n\t\t\tstr += fmt.Sprintf(\n\t\t\t\t\"\\n [%s]%s\\n\",\n\t\t\t\twidget.settings.Colors.Label,\n\t\t\t\tkey,\n\t\t\t)\n\n\t\t\tvalues := onCallTree[key]\n\t\t\tsort.Sort(ByEscalationLevel(values))\n\n\t\t\tfor _, onCall := range values {\n\t\t\t\tstr += fmt.Sprintf(\n\t\t\t\t\t\" [%s]%d - %s\\n\",\n\t\t\t\t\twidget.settings.Colors.Text,\n\t\t\t\t\tonCall.EscalationLevel,\n\t\t\t\t\twidget.userSummary(&onCall),\n\t\t\t\t)\n\n\t\t\t\tonCallEnd := widget.onCallEndSummary(&onCall)\n\t\t\t\tif onCallEnd != \"\" {\n\t\t\t\t\tstr += fmt.Sprintf(\n\t\t\t\t\t\t\"     %s\\n\",\n\t\t\t\t\t\tonCallEnd,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn str\n}\n\n// onCallEndSummary may or may not return the date that the specified onCall schedule ends\nfunc (widget *Widget) onCallEndSummary(onCall *pagerduty.OnCall) string {\n\tif !widget.settings.showOnCallEnd {\n\t\treturn \"\"\n\t}\n\n\tif onCall.End == \"\" {\n\t\treturn \"\"\n\t}\n\n\tend, err := time.Parse(onCallTimeAPILayout, onCall.End)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn end.Format(onCallTimeDisplayLayout)\n}\n\n// userSummary returns the name of the person assigned to the specified onCall schedule\nfunc (widget *Widget) userSummary(onCall *pagerduty.OnCall) string {\n\tsummary := onCall.User.Summary\n\n\tif summary == widget.settings.myName {\n\t\tsummary = fmt.Sprintf(\"[::b]%s\", summary)\n\t}\n\n\treturn summary\n}\n"
  },
  {
    "path": "modules/pihole/client.go",
    "content": "package pihole\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\turl2 \"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype Status struct {\n\tDomainsBeingBlocked string `json:\"domains_being_blocked\"`\n\tDNSQueriesToday     string `json:\"dns_queries_today\"`\n\tAdsBlockedToday     string `json:\"ads_blocked_today\"`\n\tAdsPercentageToday  string `json:\"ads_percentage_today\"`\n\tUniqueDomains       string `json:\"unique_domains\"`\n\tQueriesForwarded    string `json:\"queries_forwarded\"`\n\tQueriesCached       string `json:\"queries_cached\"`\n\tStatus              string `json:\"status\"`\n\tGravityLastUpdated  struct {\n\t\tRelative struct {\n\t\t\tDays    FlexInt `json:\"days\"`\n\t\t\tHours   FlexInt `json:\"hours\"`\n\t\t\tMinutes FlexInt `json:\"minutes\"`\n\t\t}\n\t} `json:\"gravity_last_updated\"`\n}\n\nfunc getStatus(c http.Client, apiURL string) (status Status, err error) {\n\tvar req *http.Request\n\n\tvar url *url2.URL\n\n\tif url, err = url2.Parse(apiURL); err != nil {\n\t\treturn status, fmt.Errorf(\" failed to parse API URL\\n %s\", parseError(err))\n\t}\n\n\tvar query url2.Values\n\n\tif query, err = url2.ParseQuery(url.RawQuery); err != nil {\n\t\treturn status, fmt.Errorf(\" failed to parse query\\n %s\", parseError(err))\n\t}\n\n\tquery.Add(\"summary\", \"\")\n\n\turl.RawQuery = query.Encode()\n\tif req, err = http.NewRequest(\"GET\", url.String(), http.NoBody); err != nil {\n\t\treturn status, fmt.Errorf(\" failed to create request\\n %s\", parseError(err))\n\t}\n\n\tvar resp *http.Response\n\n\tif resp, err = c.Do(req); err != nil || resp == nil {\n\t\treturn status, fmt.Errorf(\" failed to connect to Pi-hole server\\n %s\", parseError(err))\n\t}\n\n\tdefer func() {\n\t\tif closeErr := resp.Body.Close(); closeErr != nil {\n\t\t\treturn\n\t\t}\n\t}()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn status, fmt.Errorf(\" failed to retrieve version from Pi-hole server\\n http status code: %d\",\n\t\t\tresp.StatusCode)\n\t}\n\n\tvar rBody []byte\n\n\tif rBody, err = io.ReadAll(resp.Body); err != nil {\n\t\treturn status, fmt.Errorf(\" failed to read status response\")\n\t}\n\n\tif err = json.Unmarshal(rBody, &status); err != nil {\n\t\treturn status, fmt.Errorf(\" failed to retrieve status: check provided api URL and token\\n %s\",\n\t\t\tparseError(err))\n\t}\n\n\treturn status, err\n}\n\ntype FlexInt int\n\nfunc (fi *FlexInt) UnmarshalJSON(b []byte) error {\n\tif b[0] != '\"' {\n\t\treturn json.Unmarshal(b, (*int)(fi))\n\t}\n\n\tvar s string\n\n\tif err := json.Unmarshal(b, &s); err != nil {\n\t\treturn err\n\t}\n\n\ti, err := strconv.Atoi(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t*fi = FlexInt(i)\n\n\treturn nil\n}\n\ntype TopItems struct {\n\tTopQueries map[string]int `json:\"top_queries\"`\n\tTopAds     map[string]int `json:\"top_ads\"`\n}\n\nfunc getTopItems(c http.Client, settings *Settings) (ti TopItems, err error) {\n\tvar req *http.Request\n\n\tvar url *url2.URL\n\n\tif url, err = url2.Parse(settings.apiUrl); err != nil {\n\t\treturn ti, fmt.Errorf(\" failed to parse API URL\\n %s\", parseError(err))\n\t}\n\n\tvar query url2.Values\n\n\tif query, err = url2.ParseQuery(url.RawQuery); err != nil {\n\t\treturn ti, fmt.Errorf(\" failed to parse query\\n %s\", parseError(err))\n\t}\n\n\tquery.Add(\"auth\", settings.token)\n\tquery.Add(\"topItems\", strconv.Itoa(settings.showTopItems))\n\n\turl.RawQuery = query.Encode()\n\n\treq, err = http.NewRequest(\"GET\", url.String(), http.NoBody)\n\tif err != nil {\n\t\treturn ti, fmt.Errorf(\" failed to create request\\n %s\", parseError(err))\n\t}\n\n\tvar resp *http.Response\n\n\tif resp, err = c.Do(req); err != nil || resp == nil {\n\t\treturn ti, fmt.Errorf(\" failed to connect to Pi-hole server\\n %s\", parseError(err))\n\t}\n\n\tdefer func() {\n\t\tif closeErr := resp.Body.Close(); closeErr != nil {\n\t\t\treturn\n\t\t}\n\t}()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn ti, fmt.Errorf(\" failed to retrieve version from Pi-hole server\\n http status code: %d\",\n\t\t\tresp.StatusCode)\n\t}\n\n\tvar rBody []byte\n\n\trBody, err = io.ReadAll(resp.Body)\n\tif err = json.Unmarshal(rBody, &ti); err != nil {\n\t\treturn ti, fmt.Errorf(\" failed to retrieve top items: check provided api URL and token\\n %s\",\n\t\t\tparseError(err))\n\t}\n\n\treturn ti, err\n}\n\ntype TopClients struct {\n\tTopSources map[string]int `json:\"top_sources\"`\n}\n\n// parseError removes any token from output and ensures a non-nil response\nfunc parseError(err error) string {\n\tif err == nil {\n\t\treturn \"unknown error\"\n\t}\n\n\tvar re = regexp.MustCompile(`auth=[a-zA-Z0-9]*`)\n\n\treturn re.ReplaceAllString(err.Error(), \"auth=<token>\")\n}\n\nfunc getTopClients(c http.Client, settings *Settings) (tc TopClients, err error) {\n\tvar req *http.Request\n\n\tvar url *url2.URL\n\n\tif url, err = url2.Parse(settings.apiUrl); err != nil {\n\t\treturn tc, fmt.Errorf(\" failed to parse API URL\\n %s\", parseError(err))\n\t}\n\n\tvar query url2.Values\n\n\tif query, err = url2.ParseQuery(url.RawQuery); err != nil {\n\t\treturn tc, fmt.Errorf(\" failed to parse query\\n %s\", parseError(err))\n\t}\n\n\tquery.Add(\"topClients\", strconv.Itoa(settings.showTopClients))\n\tquery.Add(\"auth\", settings.token)\n\turl.RawQuery = query.Encode()\n\n\tif req, err = http.NewRequest(\"GET\", url.String(), http.NoBody); err != nil {\n\t\treturn tc, fmt.Errorf(\" failed to create request\\n %s\", parseError(err))\n\t}\n\n\tvar resp *http.Response\n\n\tif resp, err = c.Do(req); err != nil || resp == nil {\n\t\treturn tc, fmt.Errorf(\" failed to connect to Pi-hole server\\n %s\", parseError(err))\n\t}\n\n\tdefer func() {\n\t\tif closeErr := resp.Body.Close(); closeErr != nil {\n\t\t\treturn\n\t\t}\n\t}()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn tc, fmt.Errorf(\" failed to retrieve version from Pi-hole server\\n http status code: %d\",\n\t\t\tresp.StatusCode)\n\t}\n\n\tvar rBody []byte\n\n\tif rBody, err = io.ReadAll(resp.Body); err != nil {\n\t\treturn tc, fmt.Errorf(\" failed to read top clients response\\n %s\", parseError(err))\n\t}\n\n\tif err = json.Unmarshal(rBody, &tc); err != nil {\n\t\treturn tc, fmt.Errorf(\" failed to retrieve top clients: check provided api URL and token\\n %s\",\n\t\t\tparseError(err))\n\t}\n\n\treturn tc, err\n}\n\ntype QueryTypes struct {\n\tQueryTypes map[string]float32 `json:\"querytypes\"`\n}\n\nfunc getQueryTypes(c http.Client, settings *Settings) (qt QueryTypes, err error) {\n\tvar req *http.Request\n\n\tvar url *url2.URL\n\n\tif url, err = url2.Parse(settings.apiUrl); err != nil {\n\t\treturn qt, fmt.Errorf(\" failed to parse API URL\\n %s\", parseError(err))\n\t}\n\n\tvar query url2.Values\n\n\tif query, err = url2.ParseQuery(url.RawQuery); err != nil {\n\t\treturn qt, fmt.Errorf(\" failed to parse query\\n %s\", parseError(err))\n\t}\n\n\tquery.Add(\"getQueryTypes\", strconv.Itoa(settings.showTopClients))\n\tquery.Add(\"auth\", settings.token)\n\n\turl.RawQuery = query.Encode()\n\n\tif req, err = http.NewRequest(\"GET\", url.String(), http.NoBody); err != nil {\n\t\treturn qt, fmt.Errorf(\" failed to create request\\n %s\", parseError(err))\n\t}\n\n\tvar resp *http.Response\n\n\tif resp, err = c.Do(req); err != nil || resp == nil {\n\t\treturn qt, fmt.Errorf(\" failed to connect to Pi-hole server\\n %s\", parseError(err))\n\t}\n\n\tdefer func() {\n\t\tif closeErr := resp.Body.Close(); closeErr != nil {\n\t\t\treturn\n\t\t}\n\t}()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn qt, fmt.Errorf(\" failed to retrieve version from Pi-hole server\\n http status code: %d\",\n\t\t\tresp.StatusCode)\n\t}\n\n\tvar rBody []byte\n\n\tif rBody, err = io.ReadAll(resp.Body); err != nil {\n\t\treturn qt, fmt.Errorf(\" failed to read top clients response\\n %s\", parseError(err))\n\t}\n\n\tif err = json.Unmarshal(rBody, &qt); err != nil {\n\t\treturn qt, fmt.Errorf(\" failed to parse query types response\\n %s\", parseError(err))\n\t}\n\n\treturn qt, err\n}\n\nfunc checkServer(c http.Client, apiURL string) error {\n\tvar err error\n\n\tvar req *http.Request\n\n\tvar url *url2.URL\n\n\tif url, err = url2.Parse(apiURL); err != nil {\n\t\treturn fmt.Errorf(\" failed to parse API URL\\n %s\", parseError(err))\n\t}\n\n\tif url.Host == \"\" {\n\t\treturn fmt.Errorf(\" please specify 'apiUrl' in Pi-hole settings, e.g.\\n apiUrl: http://<server>:<port>/admin/api.php\")\n\t}\n\n\tif req, err = http.NewRequest(\"GET\", fmt.Sprintf(\"%s?version\",\n\t\turl.String()), http.NoBody); err != nil {\n\t\treturn fmt.Errorf(\"invalid request: %s\", parseError(err))\n\t}\n\n\tvar resp *http.Response\n\n\tif resp, err = c.Do(req); err != nil {\n\t\treturn fmt.Errorf(\" failed to connect to Pi-hole server\\n %s\", parseError(err))\n\t}\n\n\tdefer func() {\n\t\t_ = resp.Body.Close()\n\t}()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn fmt.Errorf(\" failed to retrieve version from Pi-hole server\\n http status code: %d\",\n\t\t\tresp.StatusCode)\n\t}\n\n\tvar vResp struct {\n\t\tVersion int `json:\"version\"`\n\t}\n\n\tvar rBody []byte\n\n\tif rBody, err = io.ReadAll(resp.Body); err != nil {\n\t\treturn fmt.Errorf(\" Pi-hole server failed to respond\\n %s\", parseError(err))\n\t}\n\n\tif err = json.Unmarshal(rBody, &vResp); err != nil {\n\t\treturn fmt.Errorf(\" invalid response returned from Pi-hole Server\\n %s\", parseError(err))\n\t}\n\n\tif vResp.Version != 3 {\n\t\treturn fmt.Errorf(\" only Pi-hole API version 3 is supported\\n version %d was detected\", vResp.Version)\n\t}\n\n\treturn err\n}\n\nfunc (widget *Widget) adblockSwitch(action string) {\n\tvar req *http.Request\n\n\tvar url *url2.URL\n\turl, _ = url2.Parse(widget.settings.apiUrl)\n\n\tvar query url2.Values\n\tquery, _ = url2.ParseQuery(url.RawQuery)\n\n\tquery.Add(strings.ToLower(action), \"\")\n\tquery.Add(\"auth\", widget.settings.token)\n\n\turl.RawQuery = query.Encode()\n\n\treq, _ = http.NewRequest(\"GET\", url.String(), http.NoBody)\n\n\tc := getClient()\n\tresp, _ := c.Do(req)\n\n\tdefer func() {\n\t\t_ = resp.Body.Close()\n\t}()\n\n\twidget.Refresh()\n}\n\nfunc getClient() http.Client {\n\treturn http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSHandshakeTimeout:   5 * time.Second,\n\t\t\tDisableKeepAlives:     false,\n\t\t\tDisableCompression:    false,\n\t\t\tResponseHeaderTimeout: 20 * time.Second,\n\t\t},\n\t\tTimeout: 21 * time.Second,\n\t}\n}\n"
  },
  {
    "path": "modules/pihole/keyboard.go",
    "content": "package pihole\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"d\", widget.disable, \"disable Pi-hole\")\n\twidget.SetKeyboardChar(\"e\", widget.enable, \"enable Pi-hole\")\n}\n"
  },
  {
    "path": "modules/pihole/settings.go",
    "content": "package pihole\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Pi-hole\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\twrapText       bool\n\tapiUrl         string\n\ttoken          string\n\tshowTopItems   int\n\tshowTopClients int\n\tmaxClientWidth int\n\tmaxDomainWidth int\n\tshowSummary    bool\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiUrl:         ymlConfig.UString(\"apiUrl\"),\n\t\ttoken:          ymlConfig.UString(\"token\"),\n\t\tshowSummary:    ymlConfig.UBool(\"showSummary\", true),\n\t\tshowTopItems:   ymlConfig.UInt(\"showTopItems\", 5),\n\t\tshowTopClients: ymlConfig.UInt(\"showTopClients\", 5),\n\t\tmaxClientWidth: ymlConfig.UInt(\"maxClientWidth\", 20),\n\t\tmaxDomainWidth: ymlConfig.UInt(\"maxDomainWidth\", 20),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.token).\n\t\tService(settings.apiUrl).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/pihole/view.go",
    "content": "package pihole\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/olekukonko/tablewriter\"\n)\n\nfunc getSummaryView(c http.Client, settings *Settings) string {\n\tvar err error\n\n\tvar s Status\n\n\turl := settings.apiUrl + \"?status&auth=\" + settings.token\n\n\ts, err = getStatus(c, url)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\tvar sb strings.Builder\n\n\tbuf := new(bytes.Buffer)\n\n\tswitch strings.ToLower(s.Status) {\n\tcase \"disabled\":\n\t\tsb.WriteString(\" [white]Status [red]DISABLED\\n\")\n\tcase \"enabled\":\n\t\tsb.WriteString(\" [white]Status [green]ENABLED\\n\")\n\tdefault:\n\t\tsb.WriteString(\" [white]Status [yellow]UNKNOWN\\n\")\n\t}\n\n\tsummaryTable := createTable([]string{}, buf)\n\tsummaryTable.Append([]string{\"Domain blocklist\", s.DomainsBeingBlocked, \"Queries today\", s.DNSQueriesToday})\n\tsummaryTable.Append([]string{\"Ads blocked today\", fmt.Sprintf(\"%s (%s%%)\", s.AdsBlockedToday, s.AdsPercentageToday), \"Cached queries\", s.QueriesCached})\n\tsummaryTable.Append([]string{\"Blocklist Age\", fmt.Sprintf(\"%dd %dh %dm\", s.GravityLastUpdated.Relative.Days,\n\t\ts.GravityLastUpdated.Relative.Hours, s.GravityLastUpdated.Relative.Minutes), \"Forwarded queries\", s.QueriesForwarded})\n\tsummaryTable.Render()\n\n\tsb.WriteString(buf.String())\n\n\treturn sb.String()\n}\n\nfunc getTopItemsView(c http.Client, settings *Settings) string {\n\tvar err error\n\n\tvar ti TopItems\n\n\tti, err = getTopItems(c, settings)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\tbuf := new(bytes.Buffer)\n\n\tvar sb strings.Builder\n\n\ttiTable := createTable([]string{\"Top Queries\", \"\", \"Top Ads\", \"\"}, buf)\n\n\tlargest := len(ti.TopAds)\n\tif len(ti.TopQueries) > largest {\n\t\tlargest = len(ti.TopQueries)\n\t}\n\n\tsortedTiQueries := sortMapByIntVal(ti.TopQueries)\n\n\tsortedTiAds := sortMapByIntVal(ti.TopAds)\n\n\tfor x := 0; x < largest; x++ {\n\t\ttiQVal := []string{\"\", \"\"}\n\t\tif len(sortedTiQueries) > x {\n\t\t\ttiQVal = []string{shorten(sortedTiQueries[x][0], settings.maxDomainWidth), sortedTiQueries[x][1]}\n\t\t}\n\n\t\ttiAVal := []string{\"\", \"\"}\n\n\t\tif len(sortedTiAds) > x {\n\t\t\ttiAVal = []string{shorten(sortedTiAds[x][0], settings.maxDomainWidth), sortedTiAds[x][1]}\n\t\t}\n\n\t\ttiTable.Append([]string{tiQVal[0], tiQVal[1], tiAVal[0], tiAVal[1]})\n\t}\n\n\ttiTable.Render()\n\tsb.WriteString(buf.String())\n\n\treturn sb.String()\n}\n\nfunc getTopClientsView(c http.Client, settings *Settings) string {\n\ttc, err := getTopClients(c, settings)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\tvar tq QueryTypes\n\n\ttq, err = getQueryTypes(c, settings)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\tbuf := new(bytes.Buffer)\n\n\ttcTable := createTable([]string{\"Top Clients\", \"\", \"Top Query Types\", \"\"}, buf)\n\n\tsortedTcQueries := sortMapByIntVal(tc.TopSources)\n\n\tsortedTopQT := sortMapByFloatVal(tq.QueryTypes)\n\n\tlargest := len(tc.TopSources)\n\n\tif len(tq.QueryTypes) > largest {\n\t\tlargest = len(tq.QueryTypes)\n\t}\n\n\tif settings.showTopClients < largest {\n\t\tlargest = settings.showTopClients\n\t}\n\n\tfor x := 0; x < largest; x++ {\n\t\ttcVal := []string{\"\", \"\"}\n\n\t\tif len(sortedTcQueries) > x {\n\t\t\ttcVal = []string{sortedTcQueries[x][0], sortedTcQueries[x][1]}\n\t\t}\n\n\t\ttqtVal := []string{\"\", \"\"}\n\n\t\tif len(sortedTopQT) > x && sortedTopQT[x][0] != \"\" {\n\t\t\ttqtVal = []string{sortedTopQT[x][0], sortedTopQT[x][1] + \"%\"}\n\t\t}\n\n\t\ttcTable.Append([]string{tcVal[0], tcVal[1], tqtVal[0], tqtVal[1]})\n\t}\n\n\ttcTable.Render()\n\n\tvar sb strings.Builder\n\n\tsb.WriteString(buf.String())\n\n\treturn sb.String()\n}\n\nfunc shorten(s string, limit int) string {\n\tif len(s) > limit {\n\t\treturn s[:limit] + \"...\"\n\t}\n\n\treturn s\n}\n\nfunc createTable(header []string, buf io.Writer) *tablewriter.Table {\n\ttable := tablewriter.NewWriter(buf)\n\n\tif len(header) != 0 {\n\t\ttable.SetHeader(header)\n\t\ttable.SetHeaderLine(false)\n\t\ttable.SetHeaderAlignment(0)\n\t}\n\n\ttable.SetAutoWrapText(false)\n\ttable.SetAutoFormatHeaders(true)\n\ttable.SetHeaderAlignment(tablewriter.ALIGN_LEFT)\n\ttable.SetAlignment(tablewriter.ALIGN_LEFT)\n\ttable.SetBorder(true)\n\ttable.SetCenterSeparator(\"\")\n\ttable.SetColumnSeparator(\"\")\n\ttable.SetRowSeparator(\"\")\n\ttable.SetTablePadding(\" \")\n\ttable.SetNoWhiteSpace(false)\n\n\treturn table\n}\n\nfunc sortMapByIntVal(m map[string]int) (sorted [][]string) {\n\ttype kv struct {\n\t\tKey   string\n\t\tValue int\n\t}\n\n\tss := make([]kv, len(m))\n\tfor k, v := range m {\n\t\tss = append(ss, kv{k, v})\n\t}\n\n\tsort.Slice(ss, func(i, j int) bool {\n\t\treturn ss[i].Value > ss[j].Value\n\t})\n\n\tfor _, kv := range ss {\n\t\tsorted = append(sorted, []string{kv.Key, strconv.Itoa(kv.Value)})\n\t}\n\n\treturn\n}\n\nfunc sortMapByFloatVal(m map[string]float32) (sorted [][]string) {\n\ttype kv struct {\n\t\tKey   string\n\t\tValue float32\n\t}\n\n\tss := make([]kv, len(m))\n\n\tfor k, v := range m {\n\t\tif k == \"\" || v == 0.00 {\n\t\t\tcontinue\n\t\t}\n\n\t\tss = append(ss, kv{k, v})\n\t}\n\n\tsort.Slice(ss, func(i, j int) bool {\n\t\treturn ss[i].Value > ss[j].Value\n\t})\n\n\tfor _, kv := range ss {\n\t\tsorted = append(sorted, []string{kv.Key, fmt.Sprintf(\"%.2f\", kv.Value)})\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "modules/pihole/widget.go",
    "content": "package pihole\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tsettings *Settings\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, _ *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\t\tsettings:   settings,\n\t}\n\n\twidget.settings.RefreshInterval = 30 * time.Second\n\twidget.initializeKeyboardControls()\n\twidget.SetDisplayFunction(widget.Refresh)\n\twidget.View.SetWordWrap(true)\n\twidget.View.SetWrap(settings.wrapText)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\n\tc := getClient()\n\n\tif err := checkServer(c, widget.settings.apiUrl); err != nil {\n\t\treturn title, err.Error(), widget.settings.wrapText\n\t}\n\n\tvar sb strings.Builder\n\n\tif widget.settings.showSummary {\n\t\tsb.WriteString(getSummaryView(c, widget.settings))\n\t}\n\n\tif widget.settings.showTopItems > 0 {\n\t\tsb.WriteString(getTopItemsView(c, widget.settings))\n\t}\n\n\tif widget.settings.showTopClients > 0 {\n\t\tsb.WriteString(getTopClientsView(c, widget.settings))\n\t}\n\n\toutput := sb.String()\n\n\treturn title, output, widget.settings.wrapText\n}\n\nfunc (widget *Widget) disable() {\n\twidget.adblockSwitch(\"disable\")\n}\n\nfunc (widget *Widget) enable() {\n\twidget.adblockSwitch(\"enable\")\n}\n"
  },
  {
    "path": "modules/ping/settings.go",
    "content": "package ping\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Pings\"\n)\n\ntype Host struct {\n\tLabel    string `help:\"Label: The name to use for the host you want to ping. Uses hostname if blank.\"`\n\tHostname string `help:\"Hostname: IP address or hostname to ping\"`\n\tUp       bool   // not meant to be set by user\n}\n\ntype Settings struct {\n\tcommon *cfg.Common\n\thosts  []Host\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tcommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t\thosts:  buildhosts(ymlConfig),\n\t}\n\n\treturn &settings\n}\n\nfunc buildhosts(ymlConfig *config.Config) []Host {\n\n\thosts := []Host{}\n\tyaml := ymlConfig.UList(\"hosts\")\n\n\t// Iterate through each host in the config\n\tfor _, rawHost := range yaml {\n\n\t\thost, ok := rawHost.(map[string]interface{})\n\t\tif !ok {\n\t\t\tcontinue // bad host, skip\n\t\t}\n\n\t\thostname, ok := host[\"hostname\"].(string)\n\t\tif !ok {\n\t\t\tcontinue // hostname is required, skip\n\t\t}\n\n\t\tif hostname == \"\" {\n\t\t\tcontinue // hostname is required, skip\n\t\t}\n\n\t\tlabel := hostname // a default if missing from config\n\t\tif value, ok := host[\"label\"]; ok {\n\t\t\t// Using Sprintf here instead of a string assert. This is to cover the\n\t\t\t// case where someone puts a number as the label instead of a YAML string.\n\t\t\t// Weird case, yes, but wanted to prevent runtime errors.\n\t\t\tlabel = fmt.Sprintf(\"%v\", value)\n\t\t}\n\n\t\thosts = append(hosts, Host{Label: label, Hostname: hostname, Up: false})\n\t}\n\treturn hosts\n}\n"
  },
  {
    "path": "modules/ping/widget.go",
    "content": "package ping\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tprobing \"github.com/prometheus-community/pro-bing\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget is the container for your module's data\ntype Widget struct {\n\tview.TextWidget\n\thosts []Host\n\n\tsettings *Settings\n}\n\n// NewWidget creates and returns an instance of Widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.common),\n\n\t\tsettings: settings,\n\t}\n\twidget.hosts = widget.settings.hosts\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) doPings() {\n\tvar wg sync.WaitGroup\n\tfor i := range widget.hosts {\n\t\tidx := i\n\t\thost := widget.hosts[idx]\n\t\twidget.hosts[idx].Up = false // reset to false each time\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tpinger, err := probing.NewPinger(host.Hostname)\n\t\t\tif err == nil {\n\t\t\t\tpinger.Count = 1\n\t\t\t\tpinger.Timeout = 10 * time.Second\n\t\t\t\terr = pinger.Run() // Blocks until finished.\n\t\t\t\tif err == nil {\n\t\t\t\t\tstats := pinger.Statistics() // get send/receive/duplicate/rtt stats\n\t\t\t\t\tif stats.PacketsRecv > 0 {\n\t\t\t\t\t\twidget.hosts[idx].Up = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\twidget.hosts[idx].Up = false\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tlog.Fatalf(\"error sending ping: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t}()\n\t}\n\twg.Wait()\n}\nfunc (widget *Widget) Refresh() {\n\n\twidget.doPings()\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() string {\n\tnameWidth := 12\n\tfor _, t := range widget.hosts {\n\t\tif len(t.Label) > nameWidth {\n\t\t\tnameWidth = len(t.Label) + 2\n\t\t}\n\t}\n\n\ts := []string{}\n\tfor _, t := range widget.hosts {\n\t\tvar status string\n\t\tif t.Up {\n\t\t\tstatus = \"[green]Up\"\n\t\t} else {\n\t\t\tstatus = \"[red]DOWN\"\n\t\t}\n\t\tstatusLine := fmt.Sprintf(\"[white]%-*s: %s\", nameWidth, t.Label, status)\n\t\ts = append(s, statusLine)\n\t}\n\n\treturn strings.Join(s, \"\\n\")\n}\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(func() (string, string, bool) {\n\t\treturn widget.CommonSettings().Title, widget.content(), false\n\t})\n}\n"
  },
  {
    "path": "modules/pivotal/client.go",
    "content": "package pivotal\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype Resource struct {\n\tResponse interface{}\n\tRaw      string\n}\n\ntype PivotalClient struct {\n\ttoken     string\n\tbaseUrl   string\n\tprojectId string\n\tuser      *User\n}\n\ntype Error struct {\n\tCode  string `json:\"code\"`\n\tKind  string `json:\"kind\"`\n\tError string `json:\"error\"`\n}\n\nfunc NewPivotalClient(token string, projectId string) *PivotalClient {\n\tbaseUrl := \"https://www.pivotaltracker.com/services/v5/\"\n\tif baseUrl == \"\" {\n\t\tbaseUrl = \"https://www.pivotaltracker.com/services/v5/\"\n\t}\n\tpivotal := PivotalClient{\n\t\ttoken:     token,\n\t\tbaseUrl:   baseUrl,\n\t\tprojectId: projectId,\n\t}\n\tpivotal.user, _ = pivotal.getCurrentUser()\n\treturn &pivotal\n}\n\nfunc (pivotal *PivotalClient) apiv5(resource string) (*Resource, error) {\n\ttrn := &http.Transport{}\n\tmeth := \"GET\"\n\tclient := &http.Client{\n\t\tTransport: trn,\n\t}\n\n\tapiToken := pivotal.token\n\tURL := fmt.Sprintf(\"%s%s\", pivotal.baseUrl, resource)\n\n\treq, err := http.NewRequest(meth, URL, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Header.Add(\"X-TrackerToken\", apiToken)\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tdata, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// check if we received a Pivotal Error response\n\tErr := Error{}\n\terr = json.Unmarshal([]byte(string(data)), &Err)\n\tif err == nil && Err.Error != \"\" {\n\t\treturn nil, fmt.Errorf(\"%s\", Err.Error)\n\t}\n\n\treturn &Resource{Response: &resp, Raw: string(data)}, nil\n}\n\nfunc (pivotal *PivotalClient) getCurrentUser() (*User, error) {\n\tresource, err := pivotal.apiv5(\"me\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuser := User{}\n\terr = json.Unmarshal([]byte(resource.Raw), &user)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &user, nil\n}\n\nfunc (pivotal *PivotalClient) searchStories(filter string) (*PivotalTrackerResponse, error) {\n\tfields := \":default,stories(:default,stories(:default,branches,pull_requests))\"\n\tres := fmt.Sprintf(\"projects/%s/search?fields=%s&query=%s\",\n\t\tpivotal.projectId,\n\t\tfields,\n\t\turl.QueryEscape(filter),\n\t)\n\tresource, err := pivotal.apiv5(res)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar response PivotalTrackerResponse\n\n\terr = json.Unmarshal([]byte(resource.Raw), &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &response, nil\n}\n"
  },
  {
    "path": "modules/pivotal/display.go",
    "content": "package pivotal\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\thasPullFailIcon = '💥'\n\thasPullIcon     = \"🌱\"\n)\n\nvar statusMapEmoji = map[string]string{\n\t\"started\":     \"🚧\",\n\t\"unstarted\":   \"  \",\n\t\"finished\":    \"🚀\",\n\t\"delivered\":   \"🚢\",\n\t\"rejected\":    \"❌\",\n\t\"accepted\":    \"✅\",\n\t\"planned\":     \"📅\",\n\t\"unscheduled\": \"❓\",\n}\n\nfunc (widget *Widget) display() {\n\twidget.SetItemCount(widget.CurrentSource().getItemCount())\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tproj := widget.CurrentSource()\n\n\tif proj == nil {\n\t\treturn widget.CommonSettings().Title, \"No sources\", false\n\t}\n\n\tif proj.Err != nil {\n\t\treturn widget.CommonSettings().Title, proj.Err.Error(), true\n\t}\n\n\ttitle := fmt.Sprintf(\n\t\t\"[%s]%s[white] - %d \",\n\t\twidget.settings.Colors.Title,\n\t\tproj.name, proj.getItemCount())\n\n\tstr := \"\"\n\tfor idx, item := range proj.stories {\n\t\trowColor := widget.RowColor(idx)\n\t\tdisplayText := getShowText(&item)\n\n\t\trow := fmt.Sprintf(\n\t\t\t`[%s]|%s%s| %s[%s]`,\n\t\t\twidget.RowColor(idx),\n\t\t\tgetStatusIcon(&item),\n\t\t\tgetPullStatusIcon(&item),\n\t\t\ttview.Escape(displayText),\n\t\t\trowColor,\n\t\t)\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(item.Name))\n\t}\n\n\treturn title, str, false\n}\n\nfunc getStatusIcon(story *Story) string {\n\tstate := story.CurrentState\n\tval, ok := statusMapEmoji[state]\n\tif ok {\n\t\tstate = val\n\t}\n\treturn state\n}\n\nfunc getPullStatusIcon(story *Story) string {\n\t//prs := len(story.PullRequests)\n\tvar prs string\n\tprs = \"  \"\n\tif len(story.PullRequests) > 0 {\n\t\tprs = hasPullIcon\n\t}\n\treturn prs\n}\n\nfunc getShowText(story *Story) string {\n\tif story == nil {\n\t\treturn \"\"\n\t}\n\n\tspace := regexp.MustCompile(`\\s+`)\n\ttitle := space.ReplaceAllString(story.Name, \" \")\n\t//html.UnescapeString(\"[\" + rowColor + \"]\" + title)\n\treturn title\n}\n"
  },
  {
    "path": "modules/pivotal/keyboard.go",
    "content": "package pivotal\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardChar(\"o\", widget.Open, \"Open item in browser\")\n\twidget.SetKeyboardChar(\"p\", widget.OpenPulls, \"Open pull requests in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.Open, \"Open PR in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/pivotal/settings.go",
    "content": "package pivotal\n\nimport (\n\t\"fmt\"\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"os\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Pivotal\"\n)\n\ntype customQuery struct {\n\ttitle   string `help:\"Display title for this query\"`\n\tfilter  string `help:\"Pivotal search query filter\"`\n\tperPage int    `help:\"Number of issues to show\"`\n\tproject string `help:\"Pivotal project id\"`\n}\n\ntype Settings struct {\n\t*cfg.Common\n\n\tfilter        string\n\tprojectId     string\n\tapiToken      string\n\tstatus        string\n\tcustomQueries []customQuery `help:\"Custom queries allow you to filter pull requests and issues however you like. Give the query a title and a filter. Filters can be copied directly from GitHub’s UI.\" optional:\"true\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tfilter:    ymlConfig.UString(\"filter\", ymlConfig.UString(\"filter\")),\n\t\tprojectId: ymlConfig.UString(\"projectId\", ymlConfig.UString(\"projectId\", os.Getenv(\"PIVOTALTRACKER_PROJECT\"))),\n\t\tapiToken:  ymlConfig.UString(\"apiToken\", ymlConfig.UString(\"apiToken\", os.Getenv(\"PIVOTALTRACKER_TOKEN\"))),\n\t\tstatus:    ymlConfig.UString(\"status\"),\n\t}\n\n\tsettings.customQueries = parseCustomQueries(ymlConfig)\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiToken).Load()\n\n\treturn &settings\n}\n\nfunc parseCustomQueries(ymlConfig *config.Config) []customQuery {\n\tvar result []customQuery\n\n\tif customQueries, err := ymlConfig.Map(\"customQueries\"); err == nil {\n\t\tfor _, query := range customQueries {\n\t\t\tc := customQuery{}\n\t\t\tfor key, value := range query.(map[string]interface{}) {\n\t\t\t\tswitch key {\n\t\t\t\tcase \"title\":\n\t\t\t\t\tc.title = value.(string)\n\t\t\t\tcase \"filter\":\n\t\t\t\t\tc.filter = value.(string)\n\t\t\t\tcase \"project\":\n\t\t\t\t\tswitch value := value.(type) {\n\t\t\t\t\tcase bool, float64, int:\n\t\t\t\t\t\tc.project = fmt.Sprint(value)\n\t\t\t\t\tcase string:\n\t\t\t\t\t\tc.project = value\n\t\t\t\t\t}\n\t\t\t\tcase \"perPage\":\n\t\t\t\t\tc.perPage = value.(int)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif c.title != \"\" && c.filter != \"\" {\n\t\t\t\tresult = append(result, c)\n\t\t\t}\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "modules/pivotal/structs.go",
    "content": "package pivotal\n\nimport (\n\t\"time\"\n)\n\ntype User struct {\n\tID int `json:\"id\"`\n}\n\ntype Story struct {\n\tKind       string    `json:\"kind\"`\n\tID         int       `json:\"id\"`\n\tCreatedAt  time.Time `json:\"created_at\"`\n\tUpdatedAt  time.Time `json:\"updated_at\"`\n\tAcceptedAt time.Time `json:\"accepted_at\"`\n\t//    CreatedAt       int64    `json:\"created_at\"`\n\t//    UpdatedAt       int64    `json:\"updated_at\"`\n\t//    AcceptedAt      int64    `json:\"accepted_at\"`\n\tEstimate        int                `json:\"estimate\"`\n\tStoryType       string             `json:\"story_type\"`\n\tStoryPriority   string             `json:\"story_priority\"`\n\tName            string             `json:\"name\"`\n\tDescription     string             `json:\"description\"`\n\tCurrentState    string             `json:\"current_state\"`\n\tRequestedByID   int                `json:\"requested_by_id\"`\n\tURL             string             `json:\"url\"`\n\tProjectID       int                `json:\"project_id\"`\n\tOwnerIDs        []int              `json:\"owner_ids\"`\n\tOwnedByID       int                `json:\"owned_by_id\"`\n\tLabels          []Label            `json:\"labels\"`\n\tTasks           []interface{}      `json:\"tasks\"`\n\tPullRequests    []StoryPullRequest `json:\"pull_requests\"`\n\tCicdEvents      []interface{}      `json:\"cicd_events\"`\n\tBranches        []StoryBranch      `json:\"branches\"`\n\tBlockers        []interface{}      `json:\"blockers\"`\n\tFollowerIDs     []int              `json:\"follower_ids\"`\n\tComments        []StoryComment     `json:\"comments\"`\n\tBlockedStoryIDs []int              `json:\"blocked_story_ids\"`\n\tReviews         []StoryReview      `json:\"reviews\"`\n\tProject         StoryProject       `json:\"project\"`\n}\n\ntype PivotalTrackerResponse struct {\n\tStories *StoryResponse `json:\"stories\"`\n\tEpics   *EpicResponse  `json:\"epics\"`\n\tQuery   string         `json:\"query\"`\n}\n\ntype StoryResponse struct {\n\tStories              []Story `json:\"stories\"`\n\tTotalPoints          int     `json:\"total_points\"`\n\tTotalPointsCompleted int     `json:\"total_points_completed\"`\n\tTotalHits            int     `json:\"total_hits\"`\n\tTotalHitsWithDone    int     `json:\"total_hits_with_done\"`\n}\n\ntype EpicResponse struct {\n\tEpics             []Epic `json:\"epics\"`\n\tTotalHits         int    `json:\"total_hits\"`\n\tTotalHitsWithDone int    `json:\"total_hits_with_done\"`\n}\n\ntype Epic struct {\n\tID        int       `json:\"id\"`\n\tKind      string    `json:\"kind\"`\n\tCreatedAt time.Time `json:\"created_at\"`\n\tUpdatedAt time.Time `json:\"updated_at\"`\n\t//    CreatedAt   int64    `json:\"created_at\"`\n\t//    UpdatedAt   int64    `json:\"updated_at\"`\n\tProjectID int    `json:\"project_id\"`\n\tName      string `json:\"name\"`\n\tURL       string `json:\"url\"`\n\tLabel     Label  `json:\"label\"`\n}\n\ntype Label struct {\n\tID        int       `json:\"id\"`\n\tProjectID int       `json:\"project_id\"`\n\tKind      string    `json:\"kind\"`\n\tName      string    `json:\"name\"`\n\tCreatedAt time.Time `json:\"created_at\"`\n\tUpdatedAt time.Time `json:\"updated_at\"`\n\t//    CreatedAt   int64    `json:\"created_at\"`\n\t//    UpdatedAt   int64    `json:\"updated_at\"`\n}\n\ntype StoryLabel struct {\n\tID        int    `json:\"id\"`\n\tProjectID int    `json:\"project_id\"`\n\tKind      string `json:\"kind\"`\n\tName      string `json:\"name\"`\n\tCreatedAt int64  `json:\"created_at\"`\n\tUpdatedAt int64  `json:\"updated_at\"`\n}\n\ntype StoryPullRequest struct {\n\tID        int       `json:\"id\"`\n\tKind      string    `json:\"kind\"`\n\tStoryID   int       `json:\"story_id\"`\n\tOwner     string    `json:\"owner\"`\n\tRepo      string    `json:\"repo\"`\n\tHostURL   string    `json:\"host_url\"`\n\tStatus    string    `json:\"status\"`\n\tNumber    int       `json:\"number\"`\n\tCreatedAt time.Time `json:\"created_at\"`\n\tUpdatedAt time.Time `json:\"updated_at\"`\n\t//    CreatedAt   int64  `json:\"created_at\"`\n\t//    UpdatedAt   int64  `json:\"updated_at\"`\n}\n\ntype StoryTask struct {\n\tID          int    `json:\"id\"`\n\tKind        string `json:\"kind\"`\n\tDescription string `json:\"description\"`\n\tComplete    bool   `json:\"complete\"`\n\tPosition    int    `json:\"position\"`\n\tCreatedAt   int64  `json:\"created_at\"`\n\tUpdatedAt   int64  `json:\"updated_at\"`\n\tStoryID     int    `json:\"story_id\"`\n}\n\ntype StoryBranch struct {\n\tID            int    `json:\"id,omitempty\"`\n\tName          string `json:\"name,omitempty\"`\n\tCommitHash    string `json:\"commit_hash,omitempty\"`\n\tCommitMessage string `json:\"commit_message,omitempty\"`\n\tAuthorName    string `json:\"author_name,omitempty\"`\n\tAuthorEmail   string `json:\"author_email,omitempty\"`\n\tCreatedAt     string `json:\"created_at,omitempty\"`\n}\n\ntype StoryComment struct {\n\tKind        string        `json:\"kind\"`\n\tID          int64         `json:\"id\"`\n\tText        string        `json:\"text\"`\n\tPersonID    int64         `json:\"person_id\"`\n\tCreatedAt   int64         `json:\"created_at\"`\n\tUpdatedAt   int64         `json:\"updated_at\"`\n\tStoryID     int64         `json:\"story_id\"`\n\tAttachments []interface{} `json:\"attachments\"`\n\tReactions   []interface{} `json:\"reactions\"`\n}\n\ntype StoryReview struct {\n\tID           int    `json:\"id\"`\n\tReviewerID   int    `json:\"reviewer_id\"`\n\tKind         string `json:\"kind\"`\n\tStoryID      int    `json:\"story_id\"`\n\tReviewTypeID int    `json:\"review_type_id\"`\n\tStatus       string `json:\"status\"`\n\tCreatedAt    int    `json:\"created_at\"`\n\tUpdatedAt    int    `json:\"updated_at\"`\n}\n\ntype StoryProject struct {\n\tID                          int    `json:\"id\"`\n\tKind                        string `json:\"kind\"`\n\tName                        string `json:\"name\"`\n\tVersion                     int    `json:\"version\"`\n\tIterationLength             int    `json:\"iteration_length\"`\n\tWeekStartDay                string `json:\"week_start_day\"`\n\tPointScale                  string `json:\"point_scale\"`\n\tPointScaleIsCustom          bool   `json:\"point_scale_is_custom\"`\n\tBugsAndChoresAreEstimatable bool   `json:\"bugs_and_chores_are_estimatable\"`\n\tAutomaticPlanning           bool   `json:\"automatic_planning\"`\n\tEnableTasks                 bool   `json:\"enable_tasks\"`\n\tTimeZone                    struct {\n\t\tKind      string `json:\"kind\"`\n\t\tOlsonName string `json:\"olson_name\"`\n\t\tOffset    string `json:\"offset\"`\n\t} `json:\"time_zone\"`\n\tVelocityAveragedOver         int    `json:\"velocity_averaged_over\"`\n\tNumberOfDoneIterationsToShow int    `json:\"number_of_done_iterations_to_show\"`\n\tHasGoogleDomain              bool   `json:\"has_google_domain\"`\n\tEnableIncomingEmails         bool   `json:\"enable_incoming_emails\"`\n\tInitialVelocity              int    `json:\"initial_velocity\"`\n\tPublic                       bool   `json:\"public\"`\n\tAtomEnabled                  bool   `json:\"atom_enabled\"`\n\tProjectType                  string `json:\"project_type\"`\n\tHasCICDIntegration           bool   `json:\"has_cicd_integration\"`\n\tCapabilities                 struct {\n\t\tPrioritySupport         bool `json:\"priority_support\"`\n\t\tLabelsPanel             bool `json:\"labels_panel\"`\n\t\tLabelsPanelBulkActions  bool `json:\"labels_panel_bulk_actions\"`\n\t\tDigitalRiverIntegration bool `json:\"digital_river_integration\"`\n\t\tDigitalRiverDebug       bool `json:\"digital_river_debug\"`\n\t\tStartSendingDRNotices   bool `json:\"start_sending_dr_notices\"`\n\t\tEnableEAPEvents         bool `json:\"enable_eap_events\"`\n\t} `json:\"capabilities\"`\n\tStartDate                   string `json:\"start_date\"`\n\tStartTime                   int64  `json:\"start_time\"`\n\tShownIterationsStartTime    int64  `json:\"shown_iterations_start_time\"`\n\tCreatedAt                   int64  `json:\"created_at\"`\n\tUpdatedAt                   int64  `json:\"updated_at\"`\n\tShowStoryPriority           bool   `json:\"show_story_priority\"`\n\tShowPriorityIcon            bool   `json:\"show_priority_icon\"`\n\tShowPriorityIconInAllPanels bool   `json:\"show_priority_icon_in_all_panels\"`\n\tEpics                       []struct {\n\t\tID      int    `json:\"id\"`\n\t\tName    string `json:\"name\"`\n\t\tLabelID int    `json:\"label_id\"`\n\t} `json:\"epics\"`\n}\n"
  },
  {
    "path": "modules/pivotal/view.go",
    "content": "package pivotal\n\nimport (\n\t\"fmt\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\ntype PivotalSource struct {\n\tclient    *PivotalClient\n\tname      string\n\tfilter    string\n\twidget    *Widget\n\tErr       error\n\tstories   []Story\n\tmax_items int\n}\n\n// NewPivotalSource returns a new Pivotal Filter source with a name\nfunc NewPivotalSource(name string, filter string, client *PivotalClient, widget *Widget) *PivotalSource {\n\tsource := PivotalSource{\n\t\tname:   name,\n\t\tfilter: filter,\n\t\tclient: client,\n\t\twidget: widget,\n\t}\n\tsource.loadStories()\n\treturn &source\n}\n\nfunc (source *PivotalSource) loadStories() {\n\tsearch, err := source.client.searchStories(source.filter)\n\tif err != nil {\n\t\tsource.stories = nil\n\t\tsource.Err = err\n\t\tsource.setItemCount(0)\n\t} else {\n\t\tsource.stories = search.Stories.Stories\n\t\tsource.Err = err\n\t\tsource.setItemCount(len(source.stories))\n\t}\n}\n\n// Open: Will open Pivotal search url with filter applied using the utils helper\nfunc (source *PivotalSource) Open() {\n\tsel := source.widget.GetSelected()\n\tprojectID := source.client.projectId\n\tif sel >= 0 && sel < source.getItemCount() {\n\t\tstory := &source.stories[sel]\n\t\tbaseURL := \"https://www.pivotaltracker.com/n/projects/\"\n\t\tticketURL := fmt.Sprintf(\"%s%s/stories/%d\", baseURL, projectID, story.ID)\n\t\tutils.OpenFile(ticketURL)\n\t}\n}\n\n// OpenPulls will open the GitHub Pull Requests URL using the utils helper\nfunc (source *PivotalSource) OpenPulls() {\n\tsel := source.widget.GetSelected()\n\tif sel >= 0 && sel < source.getItemCount() {\n\t\tstory := &source.stories[sel]\n\t\tif len(story.PullRequests) > 0 {\n\t\t\tpr := story.PullRequests[0]\n\t\t\tticketURL := fmt.Sprintf(\"%s%s/%s/pull/%d\", pr.HostURL, pr.Owner, pr.Repo, pr.Number)\n\t\t\tutils.OpenFile(ticketURL)\n\t\t}\n\t}\n}\n\n/* -------------------- Counts -------------------- */\n\nfunc (source *PivotalSource) getItemCount() int {\n\tif source.stories == nil {\n\t\treturn 0\n\t}\n\treturn len(source.stories)\n}\nfunc (source *PivotalSource) setItemCount(count int) {\n\tsource.max_items = count\n}\n\n/* -------------------- Unexported Functions -------------------- */\n"
  },
  {
    "path": "modules/pivotal/widget.go",
    "content": "package pivotal\n\nimport (\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// A Widget represents a Todoist widget\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.ScrollableWidget\n\tsettings *Settings\n\n\tclient        *PivotalClient\n\tprojectClient map[string]*PivotalClient\n\tsources       []*PivotalSource\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"customQuery\", \"customQueries\"),\n\t\tScrollableWidget:  view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings:      settings,\n\t\tclient:        NewPivotalClient(settings.apiToken, settings.projectId),\n\t\tprojectClient: make(map[string]*PivotalClient),\n\t}\n\n\twidget.loadSources()\n\n\t// Add the client to projectClient list\n\twidget.projectClient[widget.settings.projectId] = widget.client\n\n\t//Build the Souce lists\n\twidget.sources = widget.buildPivotalSources()\n\n\twidget.SetRenderFunction(widget.display)\n\twidget.initializeKeyboardControls()\n\twidget.SetDisplayFunction(widget.display)\n\n\treturn &widget\n}\n\nfunc (widget *Widget) loadSources() {\n\tvar queries []string\n\tfor _, query := range widget.settings.customQueries {\n\t\tqueries = append(queries, query.title)\n\t}\n\twidget.Sources = queries\n}\n\nfunc (widget *Widget) buildPivotalSources() []*PivotalSource {\n\tvar sources []*PivotalSource\n\n\tfor _, query := range widget.settings.customQueries {\n\t\tclient := widget.client\n\t\t// Make sure that we have a viable Pivotal Client\n\t\tif query.project != \"\" && query.project != widget.client.projectId {\n\t\t\tnclient, ok := widget.projectClient[query.project]\n\t\t\tif !ok {\n\t\t\t\tnclient = NewPivotalClient(widget.settings.apiToken, query.project)\n\t\t\t}\n\t\t\tclient = nclient\n\t\t}\n\n\t\tsources = append(sources,\n\t\t\tNewPivotalSource(query.title, query.filter, client, widget))\n\t}\n\n\treturn sources\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) CurrentSource() *PivotalSource {\n\tif len(widget.sources) == 0 {\n\t\treturn nil\n\t}\n\n\treturn widget.sources[widget.Idx]\n\n}\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\twidget.SetItemCount(widget.CurrentSource().getItemCount())\n\twidget.display()\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Open() {\n\twidget.CurrentSource().Open()\n}\nfunc (widget *Widget) OpenPulls() {\n\twidget.CurrentSource().OpenPulls()\n}\n"
  },
  {
    "path": "modules/pocket/client.go",
    "content": "package pocket\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\n// Client pocket client Documention at https://getpocket.com/developer/docs/overview\ntype Client struct {\n\tconsumerKey string\n\taccessToken *string\n\tbaseURL     string\n\tredirectURL string\n}\n\n// NewClient returns a new PocketClient\nfunc NewClient(consumerKey, redirectURL string) *Client {\n\treturn &Client{\n\t\tconsumerKey: consumerKey,\n\t\tredirectURL: redirectURL,\n\t\tbaseURL:     \"https://getpocket.com/v3\",\n\t}\n\n}\n\n// Item represents link in pocket api\ntype Item struct {\n\tItemID                 string `json:\"item_id\"`\n\tResolvedID             string `json:\"resolved_id\"`\n\tGivenURL               string `json:\"given_url\"`\n\tGivenTitle             string `json:\"given_title\"`\n\tFavorite               string `json:\"favorite\"`\n\tStatus                 string `json:\"status\"`\n\tTimeAdded              string `json:\"time_added\"`\n\tTimeUpdated            string `json:\"time_updated\"`\n\tTimeRead               string `json:\"time_read\"`\n\tTimeFavorited          string `json:\"time_favorited\"`\n\tSortID                 int    `json:\"sort_id\"`\n\tResolvedTitle          string `json:\"resolved_title\"`\n\tResolvedURL            string `json:\"resolved_url\"`\n\tExcerpt                string `json:\"excerpt\"`\n\tIsArticle              string `json:\"is_article\"`\n\tIsIndex                string `json:\"is_index\"`\n\tHasVideo               string `json:\"has_video\"`\n\tHasImage               string `json:\"has_image\"`\n\tWordCount              string `json:\"word_count\"`\n\tLang                   string `json:\"lang\"`\n\tTimeToRead             int    `json:\"time_to_read\"`\n\tTopImageURL            string `json:\"top_image_url\"`\n\tListenDurationEstimate int    `json:\"listen_duration_estimate\"`\n}\n\n// ItemLists represent list of links\ntype ItemLists struct {\n\tStatus   int             `json:\"status\"`\n\tComplete int             `json:\"complete\"`\n\tList     map[string]Item `json:\"list\"`\n\tSince    int             `json:\"since\"`\n}\n\ntype request struct {\n\trequestBody interface{}\n\tmethod      string\n\theaders     map[string]string\n\turl         string\n}\n\nfunc (*Client) request(req request, result interface{}) error {\n\tvar reqBody io.Reader\n\tif req.requestBody != nil {\n\t\tjsonValues, err := json.Marshal(req.requestBody)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treqBody = bytes.NewBuffer(jsonValues)\n\t}\n\n\trequest, err := http.NewRequest(req.method, req.url, reqBody)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor key, value := range req.headers {\n\t\trequest.Header.Add(key, value)\n\t}\n\trequest.Header.Set(\"User-Agent\", \"wtfutil (https://github.com/wtfutil/wtf)\")\n\n\tresp, err := http.DefaultClient.Do(request)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tresponseBody, err := io.ReadAll(resp.Body)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.StatusCode >= 400 {\n\t\treturn fmt.Errorf(`server responded with [%d]:%s,url:%s`, resp.StatusCode, responseBody, req.url)\n\t}\n\n\tif err := json.Unmarshal(responseBody, &result); err != nil {\n\t\treturn fmt.Errorf(\"could not unmarshal url [%s] \\n\\t\\tresponse [%s] error:%w\",\n\t\t\treq.url, responseBody, err)\n\t}\n\n\treturn nil\n\n}\n\ntype obtainRequestTokenRequest struct {\n\tConsumerKey string `json:\"consumer_key\"`\n\tRedirectURI string `json:\"redirect_uri\"`\n}\n\n// ObtainRequestToken get request token to be used in the auth workflow\nfunc (client *Client) ObtainRequestToken() (code string, err error) {\n\turl := fmt.Sprintf(\"%s/oauth/request\", client.baseURL)\n\trequestData := obtainRequestTokenRequest{ConsumerKey: client.consumerKey, RedirectURI: client.redirectURL}\n\n\tvar responseData map[string]string\n\treq := request{\n\t\theaders: map[string]string{\n\t\t\t\"X-Accept\":     \"application/json\",\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t},\n\t\tmethod:      \"POST\",\n\t\trequestBody: requestData,\n\t\turl:         url,\n\t}\n\terr = client.request(req, &responseData)\n\n\tif err != nil {\n\t\treturn code, err\n\t}\n\n\treturn responseData[\"code\"], nil\n\n}\n\n// CreateAuthLink create authorization link to redirect the user to\nfunc (client *Client) CreateAuthLink(requestToken string) string {\n\treturn fmt.Sprintf(\"https://getpocket.com/auth/authorize?request_token=%s&redirect_uri=%s\", requestToken, client.redirectURL)\n}\n\ntype accessTokenRequest struct {\n\tConsumerKey string `json:\"consumer_key\"`\n\tRequestCode string `json:\"code\"`\n}\n\n// accessTokenResponse represents\ntype accessTokenResponse struct {\n\tAccessToken string `json:\"access_token\"`\n}\n\n// GetAccessToken exchange request token for accesstoken\nfunc (client *Client) GetAccessToken(requestToken string) (accessToken string, err error) {\n\turl := fmt.Sprintf(\"%s/oauth/authorize\", client.baseURL)\n\trequestData := accessTokenRequest{\n\t\tConsumerKey: client.consumerKey,\n\t\tRequestCode: requestToken,\n\t}\n\treq := request{\n\t\tmethod:      \"POST\",\n\t\turl:         url,\n\t\trequestBody: requestData,\n\t}\n\treq.headers = map[string]string{\n\t\t\"X-Accept\":     \"application/json\",\n\t\t\"Content-Type\": \"application/json\",\n\t}\n\n\tvar response accessTokenResponse\n\terr = client.request(req, &response)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn response.AccessToken, nil\n\n}\n\n/*\nLinkState  represents link states to be retrieved\nAccording to the api https://getpocket.com/developer/docs/v3/retrieve\nthere are 3 states:\n\n\t1-archive\n\t2-unread\n\t3-all\n\nhowever archive does not really well work and returns links that are in the\nunread list\nbuy inspecting getpocket I found out that there is an undocumanted read state\n*/\ntype LinkState string\n\nconst (\n\t// Read links that has been read (undocumanted)\n\tRead LinkState = \"read\"\n\t// Unread links has not been read\n\tUnread LinkState = \"unread\"\n)\n\n// GetLinks retrieve links of a given states https://getpocket.com/developer/docs/v3/retrieve\nfunc (client *Client) GetLinks(state LinkState) (response ItemLists, err error) {\n\turl := fmt.Sprintf(\"%s/get?sort=newest&state=%s&consumer_key=%s&access_token=%s\", client.baseURL, state, client.consumerKey, *client.accessToken)\n\treq := request{\n\t\tmethod: \"GET\",\n\t\turl:    url,\n\t}\n\treq.headers = map[string]string{\n\t\t\"X-Accept\":     \"application/json\",\n\t\t\"Content-Type\": \"application/json\",\n\t}\n\n\terr = client.request(req, &response)\n\treturn response, err\n}\n\n// Action represents a mutation to link\ntype Action string\n\nconst (\n\t// Archive to put the link in the archived list (read list)\n\tArchive Action = \"archive\"\n\t// ReAdd to put the link back in the to reed list\n\tReAdd Action = \"readd\"\n)\n\ntype actionParams struct {\n\tAction Action `json:\"action\"`\n\tItemID string `json:\"item_id\"`\n}\n\n// ModifyLink change the state of the link\nfunc (client *Client) ModifyLink(action Action, itemID string) (ok bool, err error) {\n\n\tactions := []actionParams{\n\t\t{\n\t\t\tAction: action,\n\t\t\tItemID: itemID,\n\t\t},\n\t}\n\n\turlActionParm, err := json.Marshal(actions)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\turl := fmt.Sprintf(\"%s/send?consumer_key=%s&access_token=%s&actions=%s\", client.baseURL, client.consumerKey, *client.accessToken, urlActionParm)\n\n\treq := request{\n\t\tmethod: \"GET\",\n\t\turl:    url,\n\t}\n\n\terr = client.request(req, nil)\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n\n}\n"
  },
  {
    "path": "modules/pocket/item_service.go",
    "content": "package pocket\n\nimport \"sort\"\n\ntype sortByTimeAdded []Item\n\nfunc (a sortByTimeAdded) Len() int           { return len(a) }\nfunc (a sortByTimeAdded) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }\nfunc (a sortByTimeAdded) Less(i, j int) bool { return a[i].TimeAdded > a[j].TimeAdded }\n\nfunc orderItemResponseByKey(response ItemLists) []Item {\n\n\tvar items sortByTimeAdded\n\tfor _, v := range response.List {\n\t\titems = append(items, v)\n\t}\n\tsort.Sort(items)\n\treturn items\n}\n"
  },
  {
    "path": "modules/pocket/keyboard.go",
    "content": "package pocket\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"a\", widget.toggleLink, \"Toggle Link\")\n\twidget.SetKeyboardChar(\"t\", widget.toggleView, \"Toggle view (links ,archived links)\")\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select Next Link\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select Previous Link\")\n\twidget.SetKeyboardChar(\"o\", widget.openLink, \"Open Link in the browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select Next Link\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select Previous Link\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openLink, \"Open Link in the browser\")\n}\n"
  },
  {
    "path": "modules/pocket/settings.go",
    "content": "package pocket\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Pocket\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tconsumerKey string\n\trequestKey  *string\n\taccessToken *string\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon:      cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t\tconsumerKey: ymlConfig.UString(\"consumerKey\"),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.consumerKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/pocket/widget.go",
    "content": "package pocket\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/logger\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\t\"gopkg.in/yaml.v2\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tsettings     *Settings\n\tclient       *Client\n\titems        []Item\n\tarchivedView bool\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, _ *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, nil, settings.Common),\n\t\tsettings:         settings,\n\t\tclient:           NewClient(settings.consumerKey, \"http://localhost\"),\n\t\tarchivedView:     false,\n\t}\n\n\twidget.CommonSettings()\n\twidget.SetRenderFunction(widget.Render)\n\twidget.View.SetScrollable(true)\n\twidget.View.SetRegions(true)\n\twidget.initializeKeyboardControls()\n\twidget.Selected = -1\n\twidget.SetItemCount(0)\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Render() {\n\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) Refresh() {\n\tif widget.client.accessToken == nil {\n\t\tmetaData, err := readMetaDataFromDisk()\n\t\tif err != nil || metaData.AccessToken == nil {\n\t\t\twidget.Redraw(widget.authorizeWorkFlow)\n\t\t\treturn\n\t\t}\n\t\twidget.client.accessToken = metaData.AccessToken\n\t}\n\n\tstate := Unread\n\tif widget.archivedView {\n\t\tstate = Read\n\t}\n\tresponse, err := widget.client.GetLinks(state)\n\tif err != nil {\n\t\twidget.SetItemCount(0)\n\t}\n\n\twidget.items = orderItemResponseByKey(response)\n\twidget.SetItemCount(len(widget.items))\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\ntype pocketMetaData struct {\n\tAccessToken *string\n}\n\nfunc writeMetaDataToDisk(metaData pocketMetaData) error {\n\n\tfileData, err := yaml.Marshal(metaData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not write token to disk %w\", err)\n\t}\n\n\twtfConfigDir, err := cfg.WtfConfigDir()\n\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tfilePath := fmt.Sprintf(\"%s/%s\", wtfConfigDir, \"pocket.data\")\n\terr = os.WriteFile(filePath, fileData, 0644)\n\n\treturn err\n}\n\nfunc readMetaDataFromDisk() (pocketMetaData, error) {\n\twtfConfigDir, err := cfg.WtfConfigDir()\n\tvar metaData pocketMetaData\n\tif err != nil {\n\t\treturn metaData, err\n\t}\n\tfilePath := fmt.Sprintf(\"%s/%s\", wtfConfigDir, \"pocket.data\")\n\tfileData, err := utils.ReadFileBytes(filePath)\n\n\tif err != nil {\n\t\treturn metaData, err\n\t}\n\n\terr = yaml.Unmarshal(fileData, &metaData)\n\n\treturn metaData, err\n\n}\n\n/*\nAuthorization workflow is documented at https://getpocket.com/developer/docs/authentication\nbroken to 4 steps :\n\n\t1- Obtain a platform consumer key from http://getpocket.com/developer/apps/new.\n\t2- Obtain a request token\n\t3- Redirect user to Pocket to continue authorization\n\t4- Receive the callback from Pocket, this wont be used\n\t5- Convert a request token into a Pocket access token\n*/\nfunc (widget *Widget) authorizeWorkFlow() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\n\tif widget.settings.requestKey == nil {\n\t\trequestToken, err := widget.client.ObtainRequestToken()\n\n\t\tif err != nil {\n\t\t\tlogger.Log(err.Error())\n\t\t\treturn title, err.Error(), true\n\t\t}\n\t\twidget.settings.requestKey = &requestToken\n\t\tredirectURL := widget.client.CreateAuthLink(requestToken)\n\t\tcontent := fmt.Sprintf(\"Please click on %s to Authorize the app\", redirectURL)\n\t\treturn title, content, true\n\t}\n\n\tif widget.settings.accessToken == nil {\n\t\taccessToken, err := widget.client.GetAccessToken(*widget.settings.requestKey)\n\t\tif err != nil {\n\t\t\tlogger.Log(err.Error())\n\t\t\tredirectURL := widget.client.CreateAuthLink(*widget.settings.requestKey)\n\t\t\tcontent := fmt.Sprintf(\"Please click on %s to Authorize the app\", redirectURL)\n\t\t\treturn title, content, true\n\t\t}\n\t\tcontent := \"Authorized\"\n\t\twidget.settings.accessToken = &accessToken\n\n\t\tmetaData := pocketMetaData{\n\t\t\tAccessToken: &accessToken,\n\t\t}\n\n\t\terr = writeMetaDataToDisk(metaData)\n\t\tif err != nil {\n\t\t\tcontent = err.Error()\n\t\t}\n\n\t\treturn title, content, true\n\t}\n\n\tcontent := \"Authorized\"\n\treturn title, content, true\n\n}\n\nfunc (widget *Widget) toggleView() {\n\twidget.archivedView = !widget.archivedView\n\twidget.Refresh()\n}\n\nfunc (widget *Widget) openLink() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.items != nil && sel < len(widget.items) {\n\t\titem := &widget.items[sel]\n\t\tutils.OpenFile(item.GivenURL)\n\t}\n}\n\nfunc (widget *Widget) toggleLink() {\n\tsel := widget.GetSelected()\n\taction := Archive\n\tif widget.archivedView {\n\t\taction = ReAdd\n\t}\n\n\tif sel >= 0 && widget.items != nil && sel < len(widget.items) {\n\t\titem := &widget.items[sel]\n\t\t_, err := widget.client.ModifyLink(action, item.ItemID)\n\t\tif err != nil {\n\t\t\tlogger.Log(err.Error())\n\t\t}\n\t}\n\n\twidget.Refresh()\n}\n\nfunc (widget *Widget) formatItem(item Item, isSelected bool) string {\n\tforeColor, backColor := widget.settings.Colors.EvenForeground, widget.settings.Colors.EvenBackground\n\ttext := item.ResolvedTitle\n\tif isSelected {\n\t\tforeColor = widget.settings.Colors.HighlightedForeground\n\t\tbackColor = widget.settings.Colors.HighlightedBackground\n\n\t}\n\n\treturn fmt.Sprintf(\"[%s:%s]%s[white]\", foreColor, backColor, tview.Escape(text))\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\tcurrentViewTitle := \"Reading List\"\n\tif widget.archivedView {\n\t\tcurrentViewTitle = \"Archived list\"\n\t}\n\n\ttitle = fmt.Sprintf(\"%s-%s\", title, currentViewTitle)\n\tcontent := \"\"\n\n\tfor i, v := range widget.items {\n\t\tcontent += widget.formatItem(v, i == widget.Selected) + \"\\n\"\n\t}\n\n\treturn title, content, false\n}\n"
  },
  {
    "path": "modules/power/battery.go",
    "content": "//go:build !linux && !freebsd\n\npackage power\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\ttimeRegExp = \"^(?:\\\\d|[01]\\\\d|2[0-3]):[0-5]\\\\d\"\n)\n\ntype Battery struct {\n\targs   []string\n\tcmd    string\n\tresult string\n\n\tCharge    string\n\tRemaining string\n}\n\nfunc NewBattery() *Battery {\n\tbattery := &Battery{\n\t\targs: []string{\"-g\", \"batt\"},\n\t\tcmd:  \"pmset\",\n\t}\n\n\treturn battery\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (battery *Battery) Refresh() {\n\tdata := battery.execute()\n\tbattery.result = battery.parse(data)\n}\n\nfunc (battery *Battery) String() string {\n\treturn battery.result\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (battery *Battery) execute() string {\n\tcmd := exec.Command(battery.cmd, battery.args...)\n\treturn utils.ExecuteCommand(cmd)\n}\n\nfunc (battery *Battery) parse(data string) string {\n\tlines := strings.Split(data, \"\\n\")\n\tif len(lines) < 2 {\n\t\treturn msgNoBattery\n\t}\n\n\tstats := strings.Split(lines[1], \"\\t\")\n\tif len(stats) < 2 {\n\t\treturn msgNoBattery\n\t}\n\n\tdetails := strings.Split(stats[1], \"; \")\n\tif len(details) < 3 {\n\t\treturn msgNoBattery\n\t}\n\n\tstr := \"\"\n\tstr = str + fmt.Sprintf(\" %14s: %s\\n\", \"Charge\", battery.formatCharge(details[0]))\n\tstr = str + fmt.Sprintf(\" %14s: %s\\n\", \"Remaining\", battery.formatRemaining(details[2]))\n\tstr = str + fmt.Sprintf(\" %14s: %s\\n\", \"State\", battery.formatState(details[1]))\n\n\treturn str\n}\n\nfunc (battery *Battery) formatCharge(data string) string {\n\tpercent, _ := strconv.ParseFloat(strings.Replace(data, \"%\", \"\", -1), 32)\n\treturn utils.ColorizePercent(percent)\n}\n\nfunc (battery *Battery) formatRemaining(data string) string {\n\tr, _ := regexp.Compile(timeRegExp)\n\n\tresult := r.FindString(data)\n\tif result == \"\" || result == \"0:00\" {\n\t\tresult = \"-\"\n\t}\n\n\treturn result\n}\n\nfunc (battery *Battery) formatState(data string) string {\n\tcolor := \"\"\n\n\tswitch data {\n\tcase \"charging\":\n\t\tcolor = \"[green]\"\n\tcase \"discharging\":\n\t\tcolor = \"[yellow]\"\n\tdefault:\n\t\tcolor = \"[white]\"\n\t}\n\n\treturn color + data + \"[white]\"\n}\n"
  },
  {
    "path": "modules/power/battery_freebsd.go",
    "content": "//go:build freebsd\n\npackage power\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nvar batteryState string\n\ntype Battery struct {\n\targs   []string\n\tcmd    string\n\tresult string\n\n\tCharge    string\n\tRemaining string\n}\n\nfunc NewBattery() *Battery {\n\treturn &Battery{}\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (battery *Battery) Refresh() {\n\tdata := battery.execute()\n\tbattery.result = battery.parse(data)\n}\n\nfunc (battery *Battery) String() string {\n\treturn battery.result\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// returns 3 numbers\n//\n//\t1/0   = AC/battery\n//\tc     = battery charge percentage\n//\t-1/s  = charging / seconds to empty\nfunc (battery *Battery) execute() string {\n\tcmd := exec.Command(\"apm\", \"-alt\")\n\treturn utils.ExecuteCommand(cmd)\n}\n\nfunc (battery *Battery) parse(data string) string {\n\tlines := strings.Split(data, \"\\n\")\n\tif len(lines) < 3 {\n\t\treturn \"unknown\"\n\t}\n\tbatteryState = strings.TrimSpace(lines[0])\n\tcharge := strings.TrimSpace(lines[1])\n\ttimeToEmpty := \"∞\"\n\tseconds, err := strconv.Atoi(strings.TrimSpace(lines[2]))\n\tif err == nil && seconds >= 0 {\n\t\th := seconds / 3600\n\t\tm := seconds % 3600 / 60\n\t\ts := seconds % 60\n\t\ttimeToEmpty = fmt.Sprintf(\"%2d:%02d:%02d\", h, m, s)\n\t}\n\n\tstr := fmt.Sprintf(\" %14s: %s%%\\n\", \"Charge\", battery.formatCharge(charge))\n\tstr += fmt.Sprintf(\" %14s: %s\\n\", \"Remaining\", timeToEmpty)\n\tstr += fmt.Sprintf(\" %14s: %s\\n\", \"State\", battery.formatState(batteryState))\n\n\treturn str\n}\n\nfunc (battery *Battery) formatCharge(data string) string {\n\tpercent, _ := strconv.ParseFloat(strings.Replace(data, \"%\", \"\", -1), 32)\n\treturn utils.ColorizePercent(percent)\n}\n\nfunc (battery *Battery) formatState(data string) string {\n\tcolor := \"\"\n\n\tswitch data {\n\tcase \"1\":\n\t\tcolor = \"[green]charging\"\n\tcase \"0\":\n\t\tcolor = \"[yellow]discharging\"\n\tdefault:\n\t\tcolor = \"[white]unknown\"\n\t}\n\n\treturn color + \"[white]\"\n}\n"
  },
  {
    "path": "modules/power/battery_linux.go",
    "content": "//go:build linux\n\npackage power\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nvar batteryState string\n\ntype Battery struct {\n\tresult string\n\n\tCharge    string\n\tRemaining string\n}\n\nfunc NewBattery() *Battery {\n\treturn &Battery{}\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (battery *Battery) Refresh() {\n\tdata := battery.execute()\n\tbattery.result = battery.parse(data)\n}\n\nfunc (battery *Battery) String() string {\n\treturn battery.result\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (battery *Battery) execute() string {\n\tcmd := exec.Command(\"upower\", \"-e\")\n\tlines := strings.Split(utils.ExecuteCommand(cmd), \"\\n\")\n\tvar target string\n\tfor _, l := range lines {\n\t\tif strings.Contains(l, \"/battery\") {\n\t\t\ttarget = l\n\t\t\tbreak\n\t\t}\n\t}\n\tcmd = exec.Command(\"upower\", \"-i\", target)\n\treturn utils.ExecuteCommand(cmd)\n}\n\nfunc (battery *Battery) parse(data string) string {\n\tlines := strings.Split(data, \"\\n\")\n\tif len(lines) < 2 {\n\t\treturn \"unknown\"\n\t}\n\ttable := make(map[string]string)\n\tfor _, line := range lines {\n\t\tparts := strings.Split(line, \":\")\n\t\tif len(parts) < 2 {\n\t\t\tcontinue\n\t\t}\n\t\ttable[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])\n\t}\n\tif s := table[\"time to empty\"]; s == \"\" {\n\t\ttable[\"time to empty\"] = \"∞\"\n\t}\n\tstr := fmt.Sprintf(\" %14s: %s\\n\", \"Charge\", battery.formatCharge(table[\"percentage\"]))\n\tstr += fmt.Sprintf(\" %14s: %s\\n\", \"Remaining\", table[\"time to empty\"])\n\tstr += fmt.Sprintf(\" %14s: %s\\n\", \"State\", battery.formatState(table[\"state\"]))\n\tif s := table[\"time to full\"]; s != \"\" {\n\t\tstr += fmt.Sprintf(\" %10s: %s\\n\", \"TimeToFull\", table[\"time to full\"])\n\t}\n\tbatteryState = table[\"state\"]\n\treturn str\n}\n\nfunc (battery *Battery) formatCharge(data string) string {\n\tpercent, _ := strconv.ParseFloat(strings.ReplaceAll(data, \"%\", \"\"), 32)\n\treturn utils.ColorizePercent(percent)\n}\n\nfunc (battery *Battery) formatState(data string) string {\n\tcolor := \"\"\n\n\tswitch data {\n\tcase \"charging\":\n\t\tcolor = \"[green]\"\n\tcase \"discharging\":\n\t\tcolor = \"[yellow]\"\n\tdefault:\n\t\tcolor = \"[white]\"\n\t}\n\n\treturn color + data + \"[white]\"\n}\n"
  },
  {
    "path": "modules/power/managed_device_test.go",
    "content": "package power\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"gotest.tools/assert\"\n)\n\nfunc Test_Refresh(t *testing.T) {\n\t// ioreg -c AppleDeviceManagementHIDEventService -r -l\n\tdata := `\n+-o AppleDeviceManagementHIDEventService  <class AppleDeviceManagementHIDEventService, id 0x100000892, registered, matched, active, busy 0 (0 ms), retain 8>\n    {\n      \"LowBatteryNotificationPercentage\" = 2\n      \"PrimaryUsagePage\" = 65333\n      \"BatteryFaultNotificationType\" = \"TPBatteryFault\"\n      \"HasBattery\" = Yes\n      \"VendorID\" = 76\n      \"VersionNumber\" = 0\n      \"Built-In\" = No\n      \"DeviceAddress\" = \"3c-9b\"\n      \"WakeReason\" = \"Button (0x03)\"\n      \"Product\" = \"Magic Trackpad 2\"\n      \"SerialNumber\" = \"3c-9b\"\n      \"Transport\" = \"Bluetooth\"\n      \"BatteryLowNotificationType\" = \"TPLowBattery\"\n      \"ProductID\" = 613\n      \"DeviceUsagePairs\" = ({\"DeviceUsagePage\"=65333,\"DeviceUsage\"=11},{\"DeviceUsagePage\"=65333,\"DeviceUsage\"=20})\n      \"IOPersonalityPublisher\" = \"com.apple.driver.AppleTopCaseHIDEventDriver\"\n      \"BatteryPercent\" = 81\n      \"MTFW Version\" = 944\n      \"BD_ADDR\" = <3ca6f6cccc9b>\n      \"BatteryStatusNotificationType\" = \"BatteryStatusChanged\"\n      \"CriticallyLowBatteryNotificationPercentage\" = 1\n      \"ReportInterval\" = 11250\n      \"RadioFW Version\" = 272\n      \"VendorIDSource\" = 1\n      \"STFW Version\" = 2144\n      \"CFBundleIdentifier\" = \"com.apple.driver.AppleTopCaseHIDEventDriver\"\n      \"IOProviderClass\" = \"IOHIDInterface\"\n      \"LocationID\" = 1642556667\n      \"BluetoothDevice\" = Yes\n      \"IOClass\" = \"AppleDeviceManagementHIDEventService\"\n      \"HIDServiceSupport\" = No\n      \"CFBundleIdentifierKernel\" = \"com.apple.driver.AppleTopCaseHIDEventDriver\"\n      \"ProductIDArray\" = (613)\n      \"BatteryStatusFlags\" = 0\n      \"ColorID\" = 33\n      \"IOMatchCategory\" = \"IODefaultMatchCategory\"\n      \"CountryCode\" = 0\n      \"IOProbeScore\" = 7175\n      \"PrimaryUsage\" = 11\n      \"IOGeneralInterest\" = \"IOCommand is not serializable\"\n      \"BTFW Version\" = 272\n    }\n    \n\n+-o AppleDeviceManagementHIDEventService  <class AppleDeviceManagementHIDEventService, id 0x10000091b, registered, matched, active, busy 0 (0 ms), retain 8>\n    {\n\t\t\"LowBatteryNotificationPercentage\" = 2\n\t\t\"PrimaryUsagePage\" = 65666\n\t\t\"BatteryFaultNotificationType\" = \"KBBatteryFault\"\n\t\t\"HasBattery\" = Yes\n\t\t\"VendorID\" = 76\n\t\t\"TrustedAccessoryFW Version\" = 5666\n\t\t\"Built-In\" = No\n\t\t\"DeviceAddress\" = \"ac-c5\"\n\t\t\"VersionNumber\" = 0\n\t\t\"WakeReason\" = \"Host (0x01)\"\n\t\t\"Product\" = \"Magic Keyboard with Touch ID\"\n\t\t\"SerialNumber\" = \"ac-c5\"\n\t\t\"Transport\" = \"Bluetooth\"\n\t\t\"BatteryLowNotificationType\" = \"KB2LowBattery\"\n\t\t\"ProductID\" = 666\n\t\t\"DeviceUsagePairs\" = ({\"DeviceUsagePage\"=65666,\"DeviceUsage\"=11},{\"DeviceUsagePage\"=65666,\"DeviceUsage\"=20})\n\t\t\"IOPersonalityPublisher\" = \"com.apple.driver.AppleTopCaseDriverV2\"\n\t\t\"BatteryPercent\" = 93\n\t\t\"BD_ADDR\" = <ac49dbbbbbc5>\n\t\t\"BatteryStatusNotificationType\" = \"BatteryStatusChanged\"\n\t\t\"CriticallyLowBatteryNotificationPercentage\" = 1\n\t\t\"ReportInterval\" = 11250\n\t\t\"RadioFW Version\" = 328\n\t\t\"VendorIDSource\" = 1\n\t\t\"STFW Version\" = 1024\n\t\t\"CFBundleIdentifier\" = \"com.apple.driver.AppleTopCaseHIDEventDriver\"\n\t\t\"IOProviderClass\" = \"IOHIDInterface\"\n\t\t\"LocationID\" = 1642556667\n\t\t\"BluetoothDevice\" = Yes\n\t\t\"IOClass\" = \"AppleDeviceManagementHIDEventService\"\n\t\t\"HIDServiceSupport\" = No\n\t\t\"CFBundleIdentifierKernel\" = \"com.apple.driver.AppleTopCaseHIDEventDriver\"\n\t\t\"ProductIDArray\" = (666)\n\t\t\"BatteryStatusFlags\" = 0\n\t\t\"ColorID\" = 32\n\t\t\"IOMatchCategory\" = \"IODefaultMatchCategory\"\n\t\t\"CountryCode\" = 2\n\t\t\"IOProbeScore\" = 7175\n\t\t\"PrimaryUsage\" = 11\n\t\t\"IOGeneralInterest\" = \"IOCommand is not serializable\"\n\t\t\"BTFW Version\" = 328\n    }\n`\n\n\tmanDevices := NewManagedDevices()\n\tmanDevices.Devices = manDevices.parse(data)\n\n\tassert.Equal(t, 2, len(manDevices.Devices))\n\n\tfirst := manDevices.Devices[0]\n\tassert.Equal(t, \"Magic Trackpad 2\", first.Product())\n\tassert.Equal(t, int64(81), first.BatteryPercent())\n\tassert.Equal(t, true, first.BluetoothDevice())\n\tassert.Equal(t, true, first.HasBattery())\n}\n\nfunc Test_Add(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tsrc      string\n\t\texpected map[string]string\n\t}{\n\t\t{\n\t\t\tname:     \"with empty string\",\n\t\t\tsrc:      \"\",\n\t\t\texpected: map[string]string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"with no delimiter match\",\n\t\t\tsrc:      \"catsdogs\",\n\t\t\texpected: map[string]string{},\n\t\t},\n\t\t{\n\t\t\tname: \"with valid src\",\n\t\t\tsrc:  \"cats=dogs\",\n\t\t\texpected: map[string]string{\n\t\t\t\t\"cats\": \"dogs\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with valid multiline src\",\n\t\t\tsrc:  \"cats=dogs\\nx=y\",\n\t\t\texpected: map[string]string{\n\t\t\t\t\"cats\": \"dogs\",\n\t\t\t\t\"x\":    \"y\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmanDev := NewManagedDevice()\n\t\t\tmanDev.Add(tt.src)\n\n\t\t\tif !reflect.DeepEqual(tt.expected, manDev.Attributes) {\n\t\t\t\tt.Errorf(\"\\nexpected %v\\n     got %v\", tt.expected, manDev.Attributes)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Attributes(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata string\n\t}{\n\t\t{\n\t\t\tname: \"with valid attributes\",\n\t\t\tdata: `\n\t\t\t\t\"LowBatteryNotificationPercentage\" = 2\n\t\t\t\t\"PrimaryUsagePage\" = 65666\n\t\t\t\t\"BatteryFaultNotificationType\" = \"KBBatteryFault\"\n\t\t\t\t\"HasBattery\" = Yes\n\t\t\t\t\"VendorID\" = 76\n\t\t\t\t\"TrustedAccessoryFW Version\" = 5666\n\t\t\t\t\"Built-In\" = No\n\t\t\t\t\"DeviceAddress\" = \"ac-c5\"\n\t\t\t\t\"VersionNumber\" = 0\n\t\t\t\t\"WakeReason\" = \"Host (0x01)\"\n\t\t\t\t\"Product\" = \"Magic Keyboard with Touch ID\"\n\t\t\t\t\"SerialNumber\" = \"ac-c5\"\n\t\t\t\t\"Transport\" = \"Bluetooth\"\n\t\t\t\t\"BatteryLowNotificationType\" = \"KB2LowBattery\"\n\t\t\t\t\"ProductID\" = 666\n\t\t\t\t\"DeviceUsagePairs\" = ({\"DeviceUsagePage\"=65666,\"DeviceUsage\"=11},{\"DeviceUsagePage\"=65666,\"DeviceUsage\"=20})\n\t\t\t\t\"IOPersonalityPublisher\" = \"com.apple.driver.AppleTopCaseDriverV2\"\n\t\t\t\t\"BatteryPercent\" = 93\n\t\t\t\t\"BD_ADDR\" = <ac49dbbbbbc5>\n\t\t\t\t\"BatteryStatusNotificationType\" = \"BatteryStatusChanged\"\n\t\t\t\t\"CriticallyLowBatteryNotificationPercentage\" = 1\n\t\t\t\t\"ReportInterval\" = 11250\n\t\t\t\t\"RadioFW Version\" = 328\n\t\t\t\t\"VendorIDSource\" = 1\n\t\t\t\t\"STFW Version\" = 1024\n\t\t\t\t\"CFBundleIdentifier\" = \"com.apple.driver.AppleTopCaseHIDEventDriver\"\n\t\t\t\t\"IOProviderClass\" = \"IOHIDInterface\"\n\t\t\t\t\"LocationID\" = 1642556667\n\t\t\t\t\"BluetoothDevice\" = Yes\n\t\t\t\t\"IOClass\" = \"AppleDeviceManagementHIDEventService\"\n\t\t\t\t\"HIDServiceSupport\" = No\n\t\t\t\t\"CFBundleIdentifierKernel\" = \"com.apple.driver.AppleTopCaseHIDEventDriver\"\n\t\t\t\t\"ProductIDArray\" = (666)\n\t\t\t\t\"BatteryStatusFlags\" = 0\n\t\t\t\t\"ColorID\" = 32\n\t\t\t\t\"IOMatchCategory\" = \"IODefaultMatchCategory\"\n\t\t\t\t\"CountryCode\" = 2\n\t\t\t\t\"IOProbeScore\" = 7175\n\t\t\t\t\"PrimaryUsage\" = 11\n\t\t\t\t\"IOGeneralInterest\" = \"IOCommand is not serializable\"\n\t\t\t\t\"BTFW Version\" = 328\n\t\t\t`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmanDev := NewManagedDevice()\n\t\t\tmanDev.Add(tt.data)\n\n\t\t\tassert.Equal(t, manDev.BatteryPercent(), int64(93))\n\t\t\tassert.Equal(t, manDev.BluetoothDevice(), true)\n\t\t\tassert.Equal(t, manDev.BuiltIn(), false)\n\t\t\tassert.Equal(t, manDev.HasBattery(), true)\n\t\t\tassert.Equal(t, manDev.Product(), \"Magic Keyboard with Touch ID\")\n\t\t})\n\t}\n}\n\nfunc Test_BatteryPercent(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpercent  string\n\t\texpected int64\n\t}{\n\t\t{\n\t\t\tname:     \"with empty percent\",\n\t\t\tpercent:  \"\",\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tname:     \"with invalid percent\",\n\t\t\tpercent:  \"3a3\",\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tname:     \"with negative percent\",\n\t\t\tpercent:  \"-23\",\n\t\t\texpected: -23,\n\t\t},\n\t\t{\n\t\t\tname:     \"with valid percent\",\n\t\t\tpercent:  \"23\",\n\t\t\texpected: 23,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmanDev := NewManagedDevice()\n\t\t\tmanDev.Attributes[\"BatteryPercent\"] = tt.percent\n\n\t\t\tactual := manDev.BatteryPercent()\n\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/power/managed_devices.go",
    "content": "package power\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\n// ManageDevices are ...\ntype ManagedDevices struct {\n\tDevices []*ManagedDevice\n\n\targs []string\n\tcmd  string\n}\n\nfunc NewManagedDevices() *ManagedDevices {\n\tmanDevices := &ManagedDevices{\n\t\tDevices: []*ManagedDevice{},\n\n\t\t// This command queries for all managed devices\n\t\targs: []string{\"-c\", \"AppleDeviceManagementHIDEventService\", \"-r\", \"-l\"},\n\t\tcmd:  \"ioreg\",\n\t}\n\n\treturn manDevices\n}\n\nfunc (manDevices *ManagedDevices) Refresh() {\n\tcmd := exec.Command(manDevices.cmd, manDevices.args...)\n\tdata := utils.ExecuteCommand(cmd)\n\n\tmanDevices.Devices = manDevices.parse(data)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// parse takes the output of the command and turns it into ManagedDevice instances\nfunc (manDevices *ManagedDevices) parse(data string) []*ManagedDevice {\n\tdevices := []*ManagedDevice{}\n\n\tchunks := utils.FindBetween(data, \"{\\n\", \"}\\n\")\n\n\tfor _, chunk := range chunks {\n\t\tmanDev := NewManagedDevice()\n\t\tmanDev.Add(chunk)\n\n\t\tdevices = append(devices, manDev)\n\t}\n\n\treturn devices\n}\n\n/* -------------------- And Another Thing -------------------- */\n\n// ManagedDevice represents an entry in the output returned by ioreg when\n// passed AppleDeviceManagementHIDEventService\ntype ManagedDevice struct {\n\tAttributes map[string]string\n}\n\nfunc NewManagedDevice() *ManagedDevice {\n\tmanDev := &ManagedDevice{\n\t\tAttributes: map[string]string{},\n\t}\n\n\treturn manDev\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Add takes a chunk of raw text and attempts to parse it as managed device data\n// and create an attribute map from it.\n/*\n\tA typical chunk will look like:\n\n\t\t\"LowBatteryNotificationPercentage\" = 2\n      \t\"BatteryFaultNotificationType\" = \"TPBatteryFault\"\n      \t...\n      \t\"VersionNumber\" = 0\n\n\twhich should become:\n\n\t\t{\n\t\t\t\"LowBatteryNotificationPercentage\": 2,\n\t\t\t\"BatteryFaultNotificationType\": \"TPBatteryFault\",\n\t\t\t\"VersionNumber\": 0,\n\t\t}\n*/\nfunc (manDev *ManagedDevice) Add(chunk string) {\n\tscanner := bufio.NewScanner(strings.NewReader(chunk))\n\n\tfor scanner.Scan() {\n\t\tline := strings.ReplaceAll(scanner.Text(), \"\\\"\", \"\")\n\n\t\tpieces := strings.Split(line, \"=\")\n\t\tif len(pieces) == 2 {\n\t\t\tleft := strings.TrimSpace(pieces[0])\n\t\t\tright := strings.TrimSpace(pieces[1])\n\n\t\t\tmanDev.Attributes[left] = right\n\t\t}\n\t}\n}\n\n// Dump writes out all the device attributes as a single string\nfunc (manDev *ManagedDevice) Dump() string {\n\tout := \"\"\n\n\tfor attribute, value := range manDev.Attributes {\n\t\tout += fmt.Sprintf(\"%s %s\\n\", attribute, value)\n\t}\n\n\treturn out\n}\n\n/* -------------------- Attributes -------------------- */\n\n// BatteryPercent returns the percent of the device battery\nfunc (manDev *ManagedDevice) BatteryPercent() int64 {\n\tpercent, err := strconv.ParseInt(manDev.Attributes[\"BatteryPercent\"], 10, 64)\n\tif err != nil {\n\t\treturn -1\n\t}\n\n\treturn percent\n}\n\n// BluetoothDevice returns whether or not the device supports bluetooth\nfunc (manDev *ManagedDevice) BluetoothDevice() bool {\n\treturn manDev.Attributes[\"BluetoothDevice\"] == \"Yes\"\n}\n\n// BuiltIn returns whether or not the device is built into the computer\nfunc (manDev *ManagedDevice) BuiltIn() bool {\n\treturn manDev.Attributes[\"BuiltIn\"] == \"Yes\"\n}\n\n// HasBattery returns whether or not the device has a battery\nfunc (manDev *ManagedDevice) HasBattery() bool {\n\treturn manDev.Attributes[\"HasBattery\"] == \"Yes\"\n}\n\n// Product returns the name of the device\nfunc (manDev *ManagedDevice) Product() string {\n\treturn manDev.Attributes[\"Product\"]\n}\n"
  },
  {
    "path": "modules/power/settings.go",
    "content": "package power\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Power\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/power/source.go",
    "content": "//go:build !linux && !freebsd\n\npackage power\n\nimport (\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst SingleQuotesRegExp = \"'(.*)'\"\n\n// powerSource returns the name of the current power source, probably one of\n// \"AC Power\" or \"Battery Power\"\nfunc powerSource() string {\n\tcmd := exec.Command(\"pmset\", []string{\"-g\", \"ps\"}...)\n\tresult := utils.ExecuteCommand(cmd)\n\n\tr, _ := regexp.Compile(SingleQuotesRegExp)\n\n\tsource := r.FindString(result)\n\tsource = strings.Replace(source, \"'\", \"\", -1)\n\n\treturn source\n}\n"
  },
  {
    "path": "modules/power/source_freebsd.go",
    "content": "//go:build freebsd\n\npackage power\n\n// powerSource returns the name of the current power source, probably one of\n// \"AC Power\" or \"Battery Power\"\nfunc powerSource() string {\n\tswitch batteryState {\n\tcase \"1\":\n\t\treturn \"AC Power\"\n\tcase \"0\":\n\t\treturn \"Battery Power\"\n\t}\n\treturn batteryState\n}\n"
  },
  {
    "path": "modules/power/source_linux.go",
    "content": "//go:build linux\n\npackage power\n\n// powerSource returns the name of the current power source, probably one of\n// \"AC Power\" or \"Battery Power\"\nfunc powerSource() string {\n\tswitch batteryState {\n\tcase \"charging\", \"fully-charged\":\n\t\treturn \"AC Power\"\n\tcase \"discharging\":\n\t\treturn \"Battery Power\"\n\t}\n\treturn batteryState\n}\n"
  },
  {
    "path": "modules/power/widget.go",
    "content": "package power\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst (\n\tmsgNoBattery       = \" no battery found\"\n\tproductNameTrimLen = 14\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tBattery        *Battery\n\tManagedDevices *ManagedDevices\n\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tBattery:        NewBattery(),\n\t\tManagedDevices: NewManagedDevices(),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.View.SetWrap(true)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\twidget.Battery.Refresh()\n\n\t// Handle the reading of connected battery-driven devices\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\twidget.ManagedDevices.Refresh()\n\tcase \"linux\":\n\tcase \"windows\":\n\tdefault:\n\t}\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tcontent := fmt.Sprintf(\" %14s: %s\\n\", \"Source\", powerSource())\n\n\tif widget.Battery.String() != msgNoBattery {\n\t\tcontent += widget.Battery.String()\n\t\tcontent += \"\\n\"\n\t}\n\n\tcontent += \"\\n\"\n\n\tfor _, manDev := range widget.ManagedDevices.Devices {\n\t\tif manDev.HasBattery() {\n\t\t\tpercent := utils.ColorizePercent(float64(manDev.BatteryPercent()))\n\n\t\t\tprodName := manDev.Product()\n\n\t\t\tif len(prodName) > productNameTrimLen {\n\t\t\t\tprodName = prodName[:productNameTrimLen]\n\t\t\t}\n\n\t\t\tcontent += fmt.Sprintf(\" %s: %s\\n\", prodName, percent)\n\t\t}\n\t}\n\n\treturn widget.CommonSettings().Title, content, true\n}\n"
  },
  {
    "path": "modules/progress/settings.go",
    "content": "package progress\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Progress\"\n)\n\ntype colors struct {\n\tgradientA string `help:\"Start color for linear gradient.\" values:\"any X11 or hex color\" optional:\"true\" default:\"#56ab2f\"`\n\tgradientB string `help:\"End color for linear gradient.\" values:\"any X11 or hex color\" optional:\"true\" default:\"#a8e063\"`\n\tsolid     string `help:\"Use a solid color instead of linear color gradient .\" values:\"any X11 or hex color\" optional:\"true\"`\n}\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\tcolors\n\tcommon *cfg.Common\n\n\tshowPercentage string `help:\"Where to display the percentage\" values:\"left, right, above, below, titleLeft, titleRight, or none\" optional:\"true\" default:\"right\"`\n\tpadding        int    `help:\"Amount of spaces to add as left/right padding.\" values:\"A positive integer, 0..n\" optional:\"true\" default:\"1\"`\n\n\tminimum float64 `help:\"Minimum progress value.\" values:\"A positive decimal value, 0.0..n.n\" optional:\"true\" default:\"0\"`\n\tmaximum float64 `help:\"Maximum progress value.\" values:\"A positive decimal value, 0.0..n.n\" optional:\"true\" default:\"0\"`\n\tcurrent float64 `help:\"Current progress value. If maximum value is 0, current value is assumed to be a percentage between 0-100.\" values:\"A positive decimal value, 0.0..n.n\" optional:\"true\" default:\"0\"`\n\n\tminimumCmd string `help:\"Execute shell command to determine minimum progress value. Return value must be numeric.\" values:\"Any shell command\" optional:\"true\"`\n\tmaximumCmd string `help:\"Execute shell command to determine maximum progress value. Return value must be numeric.\" values:\"Any shell command\" optional:\"true\"`\n\tcurrentCmd string `help:\"Execute shell command to determine current progress value. Return value must be numeric.\" values:\"Any shell command\" optional:\"true\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tcommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tshowPercentage: ymlConfig.UString(\"showPercentage\", \"right\"),\n\t\tpadding:        ymlConfig.UInt(\"padding\", 1),\n\t\tminimum:        ymlConfig.UFloat64(\"minimum\", 0),\n\t\tmaximum:        ymlConfig.UFloat64(\"maximum\", 0),\n\t\tcurrent:        ymlConfig.UFloat64(\"current\", 0),\n\t\tminimumCmd:     ymlConfig.UString(\"minimumCmd\", \"\"),\n\t\tmaximumCmd:     ymlConfig.UString(\"maximumCmd\", \"\"),\n\t\tcurrentCmd:     ymlConfig.UString(\"currentCmd\", \"\"),\n\t}\n\n\tsettings.gradientA = ymlConfig.UString(\"colors.gradientA\", \"#56ab2f\")\n\tsettings.gradientB = ymlConfig.UString(\"colors.gradientB\", \"#a8e063\")\n\tsettings.solid = ymlConfig.UString(\"colors.solid\", \"\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/progress/widget.go",
    "content": "package progress\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/progress\"\n\t\"github.com/muesli/reflow/ansi\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nvar errShellUndefined = errors.New(\"command shell not defined in $SHELL environment variable\")\n\n// Widget is the container for your module's data\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n\n\tminimum float64\n\tmaximum float64\n\tcurrent float64\n\tpercent float64\n\n\tpadding string\n\n\tshell string\n\n\terr error\n}\n\n// NewWidget creates and returns an instance of Widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.common),\n\n\t\tsettings: settings,\n\n\t\tminimum: settings.minimum,\n\t\tmaximum: settings.maximum,\n\t\tcurrent: settings.current,\n\n\t\tshell: os.Getenv(\"SHELL\"),\n\n\t\tpadding: strings.Repeat(\" \", settings.padding),\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\tvar err error\n\n\tif cmd := widget.settings.minimumCmd; cmd != \"\" {\n\t\twidget.minimum, err = widget.execValueCmd(cmd)\n\t\tif err != nil {\n\t\t\twidget.err = fmt.Errorf(\"minimumCmd execution failed: %w\", err)\n\t\t\twidget.display()\n\t\t\treturn\n\t\t}\n\t}\n\n\tif cmd := widget.settings.maximumCmd; cmd != \"\" {\n\t\twidget.maximum, err = widget.execValueCmd(cmd)\n\t\tif err != nil {\n\t\t\twidget.err = fmt.Errorf(\"maximumCmd execution failed: %w\", err)\n\t\t\twidget.display()\n\t\t\treturn\n\t\t}\n\t}\n\n\tif cmd := widget.settings.currentCmd; cmd != \"\" {\n\t\twidget.current, err = widget.execValueCmd(cmd)\n\t\tif err != nil {\n\t\t\twidget.err = fmt.Errorf(\"currentCmd execution failed: %w\", err)\n\t\t\twidget.display()\n\t\t\treturn\n\t\t}\n\t}\n\n\twidget.calcPercent()\n\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() string {\n\tif widget.err != nil {\n\t\treturn \"[red]Error: \" + widget.err.Error()\n\t}\n\n\tpercent := widget.formatPercent(widget.percent)\n\tbar := widget.buildProgressBar(percent)\n\tbarView := tview.TranslateANSI(bar.ViewAs(widget.percent))\n\n\tvar sb strings.Builder\n\n\tswitch widget.settings.showPercentage {\n\tcase \"left\":\n\t\tsb.WriteString(widget.padding + percent + barView + widget.padding)\n\tcase \"right\":\n\t\tsb.WriteString(widget.padding + barView + percent + widget.padding)\n\tcase \"above\":\n\t\tcentered := utils.CenterText(percent, bar.Width+widget.settings.padding*2)\n\t\tsb.WriteString(centered + \"\\n\" + widget.padding + barView + widget.padding)\n\tcase \"below\":\n\t\tcentered := utils.CenterText(percent, bar.Width+widget.settings.padding*2)\n\t\tsb.WriteString(widget.padding + barView + widget.padding + \"\\n\" + centered)\n\tdefault:\n\t\tsb.WriteString(widget.padding + barView + widget.padding)\n\t}\n\n\treturn sb.String()\n}\n\nfunc (widget *Widget) display() {\n\ttitle := widget.CommonSettings().Title\n\n\tswitch widget.settings.showPercentage {\n\tcase \"titleLeft\":\n\t\ttitle = widget.formatPercent(widget.percent) + \" \" + title\n\tcase \"titleRight\":\n\t\ttitle = title + \" \" + widget.formatPercent(widget.percent)\n\t}\n\n\twidget.Redraw(func() (string, string, bool) {\n\t\treturn title, widget.content(), false\n\t})\n}\n\nfunc (widget *Widget) execValueCmd(cmd string) (float64, error) {\n\tif widget.shell == \"\" {\n\t\treturn -1, errShellUndefined\n\t}\n\n\tout, err := exec.Command(widget.shell, \"-c\", cmd).Output()\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\n\toutStr := strings.TrimSpace(string(out))\n\n\tval, err := strconv.ParseFloat(outStr, 64)\n\tif err != nil {\n\t\treturn -1, fmt.Errorf(\"failed to parse command output '%s' as float64: %w\", outStr, err)\n\t}\n\n\treturn val, nil\n}\n\nfunc (widget *Widget) buildProgressBar(percent string) *progress.Model {\n\tpOpts := []progress.Option{\n\t\tprogress.WithWidth(widget.calcBarWidth(percent)),\n\t\tprogress.WithoutPercentage(),\n\t}\n\n\tif widget.settings.solid != \"\" {\n\t\tpOpts = append(pOpts, progress.WithSolidFill(widget.settings.solid))\n\t} else {\n\t\tpOpts = append(pOpts, progress.WithGradient(\n\t\t\twidget.settings.gradientA,\n\t\t\twidget.settings.gradientB,\n\t\t))\n\t}\n\n\tpb := progress.New(pOpts...)\n\treturn &pb\n}\n\nfunc (widget *Widget) calcPercent() {\n\tif widget.maximum == 0 {\n\t\tif widget.current > 100 {\n\t\t\twidget.percent = 1\n\t\t}\n\n\t\tif widget.current < 0 {\n\t\t\twidget.percent = 0\n\t\t}\n\n\t\twidget.percent = widget.current / 100\n\t\treturn\n\t}\n\n\tif widget.current > widget.maximum {\n\t\twidget.percent = 1\n\t\treturn\n\t}\n\n\tif widget.current < widget.minimum {\n\t\twidget.percent = 0\n\t\treturn\n\t}\n\n\twidget.percent = (widget.current - widget.minimum) / (widget.maximum - widget.minimum)\n}\n\nfunc (widget *Widget) formatPercent(p float64) string {\n\tswitch widget.settings.showPercentage {\n\tcase \"left\":\n\t\treturn fmt.Sprintf(\"%.0f%% \", p*100)\n\tcase \"right\":\n\t\treturn fmt.Sprintf(\" %.0f%%\", p*100)\n\tcase \"none\":\n\t\treturn \"\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"%.0f%%\", p*100)\n\t}\n}\n\nfunc (widget *Widget) calcBarWidth(percent string) int {\n\t_, _, width, _ := widget.View.GetInnerRect()\n\twidth -= widget.settings.padding * 2\n\n\tif widget.settings.showPercentage == \"left\" || widget.settings.showPercentage == \"right\" {\n\t\twidth -= ansi.PrintableRuneWidth(percent)\n\t}\n\n\treturn width\n}\n"
  },
  {
    "path": "modules/resourceusage/settings.go",
    "content": "package resourceusage\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable       = false\n\tdefaultRefreshInterval = \"1s\"\n\tdefaultTitle           = \"ResourceUsage\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcpuCombined bool\n\tshowCPU     bool\n\tshowMem     bool\n\tshowSwp     bool\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tcpuCombined: ymlConfig.UBool(\"cpuCombined\", false),\n\t\tshowCPU:     ymlConfig.UBool(\"showCPU\", true),\n\t\tshowMem:     ymlConfig.UBool(\"showMem\", true),\n\t\tshowSwp:     ymlConfig.UBool(\"showSwp\", true),\n\t}\n\tsettings.RefreshInterval = cfg.ParseTimeString(ymlConfig, \"refreshInterval\", defaultRefreshInterval)\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/resourceusage/widget.go",
    "content": "package resourceusage\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"time\"\n\n\t\"code.cloudfoundry.org/bytefmt\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/shirou/gopsutil/cpu\"\n\t\"github.com/shirou/gopsutil/mem\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget define wtf widget to register widget later\ntype Widget struct {\n\tsettings *Settings\n\ttviewApp *tview.Application\n\tview.BarGraph\n}\n\n// NewWidget Make new instance of widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tBarGraph: view.NewBarGraph(tviewApp, redrawChan, settings.Name, settings.Common),\n\n\t\ttviewApp: tviewApp,\n\t\tsettings: settings,\n\t}\n\n\twidget.View.SetWrap(false)\n\twidget.View.SetWordWrap(false)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// MakeGraph - Load the dead drop stats\nfunc MakeGraph(widget *Widget) {\n\tcpuStats, memInfo := getDataFromSystem(widget)\n\n\tvar itemsCount = 0\n\tif widget.settings.showCPU {\n\t\titemsCount += len(cpuStats)\n\t}\n\n\tif widget.settings.showMem {\n\t\titemsCount++\n\t}\n\n\tif widget.settings.showSwp {\n\t\titemsCount++\n\t}\n\n\tvar stats = make([]view.Bar, itemsCount)\n\tvar nextIndex = 0\n\n\tif widget.settings.showCPU && len(cpuStats) > 0 {\n\t\tfor i, stat := range cpuStats {\n\t\t\t// Stats sometimes jump outside the 0-100 range, possibly due to timing\n\t\t\tstat = math.Min(100, stat)\n\t\t\tstat = math.Max(0, stat)\n\n\t\t\tvar label string\n\t\t\tif widget.settings.cpuCombined {\n\t\t\t\tlabel = \"CPU\"\n\t\t\t} else {\n\t\t\t\tlabel = fmt.Sprint(i)\n\t\t\t}\n\n\t\t\tbar := view.Bar{\n\t\t\t\tLabel:      label,\n\t\t\t\tPercent:    int(stat),\n\t\t\t\tValueLabel: fmt.Sprintf(\"%d%%\", int(stat)),\n\t\t\t\tLabelColor: \"red\",\n\t\t\t}\n\n\t\t\tstats[nextIndex] = bar\n\t\t\tnextIndex++\n\t\t}\n\t}\n\n\tif widget.settings.showMem {\n\t\tusedMemLabel := bytefmt.ByteSize(memInfo.Used)\n\t\ttotalMemLabel := bytefmt.ByteSize(memInfo.Total)\n\n\t\tif usedMemLabel[len(usedMemLabel)-1] == totalMemLabel[len(totalMemLabel)-1] {\n\t\t\tusedMemLabel = usedMemLabel[:len(usedMemLabel)-1]\n\t\t}\n\n\t\tstats[nextIndex] = view.Bar{\n\t\t\tLabel:      \"Mem\",\n\t\t\tPercent:    int(memInfo.UsedPercent),\n\t\t\tValueLabel: fmt.Sprintf(\"%s/%s\", usedMemLabel, totalMemLabel),\n\t\t\tLabelColor: \"green\",\n\t\t}\n\t\tnextIndex++\n\t}\n\n\tif widget.settings.showSwp {\n\t\tswapUsed := memInfo.SwapTotal - memInfo.SwapFree\n\t\tvar swapPercent float64\n\t\tif memInfo.SwapTotal > 0 {\n\t\t\tswapPercent = float64(swapUsed) / float64(memInfo.SwapTotal)\n\t\t}\n\n\t\tusedSwapLabel := bytefmt.ByteSize(swapUsed)\n\t\ttotalSwapLabel := bytefmt.ByteSize(memInfo.SwapTotal)\n\n\t\tif usedSwapLabel[len(usedSwapLabel)-1] == totalSwapLabel[len(totalSwapLabel)-1] {\n\t\t\tusedSwapLabel = usedSwapLabel[:len(usedSwapLabel)-1]\n\t\t}\n\n\t\tstats[nextIndex] = view.Bar{\n\t\t\tLabel:      \"Swp\",\n\t\t\tPercent:    int(swapPercent * 100),\n\t\t\tValueLabel: fmt.Sprintf(\"%s/%s\", usedSwapLabel, totalSwapLabel),\n\t\t\tLabelColor: \"yellow\",\n\t\t}\n\t}\n\n\twidget.BuildBars(stats)\n\n}\n\n// Refresh & update after interval time\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\twidget.View.Clear()\n\tMakeGraph(widget)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc getDataFromSystem(widget *Widget) (cpuStats []float64, memInfo mem.VirtualMemoryStat) {\n\tif widget.settings.showCPU {\n\t\trCPUStats, err := cpu.Percent(time.Duration(0), !widget.settings.cpuCombined)\n\t\tif err == nil {\n\t\t\tcpuStats = rCPUStats\n\t\t}\n\t}\n\n\tif widget.settings.showMem || widget.settings.showSwp {\n\t\trMemInfo, err := mem.VirtualMemory()\n\t\tif err == nil {\n\t\t\tmemInfo = *rMemInfo\n\t\t}\n\t}\n\n\treturn cpuStats, memInfo\n}\n"
  },
  {
    "path": "modules/rollbar/client.go",
    "content": "package rollbar\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc CurrentActiveItems(accessToken, assignedToName string, activeOnly bool) (*ActiveItems, error) {\n\titems := &ActiveItems{}\n\n\trollbarAPIURL.Host = \"api.rollbar.com\"\n\trollbarAPIURL.Path = \"/api/1/items\"\n\tresp, err := rollbarItemRequest(accessToken, assignedToName, activeOnly)\n\tif err != nil {\n\t\treturn items, err\n\t}\n\n\terr = utils.ParseJSON(&items, resp.Body)\n\tif err != nil {\n\t\treturn items, err\n\t}\n\n\treturn items, nil\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nvar (\n\trollbarAPIURL = &url.URL{Scheme: \"https\"}\n)\n\nfunc rollbarItemRequest(accessToken, assignedToName string, activeOnly bool) (*http.Response, error) {\n\tparams := url.Values{}\n\tparams.Add(\"access_token\", accessToken)\n\tparams.Add(\"assigned_user\", assignedToName)\n\tif activeOnly {\n\t\tparams.Add(\"status\", \"active\")\n\t}\n\n\trequestURL := rollbarAPIURL.ResolveReference(&url.URL{RawQuery: params.Encode()})\n\treq, _ := http.NewRequest(\"GET\", requestURL.String(), http.NoBody)\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "modules/rollbar/keyboard.go",
    "content": "package rollbar\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openBuild, \"Open item in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openBuild, \"Open item in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/rollbar/rollbar.go",
    "content": "package rollbar\n\ntype ActiveItems struct {\n\tResults Result `json:\"result\"`\n}\ntype Item struct {\n\tEnvironment      string `json:\"environment\"`\n\tTitle            string `json:\"title\"`\n\tPlatform         string `json:\"platform\"`\n\tStatus           string `json:\"status\"`\n\tTotalOccurrences int    `json:\"total_occurrences\"`\n\tLevel            string `json:\"level\"`\n\tID               int    `json:\"counter\"`\n}\ntype Result struct {\n\tItems []Item `json:\"items\"`\n}\n"
  },
  {
    "path": "modules/rollbar/settings.go",
    "content": "package rollbar\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Rollbar\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\taccessToken    string `help:\"Your Rollbar project access token (Only needs read capabilities).\"`\n\tactiveOnly     bool   `help:\"Only show items that are active.\" optional:\"true\"`\n\tassignedToName string `help:\"Set this to your username if you only want to see items assigned to you.\" optional:\"true\"`\n\tcount          int    `help:\"How many items you want to see. 100 is max.\" optional:\"true\"`\n\tprojectName    string `help:\"This is used to create a link to the item.\"`\n\tprojectOwner   string `help:\"This is used to create a link to the item.\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\taccessToken:    ymlConfig.UString(\"accessToken\", os.Getenv(\"WTF_ROLLBAR_ACCESS_TOKEN\")),\n\t\tactiveOnly:     ymlConfig.UBool(\"activeOnly\", false),\n\t\tassignedToName: ymlConfig.UString(\"assignedToName\"),\n\t\tcount:          ymlConfig.UInt(\"count\", 10),\n\t\tprojectName:    ymlConfig.UString(\"projectName\", \"Items\"),\n\t\tprojectOwner:   ymlConfig.UString(\"projectOwner\"),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.accessToken).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/rollbar/widget.go",
    "content": "package rollbar\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// A Widget represents a Rollbar widget\ntype Widget struct {\n\tview.ScrollableWidget\n\n\titems    *Result\n\tsettings *Settings\n\terr      error\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\titems, err := CurrentActiveItems(\n\t\twidget.settings.accessToken,\n\t\twidget.settings.assignedToName,\n\t\twidget.settings.activeOnly,\n\t)\n\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.items = nil\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\twidget.items = &items.Results\n\t\twidget.SetItemCount(len(widget.items.Items))\n\t}\n\n\twidget.Render()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := fmt.Sprintf(\"%s - %s\", widget.CommonSettings().Title, widget.settings.projectName)\n\tif widget.err != nil {\n\t\treturn widget.CommonSettings().Title, widget.err.Error(), true\n\t}\n\tresult := widget.items\n\tif result == nil || len(result.Items) == 0 {\n\t\treturn title, \"No results\", false\n\t}\n\tvar str string\n\tif len(result.Items) > widget.settings.count {\n\t\tresult.Items = result.Items[:widget.settings.count]\n\t}\n\tfor idx, item := range result.Items {\n\n\t\trow := fmt.Sprintf(\n\t\t\t\"[%s] [%s] %s [%s] %s [%s]count: %d [%s]%s\",\n\t\t\twidget.RowColor(idx),\n\t\t\tlevelColor(&item),\n\t\t\titem.Level,\n\t\t\tstatusColor(&item),\n\t\t\titem.Title,\n\t\t\twidget.RowColor(idx),\n\t\t\titem.TotalOccurrences,\n\t\t\t\"blue\",\n\t\t\titem.Environment,\n\t\t)\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(item.Title))\n\t}\n\n\treturn title, str, false\n}\n\nfunc statusColor(item *Item) string {\n\tswitch item.Status {\n\tcase \"active\":\n\t\treturn \"red\"\n\tcase \"resolved\":\n\t\treturn \"green\"\n\tdefault:\n\t\treturn \"red\"\n\t}\n}\nfunc levelColor(item *Item) string {\n\tswitch item.Level {\n\tcase \"error\":\n\t\treturn \"red\"\n\tcase \"critical\":\n\t\treturn \"green\"\n\tcase \"warning\":\n\t\treturn \"yellow\"\n\tdefault:\n\t\treturn \"grey\"\n\t}\n}\n\nfunc (widget *Widget) openBuild() {\n\tif widget.GetSelected() >= 0 && widget.items != nil && widget.GetSelected() < len(widget.items.Items) {\n\t\titem := &widget.items.Items[widget.GetSelected()]\n\n\t\tutils.OpenFile(\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"https://rollbar.com/%s/%s/%s/%d\",\n\t\t\t\twidget.settings.projectOwner,\n\t\t\t\twidget.settings.projectName,\n\t\t\t\t\"items\",\n\t\t\t\titem.ID,\n\t\t\t),\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "modules/security/dns.go",
    "content": "package security\n\nimport (\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc DnsServers() []string {\n\tswitch runtime.GOOS {\n\tcase \"linux\":\n\t\treturn dnsLinux()\n\tcase \"darwin\":\n\t\treturn dnsMacOS()\n\tcase \"windows\":\n\t\treturn dnsWindows()\n\tdefault:\n\t\treturn []string{runtime.GOOS}\n\t}\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc dnsLinux() []string {\n\t// This may be very Ubuntu specific\n\tcmd := exec.Command(\"nmcli\", \"device\", \"show\")\n\tout := utils.ExecuteCommand(cmd)\n\n\tlines := strings.Split(out, \"\\n\")\n\n\tdns := []string{}\n\n\tfor _, l := range lines {\n\t\tif strings.HasPrefix(l, \"IP4.DNS\") {\n\t\t\tparts := strings.Split(l, \":\")\n\t\t\tdns = append(dns, strings.TrimSpace(parts[1]))\n\t\t}\n\t}\n\n\treturn dns\n}\n\nfunc dnsMacOS() []string {\n\tcmdString := `scutil --dns | head -n 7 | grep -o '[0-9]\\{1,3\\}\\.[0-9]\\{1,3\\}\\.[0-9]\\{1,3\\}\\.[0-9]\\{1,3\\}'`\n\tcmd := exec.Command(\"sh\", \"-c\", cmdString)\n\tout := utils.ExecuteCommand(cmd)\n\n\tlines := strings.Split(out, \"\\n\")\n\n\tif len(lines) > 0 {\n\t\treturn lines\n\t}\n\n\treturn []string{}\n}\n\nfunc dnsWindows() []string {\n\n\tcmd := exec.Command(\"powershell.exe\", \"-NoProfile\", \"Get-DnsClientServerAddress | Select-Object –ExpandProperty ServerAddresses\")\n\n\treturn []string{utils.ExecuteCommand(cmd)}\n}\n"
  },
  {
    "path": "modules/security/firewall.go",
    "content": "package security\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst osxFirewallCmd = \"/usr/libexec/ApplicationFirewall/socketfilterfw\"\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc FirewallState() string {\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\treturn firewallStateMacOS()\n\tcase \"linux\":\n\t\treturn firewallStateLinux()\n\tcase \"windows\":\n\t\treturn firewallStateWindows()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc FirewallStealthState() string {\n\tswitch runtime.GOOS {\n\tcase \"linux\":\n\t\treturn firewallStealthStateLinux()\n\tcase \"darwin\":\n\t\treturn firewallStealthStateMacOS()\n\tcase \"windows\":\n\t\treturn firewallStealthStateWindows()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc firewallStateLinux() string {\n\t// Check UFW first\n\tif hasUfw := checkUfw(); hasUfw != \"\" {\n\t\treturn hasUfw\n\t}\n\n\t// Check Firewalld\n\tif hasFirewalld := checkFirewalld(); hasFirewalld != \"\" {\n\t\treturn hasFirewalld\n\t}\n\n\t// Check nftables\n\tif hasNft := checkNftables(); hasNft != \"\" {\n\t\treturn hasNft\n\t}\n\n\t// Check iptables as last resort\n\tif hasIpt := checkIptables(); hasIpt != \"\" {\n\t\treturn hasIpt\n\t}\n\n\treturn \"[red]No firewall[white]\"\n}\n\nfunc checkFirewalld() string {\n\tcheckInstalled := exec.Command(\"which\", \"firewall-cmd\")\n\tif err := checkInstalled.Run(); err != nil {\n\t\treturn \"\"\n\t}\n\n\tcmd := exec.Command(\"firewall-cmd\", \"--state\")\n\terr := cmd.Start()\n\tif err != nil {\n\t\treturn \"[red]Failed to start status check (firewalld)[white]\"\n\t}\n\n\terr = cmd.Wait()\n\tif err == nil {\n\t\treturn \"[green]Active (firewalld)[white]\"\n\t}\n\n\tif exitError, ok := err.(*exec.ExitError); ok {\n\t\tsc := exitError.Sys().(syscall.WaitStatus).ExitStatus()\n\t\tswitch sc {\n\t\tcase 251:\n\t\t\treturn \"[yellow]Running but failed (firewalld)[white]\"\n\t\tcase 252:\n\t\t\treturn \"[red]Not running (firewalld)[white]\"\n\t\tdefault:\n\t\t\treturn fmt.Sprintf(\"[red]Unexpected state (%d) assume not running (firewalld)[white]\", sc)\n\t\t}\n\t} else {\n\t\treturn fmt.Sprintf(\"[red] Error waiting for command: %v (firewalld)[white]\", err)\n\t}\n}\n\nfunc checkUfw() string {\n\t// First check if UFW is installed\n\tcheckInstalled := exec.Command(\"which\", \"ufw\")\n\tif err := checkInstalled.Run(); err != nil {\n\t\treturn \"\"\n\t}\n\n\t// Then check if service is running\n\tcmd := exec.Command(\"systemctl\", \"is-active\", \"ufw\")\n\terr := cmd.Run()\n\tif err == nil {\n\t\treturn \"[green]Enabled (ufw)[white]\"\n\t}\n\treturn \"[red]Disabled (ufw)[white]\"\n}\n\nfunc checkNftables() string {\n\t// First check if nftables is installed\n\tcheckInstalled := exec.Command(\"which\", \"nft\")\n\tif err := checkInstalled.Run(); err != nil {\n\t\treturn \"\"\n\t}\n\n\t// Then check if service is running\n\tcmd := exec.Command(\"systemctl\", \"is-active\", \"nftables\")\n\terr := cmd.Run()\n\tif err == nil {\n\t\treturn \"[green]Enabled (nftables)[white]\"\n\t}\n\treturn \"[red]Disabled (nftables)[white]\"\n}\n\nfunc checkIptables() string {\n\t// First check if iptables is installed\n\tcheckInstalled := exec.Command(\"which\", \"iptables\")\n\tif strings.Contains(utils.ExecuteCommand(checkInstalled), \"not found\") {\n\t\treturn \"\"\n\t}\n\n\t// Check if iptables module is loaded\n\tcmd := exec.Command(\"lsmod\")\n\tout := utils.ExecuteCommand(cmd)\n\n\tif strings.Contains(out, \"ip_tables\") {\n\t\t// Check for any active rules\n\t\tcmd := exec.Command(\"iptables\", \"-L\")\n\t\tout := utils.ExecuteCommand(cmd)\n\t\tif strings.Contains(out, \"Chain\") && !strings.Contains(out, \"0 references\") {\n\t\t\treturn \"[green]Enabled (iptables)[white]\"\n\t\t}\n\t\treturn \"[yellow]Loaded but unable to check rules (iptables)[white]\"\n\t}\n\treturn \"\"\n}\n\nfunc firewallStateMacOS() string {\n\tcmd := exec.Command(osxFirewallCmd, \"--getglobalstate\")\n\tstr := utils.ExecuteCommand(cmd)\n\n\treturn statusLabel(str)\n}\n\nfunc firewallStateWindows() string {\n\t// The raw way to do this in PS, not using netsh, nor registry, is the following:\n\t//   if (((Get-NetFirewallProfile | select name,enabled)\n\t//                                | where { $_.Enabled -eq $True } | measure ).Count -eq 3)\n\t//   { Write-Host \"OK\" -ForegroundColor Green} else { Write-Host \"OFF\" -ForegroundColor Red }\n\n\tcmd := exec.Command(\"powershell.exe\", \"-NoProfile\",\n\t\t\"-Command\", \"& { ((Get-NetFirewallProfile | select name,enabled) | where { $_.Enabled -eq $True } | measure ).Count }\")\n\n\tfwStat := utils.ExecuteCommand(cmd)\n\tfwStat = strings.TrimSpace(fwStat) // Always sanitize PowerShell output:  \"3\\r\\n\"\n\n\tswitch fwStat {\n\tcase \"3\":\n\t\treturn \"[green]Good[white] (3/3)\"\n\tcase \"2\":\n\t\treturn \"[orange]Poor[white] (2/3)\"\n\tcase \"1\":\n\t\treturn \"[yellow]Bad[white] (1/3)\"\n\tcase \"0\":\n\t\treturn \"[red]Disabled[white]\"\n\tdefault:\n\t\treturn \"[white]N/A[white]\"\n\t}\n}\n\n/* -------------------- Getting Stealth State ------------------- */\n// \"Stealth\": Not responding to pings from unauthorized devices\n\nfunc firewallStealthStateLinux() string {\n\treturn \"[white]N/A[white]\"\n}\n\nfunc firewallStealthStateMacOS() string {\n\tcmd := exec.Command(osxFirewallCmd, \"--getstealthmode\")\n\tstr := utils.ExecuteCommand(cmd)\n\n\treturn statusLabel(str)\n}\n\nfunc firewallStealthStateWindows() string {\n\treturn \"[white]N/A[white]\"\n}\n\nfunc statusLabel(str string) string {\n\tlabel := \"off\"\n\n\tif strings.Contains(str, \"enabled\") {\n\t\tlabel = \"on\"\n\t}\n\n\treturn label\n}\n"
  },
  {
    "path": "modules/security/security_data.go",
    "content": "package security\n\ntype SecurityData struct {\n\tDns             []string\n\tFirewallEnabled string\n\tFirewallStealth string\n\tLoggedInUsers   []string\n\tWifiEncryption  string\n\tWifiName        string\n}\n\nfunc NewSecurityData() *SecurityData {\n\treturn &SecurityData{}\n}\n\nfunc (data SecurityData) DnsAt(idx int) string {\n\tif len(data.Dns) > idx {\n\t\treturn data.Dns[idx]\n\t}\n\treturn \"\"\n}\n\nfunc (data *SecurityData) Fetch() {\n\tdata.Dns = DnsServers()\n\tdata.FirewallEnabled = FirewallState()\n\tdata.FirewallStealth = FirewallStealthState()\n\tdata.LoggedInUsers = LoggedInUsers()\n\tdata.WifiName = WifiName()\n\tdata.WifiEncryption = WifiEncryption()\n}\n"
  },
  {
    "path": "modules/security/settings.go",
    "content": "package security\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Security\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/security/users.go",
    "content": "package security\n\n// http://applehelpwriter.com/2017/05/21/how-to-reveal-hidden-users/\n\nimport (\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc LoggedInUsers() []string {\n\tswitch runtime.GOOS {\n\tcase \"linux\":\n\t\treturn loggedInUsersLinux()\n\tcase \"darwin\":\n\t\treturn loggedInUsersMacOs()\n\tcase \"windows\":\n\t\treturn loggedInUsersWindows()\n\tdefault:\n\t\treturn []string{}\n\t}\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc cleanUsers(users []string) []string {\n\trejects := []string{\"_\", \"root\", \"nobody\", \"daemon\", \"Guest\"}\n\tcleaned := []string{}\n\n\tfor _, user := range users {\n\t\tclean := true\n\n\t\tfor _, reject := range rejects {\n\t\t\tif strings.HasPrefix(user, reject) {\n\t\t\t\tclean = false\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif clean && user != \"\" {\n\t\t\tcleaned = append(cleaned, user)\n\t\t}\n\t}\n\n\treturn cleaned\n}\n\nfunc loggedInUsersLinux() []string {\n\tcmd := exec.Command(\"who\", \"-us\")\n\tusers := utils.ExecuteCommand(cmd)\n\n\tcleaned := []string{}\n\n\tfor _, user := range strings.Split(users, \"\\n\") {\n\t\tclean := true\n\t\tcol := strings.Split(user, \" \")\n\n\t\tif len(col) > 0 {\n\t\t\tfor _, cleanedU := range cleaned {\n\t\t\t\tu := strings.TrimSpace(col[0])\n\t\t\t\tif u == \"\" || strings.Compare(cleanedU, col[0]) == 0 {\n\t\t\t\t\tclean = false\n\t\t\t\t}\n\t\t\t}\n\t\t\tif clean {\n\t\t\t\tcleaned = append(cleaned, col[0])\n\t\t\t}\n\t\t}\n\t}\n\n\treturn cleaned\n}\n\nfunc loggedInUsersMacOs() []string {\n\tcmd := exec.Command(\"dscl\", []string{\".\", \"-list\", \"/Users\"}...)\n\tusers := utils.ExecuteCommand(cmd)\n\n\treturn cleanUsers(strings.Split(users, \"\\n\"))\n}\n\nfunc loggedInUsersWindows() []string {\n\t// We can use either one:\n\t// \t\t(Get-WMIObject -class Win32_ComputerSystem | select username).username\n\t// \t\t[System.Security.Principal.WindowsIdentity]::GetCurrent().Name\n\t// The original was:\n\t//\t\tcmd := exec.Command(\"powershell.exe\", \"(query user) -replace '\\\\s{2,}', ','\")\n\t// But that didn't work!\n\t// The real powershell command reads:\n\t// \t powershell.exe -NoProfile -Command \"& { [System.Security.Principal.WindowsIdentity]::GetCurrent().Name }\"\n\t// But we here have to write it as:\n\tcmd := exec.Command(\"powershell.exe\", \"-NoProfile\", \"-Command\", \"& { [System.Security.Principal.WindowsIdentity]::GetCurrent().Name }\")\n\t// ToDo:  Make list for multi-user systems\n\n\tusers := utils.ExecuteCommand(cmd)\n\treturn cleanUsers(strings.Split(users, \"\\n\"))\n}\n"
  },
  {
    "path": "modules/security/widget.go",
    "content": "package security\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tdata := NewSecurityData()\n\tdata.Fetch()\n\tvar str string\n\n\tif data.WifiName != \"\" {\n\t\tstr += fmt.Sprintf(\" [%s]WiFi[white]\\n\", widget.settings.Colors.Subheading)\n\t\tstr += fmt.Sprintf(\" %8s: %s\\n\", \"Network\", data.WifiName)\n\t\tstr += fmt.Sprintf(\" %8s: %s\\n\", \"Crypto\", data.WifiEncryption)\n\t\tstr += \"\\n\"\n\t}\n\n\tstr += fmt.Sprintf(\" [%s]Firewall[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += fmt.Sprintf(\" %8s: %4s\\n\", \"Status\", data.FirewallEnabled)\n\tstr += fmt.Sprintf(\" %8s: %4s\\n\", \"Stealth\", data.FirewallStealth)\n\tstr += \"\\n\"\n\n\tstr += fmt.Sprintf(\" [%s]Users[white]\\n\", widget.settings.Colors.Subheading)\n\tstr += fmt.Sprintf(\"  %s\", strings.Join(data.LoggedInUsers, \"\\n  \"))\n\tstr += \"\\n\\n\"\n\n\tstr += fmt.Sprintf(\" [%s]DNS[white]\\n\", widget.settings.Colors.Subheading)\n\t// If no DNS servers are found, display a single line of 'n/a'\n\tif len(data.Dns) == 0 {\n\t\tstr += fmt.Sprintf(\" %6s\\n\", \"n/a\")\n\t} else {\n\t\tfor _, ip := range data.Dns {\n\t\t\tstr += fmt.Sprintf(\" %12s\\n\", ip)\n\t\t}\n\t}\n\tstr += \"\\n\"\n\n\treturn widget.CommonSettings().Title, str, false\n}\n"
  },
  {
    "path": "modules/security/wifi.go",
    "content": "package security\n\nimport (\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\n// https://github.com/yelinaung/wifi-name/blob/master/wifi-name.go\nconst osxWifiCmd = \"/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport\"\nconst osxWifiArg = \"-I\"\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc WifiEncryption() string {\n\tswitch runtime.GOOS {\n\tcase \"linux\":\n\t\treturn wifiEncryptionLinux()\n\tcase \"darwin\":\n\t\treturn wifiEncryptionMacOS()\n\tcase \"windows\":\n\t\treturn wifiEncryptionWindows()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc WifiName() string {\n\tswitch runtime.GOOS {\n\tcase \"linux\":\n\t\treturn wifiNameLinux()\n\tcase \"darwin\":\n\t\treturn wifiNameMacOS()\n\tcase \"windows\":\n\t\treturn wifiNameWindows()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc wifiEncryptionLinux() string {\n\tcmd := exec.Command(\"nmcli\", \"-t\", \"-f\", \"in-use,security\", \"dev\", \"wifi\")\n\tout := utils.ExecuteCommand(cmd)\n\n\tname := utils.FindMatch(`\\*:(.+)`, out)\n\n\tif len(name) > 0 {\n\t\treturn name[0][1]\n\t}\n\n\treturn \"\"\n}\n\nfunc wifiEncryptionMacOS() string {\n\tname := utils.FindMatch(`s*auth: (.+)s*`, wifiInfo())\n\treturn matchStr(name)\n}\n\nfunc wifiInfo() string {\n\tcmd := exec.Command(osxWifiCmd, osxWifiArg)\n\treturn utils.ExecuteCommand(cmd)\n}\n\nfunc wifiNameLinux() string {\n\tcmd, _ := exec.Command(\"iwgetid\", \"-r\").Output()\n\treturn string(cmd)\n}\n\nfunc wifiNameMacOS() string {\n\tname := utils.FindMatch(`s*SSID: (.+)s*`, wifiInfo())\n\treturn matchStr(name)\n}\n\nfunc matchStr(data [][]string) string {\n\tif len(data) <= 1 {\n\t\treturn \"\"\n\t}\n\n\treturn data[1][1]\n}\n\n// Windows\nfunc wifiEncryptionWindows() string {\n\treturn parseWlanNetsh(\"Authentication\")\n}\n\nfunc wifiNameWindows() string {\n\treturn parseWlanNetsh(\"SSID\")\n}\n\nfunc parseWlanNetsh(target string) string {\n\tcmd := exec.Command(\"netsh.exe\", \"wlan\", \"show\", \"interfaces\")\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tsplits := strings.Split(string(out), \"\\n\")\n\tvar words []string\n\tfor _, line := range splits {\n\t\ttoken := strings.Split(line, \":\")\n\t\tfor _, word := range token {\n\t\t\twords = append(words, strings.TrimSpace(word))\n\t\t}\n\t}\n\tfor i, token := range words {\n\t\tif token == target {\n\t\t\treturn words[i+1]\n\t\t}\n\t}\n\treturn \"N/A\"\n}\n"
  },
  {
    "path": "modules/spacex/client.go",
    "content": "package spacex\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tspacexLaunchAPI = \"https://api.spacexdata.com/v3/launches/next\"\n)\n\ntype Launch struct {\n\tFlightNumber int        `json:\"flight_number\"`\n\tMissionName  string     `json:\"mission_name\"`\n\tLaunchDate   int64      `json:\"launch_date_unix\"`\n\tIsTentative  bool       `json:\"tentative\"`\n\tRocket       Rocket     `json:\"rocket\"`\n\tLaunchSite   LaunchSite `json:\"launch_site\"`\n\tLinks        Links      `json:\"links\"`\n\tDetails      string     `json:\"details\"`\n}\n\ntype LaunchSite struct {\n\tName string `json:\"site_name_long\"`\n}\n\ntype Rocket struct {\n\tName string `json:\"rocket_name\"`\n}\n\ntype Links struct {\n\tRedditLink  string `json:\"reddit_campaign\"`\n\tYouTubeLink string `json:\"video_link\"`\n}\n\nfunc NextLaunch() (*Launch, error) {\n\tresp, err := http.Get(spacexLaunchAPI)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tvar data Launch\n\terr = utils.ParseJSON(&data, resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &data, nil\n}\n"
  },
  {
    "path": "modules/spacex/settings.go",
    "content": "package spacex\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n)\n\ntype Settings struct {\n\t*cfg.Common\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tspacex := ymlConfig.UString(\"spacex\")\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, spacex, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/spacex/widget.go",
    "content": "package spacex\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\tsettings *Settings\n\terr      error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\t\tsettings:   settings,\n\t}\n\treturn widget\n}\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tvar title = \"Next SpaceX 🚀\"\n\tif widget.CommonSettings().Title != \"\" {\n\t\ttitle = widget.CommonSettings().Title\n\t}\n\n\tlaunch, err := NextLaunch()\n\tvar str string\n\tif err != nil {\n\t\thandleError(widget, err)\n\t} else {\n\n\t\tstr = fmt.Sprintf(\"[%s]Mission[white]\\n\", widget.settings.Colors.Subheading)\n\t\tstr += fmt.Sprintf(\"%s: %s\\n\", \"Name\", launch.MissionName)\n\t\tstr += fmt.Sprintf(\"%s: %s\\n\", \"Date\", wtf.UnixTime(launch.LaunchDate).Format(time.RFC822))\n\t\tstr += fmt.Sprintf(\"%s: %s\\n\", \"Site\", launch.LaunchSite.Name)\n\t\tstr += \"\\n\"\n\n\t\tstr += fmt.Sprintf(\"[%s]Links[white]\\n\", widget.settings.Colors.Subheading)\n\t\tstr += fmt.Sprintf(\"%s: %s\\n\", \"YouTube\", launch.Links.YouTubeLink)\n\t\tstr += fmt.Sprintf(\"%s: %s\\n\", \"Reddit\", launch.Links.RedditLink)\n\n\t\tif widget.CommonSettings().Height >= 2 {\n\t\t\tstr += \"\\n\"\n\t\t\tstr += fmt.Sprintf(\"[%s]Details[white]\\n\", widget.settings.Colors.Subheading)\n\t\t\tstr += fmt.Sprintf(\"%s: %s\\n\", \"RocketName\", launch.Rocket.Name)\n\t\t\tstr += fmt.Sprintf(\"%s: %s\\n\", \"Details\", launch.Details)\n\t\t}\n\t}\n\treturn title, str, true\n}\n\nfunc handleError(widget *Widget, err error) {\n\twidget.err = err\n}\n"
  },
  {
    "path": "modules/spotify/keyboard.go",
    "content": "package spotify\n\nimport (\n\t\"time\"\n\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"l\", widget.next, \"Select next item\")\n\twidget.SetKeyboardChar(\"h\", widget.previous, \"Select previous item\")\n\twidget.SetKeyboardChar(\" \", widget.playPause, \"Play/pause song\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.previous, \"Select previous item\")\n}\n\nfunc (widget *Widget) previous() {\n\twidget.client.Previous()\n\ttime.Sleep(time.Second * 1)\n\twidget.Refresh()\n}\n\nfunc (widget *Widget) next() {\n\twidget.client.Next()\n\ttime.Sleep(time.Second * 1)\n\twidget.Refresh()\n}\n\nfunc (widget *Widget) playPause() {\n\twidget.client.PlayPause()\n\ttime.Sleep(time.Second * 1)\n\twidget.Refresh()\n}\n"
  },
  {
    "path": "modules/spotify/settings.go",
    "content": "package spotify\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Spotify\"\n)\n\ntype colors struct {\n\tlabel string\n\ttext  string\n}\n\ntype Settings struct {\n\tcolors\n\t*cfg.Common\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\tsettings.label = ymlConfig.UString(\"colors.label\", \"green\")\n\tsettings.text = ymlConfig.UString(\"colors.text\", \"white\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/spotify/widget.go",
    "content": "package spotify\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/spotigopher/spotigopher\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// A Widget represents a Spotify widget\ntype Widget struct {\n\tview.TextWidget\n\n\tclient   spotigopher.SpotifyClient\n\tsettings *Settings\n\tspotigopher.Info\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tInfo:   spotigopher.Info{},\n\t\tclient: spotigopher.NewClient(),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.settings.RefreshInterval = 5 * time.Second\n\n\twidget.initializeKeyboardControls()\n\n\twidget.View.SetWrap(true)\n\twidget.View.SetWordWrap(true)\n\n\treturn &widget\n}\n\nfunc (w *Widget) refreshSpotifyInfos() error {\n\tinfo, err := w.client.GetInfo()\n\tw.Info = info\n\treturn err\n}\n\nfunc (w *Widget) Refresh() {\n\tw.Redraw(w.createOutput)\n}\n\nfunc (w *Widget) createOutput() (string, string, bool) {\n\tvar content string\n\terr := w.refreshSpotifyInfos()\n\tif err != nil {\n\t\tcontent = err.Error()\n\t} else {\n\t\tlabelColor := w.settings.label\n\t\ttextColor := w.settings.text\n\n\t\tartist := strings.Join(w.Artist, \", \")\n\n\t\tcontent = utils.CenterText(fmt.Sprintf(\"[%s]Now %v [%s]\\n\", labelColor, w.Status, textColor), w.CommonSettings().Width)\n\t\tcontent += utils.CenterText(fmt.Sprintf(\"[%s]Title:[%s] %v\\n \", labelColor, textColor, w.Title), w.CommonSettings().Width)\n\t\tcontent += utils.CenterText(fmt.Sprintf(\"[%s]Artist:[%s] %v\\n\", labelColor, textColor, artist), w.CommonSettings().Width)\n\t\tcontent += utils.CenterText(fmt.Sprintf(\"[%s]%v:[%s] %v\\n\", labelColor, w.TrackNumber, textColor, w.Album), w.CommonSettings().Width)\n\t}\n\treturn w.CommonSettings().Title, content, true\n}\n"
  },
  {
    "path": "modules/spotifyweb/keyboard.go",
    "content": "package spotifyweb\n\nimport (\n\t\"time\"\n\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"h\", widget.selectPrevious, \"Select previous item\")\n\twidget.SetKeyboardChar(\"l\", widget.selectNext, \"Select next item\")\n\twidget.SetKeyboardChar(\" \", widget.playPause, \"Play/pause\")\n\twidget.SetKeyboardChar(\"s\", widget.toggleShuffle, \"Toggle shuffle\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.selectNext, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.selectPrevious, \"Select previous item\")\n}\n\nfunc (widget *Widget) selectPrevious() {\n\terr := widget.client.Previous()\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttime.Sleep(time.Millisecond * 500)\n\twidget.Refresh()\n}\n\nfunc (widget *Widget) selectNext() {\n\terr := widget.client.Next()\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttime.Sleep(time.Millisecond * 500)\n\twidget.Refresh()\n}\n\nfunc (widget *Widget) playPause() {\n\tvar err error\n\tif widget.playerState.Playing {\n\t\terr = widget.client.Pause()\n\t} else {\n\t\terr = widget.client.Play()\n\t}\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttime.Sleep(time.Millisecond * 500)\n\twidget.Refresh()\n}\n\nfunc (widget *Widget) toggleShuffle() {\n\twidget.playerState.ShuffleState = !widget.playerState.ShuffleState\n\terr := widget.client.Shuffle(widget.playerState.ShuffleState)\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttime.Sleep(time.Millisecond * 500)\n\twidget.Refresh()\n}\n"
  },
  {
    "path": "modules/spotifyweb/settings.go",
    "content": "package spotifyweb\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Spotify Web\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcallbackPort string\n\tclientID     string\n\tsecretKey    string\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tcallbackPort: ymlConfig.UString(\"callbackPort\", \"8080\"),\n\t\tclientID:     ymlConfig.UString(\"clientID\", os.Getenv(\"SPOTIFY_ID\")),\n\t\tsecretKey:    ymlConfig.UString(\"secretKey\", os.Getenv(\"SPOTIFY_SECRET\")),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.secretKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/spotifyweb/widget.go",
    "content": "package spotifyweb\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\t\"github.com/zmb3/spotify\"\n)\n\nvar (\n\tauth           spotify.Authenticator\n\ttempClientChan = make(chan *spotify.Client)\n\tstate          = \"wtfSpotifyWebStateString\"\n\tauthURL        string\n\tcallbackPort   string\n\tredirectURI    string\n)\n\n// Info is the struct that contains all the information the Spotify player displays to the user\ntype Info struct {\n\tArtists     string\n\tTitle       string\n\tAlbum       string\n\tTrackNumber int\n\tStatus      string\n}\n\n// Widget is the struct used by all WTF widgets to transfer to the main widget controller\ntype Widget struct {\n\tview.TextWidget\n\n\tInfo\n\n\tclient      *spotify.Client\n\tclientChan  chan *spotify.Client\n\tplayerState *spotify.PlayerState\n\tsettings    *Settings\n}\n\nfunc authHandler(w http.ResponseWriter, r *http.Request) {\n\ttok, err := auth.Token(state, r)\n\tif err != nil {\n\t\thttp.Error(w, \"Couldn't get token\", http.StatusForbidden)\n\t}\n\tif st := r.FormValue(\"state\"); st != state {\n\t\thttp.NotFound(w, r)\n\t}\n\t// use the token to get an authenticated client\n\tclient := auth.NewClient(tok)\n\t_, err = fmt.Fprintf(w, \"Login Completed!\")\n\tif err != nil {\n\t\treturn\n\t}\n\ttempClientChan <- &client\n}\n\n// NewWidget creates a new widget for WTF\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\tredirectURI = \"http://localhost:\" + settings.callbackPort + \"/callback\"\n\n\tauth = spotify.NewAuthenticator(redirectURI, spotify.ScopeUserReadCurrentlyPlaying, spotify.ScopeUserReadPlaybackState, spotify.ScopeUserModifyPlaybackState)\n\tauth.SetAuthInfo(settings.clientID, settings.secretKey)\n\tauthURL = auth.AuthURL(state)\n\n\tvar client *spotify.Client\n\tvar playerState *spotify.PlayerState\n\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tInfo: Info{},\n\n\t\tclient:      client,\n\t\tclientChan:  tempClientChan,\n\t\tplayerState: playerState,\n\t\tsettings:    settings,\n\t}\n\n\thttp.HandleFunc(\"/callback\", authHandler)\n\tgo func() {\n\t\terr := http.ListenAndServe(\":\"+callbackPort, nil)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}()\n\n\tgo func() {\n\t\t// wait for auth to complete\n\t\tclient = <-tempClientChan\n\n\t\t// use the client to make calls that require authorization\n\t\t_, err := client.CurrentUser()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tplayerState, err = client.PlayerState()\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\twidget.client = client\n\t\twidget.playerState = playerState\n\t\twidget.Refresh()\n\t}()\n\n\t// While I wish I could find the reason this doesn't work, I can't.\n\t//\n\t// Normally, this should open the URL to the browser, however it opens the Explorer window in Windows.\n\t// This mostly likely has to do with the fact that the URL includes some very special characters that no terminal likes.\n\t// The only solution would be to include quotes in the command, which is why I do here, but it doesn't work.\n\t//\n\t// If inconvenient, I'll remove this option and save the URL in a file or some other method.\n\tutils.OpenFile(`\"` + authURL + `\"`)\n\n\twidget.settings.RefreshInterval = 5 * time.Second\n\n\twidget.initializeKeyboardControls()\n\n\twidget.View.SetWrap(true)\n\twidget.View.SetWordWrap(true)\n\n\treturn &widget\n}\n\nfunc (w *Widget) refreshSpotifyInfos() error {\n\tif w.client == nil || w.playerState == nil {\n\t\treturn errors.New(\"authentication failed! Please log in to Spotify by visiting the following page in your browser: \" + authURL)\n\t}\n\tvar err error\n\tw.playerState, err = w.client.PlayerState()\n\tif err != nil {\n\t\treturn errors.New(\"extracting player state failed! Please refresh or restart WTF\")\n\t}\n\tw.Album = fmt.Sprint(w.playerState.Item.Album.Name)\n\tartists := \"\"\n\tfor _, artist := range w.playerState.Item.Artists {\n\t\tartists += artist.Name + \", \"\n\t}\n\tartists = artists[:len(artists)-2]\n\tw.Artists = artists\n\tw.Title = fmt.Sprint(w.playerState.Item.Name)\n\tw.TrackNumber = w.playerState.Item.TrackNumber\n\tif w.playerState.Playing {\n\t\tw.Status = \"Playing\"\n\t} else {\n\t\tw.Status = \"Paused\"\n\t}\n\treturn nil\n}\n\n// Refresh refreshes the current view of the widget\nfunc (w *Widget) Refresh() {\n\tw.Redraw(w.createOutput)\n}\n\nfunc (w *Widget) createOutput() (string, string, bool) {\n\tvar output string\n\n\terr := w.refreshSpotifyInfos()\n\tif err != nil {\n\t\toutput = err.Error()\n\t} else {\n\t\toutput += utils.CenterText(fmt.Sprintf(\"[green]Now %v [white]\\n\", w.Status), w.CommonSettings().Width)\n\t\toutput += utils.CenterText(fmt.Sprintf(\"[green]Title:[white] %v\\n\", w.Title), w.CommonSettings().Width)\n\t\toutput += utils.CenterText(fmt.Sprintf(\"[green]Artist:[white] %v\\n\", w.Artists), w.CommonSettings().Width)\n\t\toutput += utils.CenterText(fmt.Sprintf(\"[green]Album:[white] %v\\n\", w.Album), w.CommonSettings().Width)\n\t\tif w.playerState.ShuffleState {\n\t\t\toutput += utils.CenterText(\"[green]Shuffle:[white] on\\n\", w.CommonSettings().Width)\n\t\t} else {\n\t\t\toutput += utils.CenterText(\"[green]Shuffle:[white] off\\n\", w.CommonSettings().Width)\n\t\t}\n\t}\n\treturn w.CommonSettings().Title, output, true\n}\n"
  },
  {
    "path": "modules/status/settings.go",
    "content": "package status\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Status\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/status/widget.go",
    "content": "package status\n\nimport (\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tCurrentIcon int\n\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tCurrentIcon: 0,\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.animation)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) animation() (string, string, bool) {\n\ticons := []string{\"|\", \"/\", \"-\", \"\\\\\", \"|\"}\n\tnext := icons[widget.CurrentIcon]\n\n\twidget.CurrentIcon++\n\tif widget.CurrentIcon == len(icons) {\n\t\twidget.CurrentIcon = 0\n\t}\n\n\treturn widget.CommonSettings().Title, next, false\n}\n"
  },
  {
    "path": "modules/steam/client.go",
    "content": "package steam\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\ntype Steam struct {\n\tclient  *http.Client\n\tbaseUrl string\n}\n\ntype ClientOpts struct {\n\tkey     string\n\tbaseUrl string\n}\n\nfunc NewClient(opts *ClientOpts) *Steam {\n\tbaseUrl := opts.baseUrl\n\n\tif opts.baseUrl == \"\" {\n\t\tbaseUrl = \"http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=\"\n\t}\n\n\tbaseUrl += opts.key + \"&steamids=\"\n\n\treturn &Steam{\n\t\tclient:  &http.Client{},\n\t\tbaseUrl: baseUrl,\n\t}\n}\n\ntype Player struct {\n\tPersonaname   string `json:\"personaname\"`\n\tProfileUrl    string `json:\"profileurl\"`\n\tPersonastate  int    `json:\"personastate\"`\n\tGameextrainfo string `json:\"gameextrainfo\"`\n}\n\ntype SteamResponse struct {\n\tResponse struct {\n\t\tPlayers []Player `json:\"players\"`\n\t} `json:\"response\"`\n}\n\nfunc (s *Steam) Status(steamID string) (*Player, error) {\n\tresp, err := s.fetch(steamID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar response SteamResponse\n\n\tif err := json.Unmarshal(resp, &response); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &response.Response.Players[0], nil\n}\n\nfunc (s *Steam) fetch(id string) ([]byte, error) {\n\tresp, err := http.Get(s.baseUrl + id)\n\n\tif err != nil || resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"error fetching %s steam status: %v, status: %d\", id, err, resp.StatusCode)\n\t}\n\n\tdefer resp.Body.Close()\n\treturn io.ReadAll(resp.Body)\n}\n"
  },
  {
    "path": "modules/steam/keyboard.go",
    "content": "package steam\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/steam/settings.go",
    "content": "package steam\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = true\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tnumberOfResults int      `help:\"Number of rows to show. Default is 10.\" optional:\"true\"`\n\tkey             string   `help:\"Steam API key (default is env var STEAM_API_KEY)\"`\n\tuserIds         []string `help:\"Steam user ids\" optional:\"true\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsteam := ymlConfig.UString(\"steam\")\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, steam, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tnumberOfResults: ymlConfig.UInt(\"numberOfResults\", 10),\n\t\tkey:             ymlConfig.UString(\"key\", os.Getenv(\"STEAM_API_KEY\")),\n\t\tuserIds:         utils.ToStrs(ymlConfig.UList(\"userIds\", make([]interface{}, 0))),\n\t}\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/steam/widget.go",
    "content": "package steam\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tsettings *Settings\n\terr      error\n\tsteam    *Steam\n\tplayers  []*Player\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\t\tsettings:         settings,\n\t\tsteam:            NewClient(&ClientOpts{key: settings.key}),\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\nfunc (widget *Widget) Refresh() {\n\terrg, _ := errgroup.WithContext(context.Background())\n\tplayers := make([]*Player, len(widget.settings.userIds))\n\n\tfor i, id := range widget.settings.userIds {\n\t\tfunc(idx int, id string) {\n\t\t\terrg.Go(func() error {\n\t\t\t\tstatus, err := widget.steam.Status(id)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tplayers[idx] = status\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}(i, id)\n\t}\n\n\tif err := errg.Wait(); err != nil {\n\t\twidget.err = err\n\t\twidget.players = nil\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\twidget.err = nil\n\t\tif len(players) <= widget.settings.numberOfResults {\n\t\t\twidget.players = players\n\t\t} else {\n\t\t\twidget.players = players[:widget.settings.numberOfResults]\n\t\t}\n\t\twidget.SetItemCount(len(widget.players))\n\t}\n\n\twidget.Render()\n}\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\nfunc friendlyStatus(personastate int) string {\n\tswitch personastate {\n\tcase 0:\n\t\treturn \"Offline\"\n\tcase 1:\n\t\treturn \"Online\"\n\tcase 2:\n\t\treturn \"Busy\"\n\tcase 3:\n\t\treturn \"Away\"\n\tcase 4:\n\t\treturn \"Snooze\"\n\tcase 5:\n\t\treturn \"Looking to Trade\"\n\tcase 6:\n\t\treturn \"Looking to Play\"\n\t}\n\treturn \"\"\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tvar title = \"Steam Statuses\"\n\n\tif widget.CommonSettings().Title != \"\" {\n\t\ttitle = widget.CommonSettings().Title\n\t}\n\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif len(widget.players) == 0 {\n\t\treturn title, \"No data\", false\n\t}\n\n\tvar str string\n\n\tfor idx, player := range widget.players {\n\t\tstatus := friendlyStatus(player.Personastate)\n\n\t\trow := fmt.Sprintf(\n\t\t\t\"[white]%s: [yellow]%s\",\n\t\t\tplayer.Personaname,\n\t\t\tstatus,\n\t\t)\n\n\t\tif len(player.Gameextrainfo) > 0 {\n\t\t\trow += \" [red](\" + player.Gameextrainfo + \")\"\n\t\t}\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(player.Personaname))\n\t}\n\n\treturn title, str, false\n}\n"
  },
  {
    "path": "modules/stocks/finnhub/client.go",
    "content": "package finnhub\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\n// Client ..\ntype Client struct {\n\tsymbols []string\n\tapiKey  string\n}\n\n// NewClient ..\nfunc NewClient(symbols []string, apiKey string) *Client {\n\tclient := Client{\n\t\tsymbols: symbols,\n\t\tapiKey:  apiKey,\n\t}\n\n\treturn &client\n}\n\n// Getquote ..\nfunc (client *Client) Getquote() ([]Quote, error) {\n\tquotes := []Quote{}\n\n\tfor _, s := range client.symbols {\n\t\tresp, err := client.finnhubRequest(s)\n\t\tif err != nil {\n\t\t\treturn quotes, err\n\t\t}\n\n\t\tvar quote Quote\n\t\tquote.Stock = s\n\t\terr = json.NewDecoder(resp.Body).Decode(&quote)\n\t\tif err != nil {\n\t\t\treturn quotes, err\n\t\t}\n\t\tquotes = append(quotes, quote)\n\t}\n\n\treturn quotes, nil\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nvar (\n\tfinnhubURL = &url.URL{Scheme: \"https\", Host: \"finnhub.io\", Path: \"/api/v1/quote\"}\n)\n\nfunc (client *Client) finnhubRequest(symbol string) (*http.Response, error) {\n\tparams := url.Values{}\n\tparams.Add(\"symbol\", symbol)\n\tparams.Add(\"token\", client.apiKey)\n\n\turl := finnhubURL.ResolveReference(&url.URL{RawQuery: params.Encode()})\n\n\treq, err := http.NewRequest(\"GET\", url.String(), http.NoBody)\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "modules/stocks/finnhub/quote.go",
    "content": "package finnhub\n\ntype Quote struct {\n\tC  float64 `json:\"c\"`\n\tH  float64 `json:\"h\"`\n\tL  float64 `json:\"l\"`\n\tO  float64 `json:\"o\"`\n\tPc float64 `json:\"pc\"`\n\tT  int     `json:\"t\"`\n\n\tStock string\n}\n"
  },
  {
    "path": "modules/stocks/finnhub/settings.go",
    "content": "package finnhub\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"📈 Stocks Price\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey  string   `help:\"Your finnhub API token.\"`\n\tsymbols []string `help:\"An array of stocks symbols (i.e. AAPL, MSFT)\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:  ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_FINNHUB_API_KEY\"))),\n\t\tsymbols: utils.ToStrs(ymlConfig.UList(\"symbols\")),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/stocks/finnhub/widget.go",
    "content": "package finnhub\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/jedib0t/go-pretty/v6/table\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget ..\ntype Widget struct {\n\tview.TextWidget\n\t*Client\n\n\tsettings *Settings\n}\n\n// NewWidget ..\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tClient:     NewClient(settings.symbols, settings.apiKey),\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tquotes, err := widget.Getquote()\n\n\ttitle := widget.CommonSettings().Title\n\tt := table.NewWriter()\n\tt.AppendHeader(table.Row{\"#\", \"Stock\", \"Current Price\", \"Open Price\", \"Change\"})\n\twrap := false\n\tif err != nil {\n\t\twrap = true\n\t} else {\n\t\tfor idx, q := range quotes {\n\t\t\tt.AppendRows([]table.Row{\n\t\t\t\t{idx, q.Stock, q.C, q.O, fmt.Sprintf(\"%.4f\", (q.C-q.O)/q.C)},\n\t\t\t})\n\t\t}\n\t}\n\n\treturn title, t.Render(), wrap\n}\n"
  },
  {
    "path": "modules/stocks/yfinance/settings.go",
    "content": "package yfinance\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Yahoo Finance\"\n)\n\ntype colors struct {\n\tbigup   string\n\tup      string\n\tdrop    string\n\tbigdrop string\n}\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\tcommon *cfg.Common\n\n\tcolors  colors\n\tsort    bool\n\tsymbols []string `help:\"An array of Yahoo Finance symbols (for example: DOCN, GME, GC=F)\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tcommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t\t// RefreshInterval: ,\n\t}\n\n\tsettings.common.RefreshInterval = cfg.ParseTimeString(ymlConfig, \"refreshInterval\", \"60s\")\n\tsettings.colors.bigup = ymlConfig.UString(\"colors.bigup\", \"greenyellow\")\n\tsettings.colors.up = ymlConfig.UString(\"colors.up\", \"green\")\n\tsettings.colors.drop = ymlConfig.UString(\"colors.drop\", \"firebrick\")\n\tsettings.colors.bigdrop = ymlConfig.UString(\"colors.bigdrop\", \"red\")\n\tsettings.sort = ymlConfig.UBool(\"sort\", false)\n\tsettings.symbols = utils.ToStrs(ymlConfig.UList(\"symbols\"))\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/stocks/yfinance/widget.go",
    "content": "package yfinance\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/jedib0t/go-pretty/v6/table\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget is the container for your module's data\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n}\n\n// NewWidget creates and returns an instance of Widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\n\t// The last call should always be to the display function\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() string {\n\tyquotes := quotes(widget.settings.symbols)\n\n\tcolors := map[string]string{\n\t\t\"bigup\":   widget.settings.colors.bigup,\n\t\t\"up\":      widget.settings.colors.up,\n\t\t\"drop\":    widget.settings.colors.drop,\n\t\t\"bigdrop\": widget.settings.colors.bigdrop,\n\t}\n\n\tif widget.settings.sort {\n\t\tsort.SliceStable(yquotes, func(i, j int) bool { return yquotes[i].MarketChangePct > yquotes[j].MarketChangePct })\n\t}\n\n\tt := table.NewWriter()\n\tt.SetStyle(tableStyle())\n\tfor _, yq := range yquotes {\n\t\tt.AppendRow([]interface{}{\n\t\t\tGetMarketIcon(yq.MarketState),\n\t\t\tyq.Symbol,\n\t\t\tfmt.Sprintf(\"%8.2f %s\", yq.MarketPrice, yq.Currency),\n\t\t\tGetTrendIcon(yq.Trend),\n\t\t\tfmt.Sprintf(\"[%s]%+6.2f (%+5.2f%%)[white]\", colors[yq.Trend], yq.MarketChange, yq.MarketChangePct),\n\t\t})\n\t}\n\n\treturn t.Render()\n}\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(func() (string, string, bool) {\n\t\treturn widget.CommonSettings().Title, widget.content(), false\n\t})\n}\n"
  },
  {
    "path": "modules/stocks/yfinance/yquote.go",
    "content": "package yfinance\n\nimport (\n\t\"github.com/jedib0t/go-pretty/v6/table\"\n\t\"github.com/jedib0t/go-pretty/v6/text\"\n\t\"github.com/piquette/finance-go/quote\"\n)\n\ntype MarketState string\n\ntype yquote struct {\n\tTrend           string // can be bigup (>3%), up, drop or bigdrop (<3%)\n\tSymbol          string\n\tCurrency        string\n\tMarketState     string\n\tMarketPrice     float64\n\tMarketChange    float64\n\tMarketChangePct float64\n}\n\nfunc tableStyle() table.Style {\n\treturn table.Style{\n\t\tName: \"yfinance\",\n\t\tBox: table.BoxStyle{\n\t\t\tBottomLeft:       \"\",\n\t\t\tBottomRight:      \"\",\n\t\t\tBottomSeparator:  \"\",\n\t\t\tLeft:             \"\",\n\t\t\tLeftSeparator:    \"\",\n\t\t\tMiddleHorizontal: \" \",\n\t\t\tMiddleSeparator:  \"\",\n\t\t\tMiddleVertical:   \"\",\n\t\t\tPaddingLeft:      \" \",\n\t\t\tPaddingRight:     \"\",\n\t\t\tRight:            \"\",\n\t\t\tRightSeparator:   \"\",\n\t\t\tTopLeft:          \"\",\n\t\t\tTopRight:         \"\",\n\t\t\tTopSeparator:     \"\",\n\t\t\tUnfinishedRow:    \"\",\n\t\t},\n\t\tColor: table.ColorOptions{\n\t\t\tFooter:       text.Colors{},\n\t\t\tHeader:       text.Colors{},\n\t\t\tRow:          text.Colors{},\n\t\t\tRowAlternate: text.Colors{},\n\t\t},\n\t\tFormat: table.FormatOptions{\n\t\t\tFooter: text.FormatUpper,\n\t\t\tHeader: text.FormatUpper,\n\t\t\tRow:    text.FormatDefault,\n\t\t},\n\t\tOptions: table.Options{\n\t\t\tDrawBorder:      false,\n\t\t\tSeparateColumns: false,\n\t\t\tSeparateFooter:  false,\n\t\t\tSeparateHeader:  false,\n\t\t\tSeparateRows:    false,\n\t\t},\n\t}\n}\n\nfunc quotes(symbols []string) []yquote {\n\tvar yquotes []yquote\n\tfor _, symbol := range symbols {\n\t\tvar yq yquote\n\n\t\tvar MarketPrice float64\n\t\tvar MarketChange float64\n\t\tvar MarketChangePct float64\n\n\t\tq, err := quote.Get(symbol)\n\t\tif q == nil || err != nil {\n\t\t\tyq = yquote{\n\t\t\t\tSymbol:      symbol,\n\t\t\t\tTrend:       \"?\",\n\t\t\t\tMarketState: \"?\",\n\t\t\t}\n\t\t} else {\n\n\t\t\tswitch q.MarketState {\n\t\t\tcase \"PRE\":\n\t\t\t\tMarketPrice = q.PreMarketPrice\n\t\t\t\tMarketChange = q.PreMarketChange\n\t\t\t\tMarketChangePct = q.PreMarketChangePercent\n\t\t\tcase \"POST\":\n\t\t\t\tMarketPrice = q.PostMarketPrice\n\t\t\t\tMarketChange = q.PostMarketChange\n\t\t\t\tMarketChangePct = q.PostMarketChangePercent\n\t\t\tdefault:\n\t\t\t\tMarketPrice = q.RegularMarketPrice\n\t\t\t\tMarketChange = q.RegularMarketChange\n\t\t\t\tMarketChangePct = q.RegularMarketChangePercent\n\t\t\t}\n\n\t\t\tyq = yquote{\n\t\t\t\tSymbol:          q.Symbol,\n\t\t\t\tCurrency:        q.CurrencyID,\n\t\t\t\tTrend:           GetTrend(MarketChangePct),\n\t\t\t\tMarketState:     string(q.MarketState),\n\t\t\t\tMarketPrice:     MarketPrice,\n\t\t\t\tMarketChange:    MarketChange,\n\t\t\t\tMarketChangePct: MarketChangePct,\n\t\t\t}\n\t\t}\n\t\tyquotes = append(yquotes, yq)\n\t}\n\treturn yquotes\n}\n\nfunc GetMarketIcon(state string) string {\n\tstates := map[string]string{\n\t\t\"PRE\":     \"⏭\",\n\t\t\"REGULAR\": \"▶\",\n\t\t\"POST\":    \"⏮\",\n\t\t\"?\":       \"?\",\n\t}\n\tif icon, ok := states[state]; ok {\n\t\treturn icon\n\t} else {\n\t\treturn \"⏹\"\n\t}\n}\n\nfunc GetTrendIcon(trend string) string {\n\ticons := map[string]string{\n\t\t\"bigup\":   \"⬆️ \",\n\t\t\"up\":      \"↗️ \",\n\t\t\"drop\":    \"↘️ \",\n\t\t\"bigdrop\": \"⬇️ \",\n\t}\n\treturn icons[trend]\n}\n\nfunc GetTrend(pct float64) string {\n\tvar trend string\n\tif pct > 3 {\n\t\ttrend = \"bigup\"\n\t} else if pct > 0 {\n\t\ttrend = \"up\"\n\t} else if pct > -3 {\n\t\ttrend = \"drop\"\n\t} else {\n\t\ttrend = \"bigdrop\"\n\t}\n\treturn trend\n}\n"
  },
  {
    "path": "modules/subreddit/api.go",
    "content": "package subreddit\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nvar rootPage = \"https://www.reddit.com/r/\"\n\nfunc GetLinks(subreddit string, sortMode string, topTimePeriod string) ([]Link, error) {\n\turl := rootPage + subreddit + \"/\" + sortMode + \".json\"\n\tif sortMode == \"top\" {\n\t\turl = url + \"?sort=top&t=\" + topTimePeriod\n\t}\n\n\trequest, err := http.NewRequest(\"GET\", url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequest.Header.Set(\"User-Agent\", \"wtfutil (https://github.com/wtfutil/wtf)\")\n\n\t// See https://www.reddit.com/r/redditdev/comments/t8e8hc/comment/i18yga2/?utm_source=share&utm_medium=web2x&context=3\n\tclient := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},\n\t\t},\n\t}\n\tresp, err := client.Do(request)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode > 299 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\tvar m RedditDocument\n\terr = utils.ParseJSON(&m, resp.Body)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(m.Data.Children) == 0 {\n\t\treturn nil, fmt.Errorf(\"no links\")\n\t}\n\n\tvar links []Link\n\tfor _, l := range m.Data.Children {\n\t\tlinks = append(links, l.Data)\n\t}\n\treturn links, nil\n}\n"
  },
  {
    "path": "modules/subreddit/keyboard.go",
    "content": "package subreddit\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openLink, \"Open target URL in browser\")\n\twidget.SetKeyboardChar(\"c\", widget.openReddit, \"Open Reddit comments in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openReddit, \"Open story in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/subreddit/link.go",
    "content": "package subreddit\n\ntype Link struct {\n\tScore     int    `json:\"ups\"`\n\tTitle     string `json:\"title\"`\n\tItemURL   string `json:\"url\"`\n\tPermalink string `json:\"permalink\"`\n}\n\ntype RedditDocument struct {\n\tData Subreddit `json:\"data\"`\n}\n\ntype RedditLinkDocument struct {\n\tData Link `json:\"data\"`\n}\n\ntype Subreddit struct {\n\tChildren []RedditLinkDocument `json:\"Children\"`\n}\n"
  },
  {
    "path": "modules/subreddit/settings.go",
    "content": "package subreddit\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n)\n\n// Settings contains the settings for the subreddit view\ntype Settings struct {\n\t*cfg.Common\n\n\tsubreddit     string `help:\"Subreddit to look at\" optional:\"false\"`\n\tnumberOfPosts int    `help:\"Number of posts to show. Default is 10.\" optional:\"true\"`\n\tsortOrder     string `help:\"Sort order for the posts (hot, new, rising, top), default hot\" optional:\"true\"`\n\ttopTimePeriod string `help:\"If top sort is selected, the time period to show posts from (hour, week, day, month, year, all, default all)\"`\n}\n\n// NewSettingsFromYAML creates the settings for this module from a yaml file\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsubreddit := ymlConfig.UString(\"subreddit\")\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, subreddit, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tnumberOfPosts: ymlConfig.UInt(\"numberOfPosts\", 10),\n\t\tsortOrder:     ymlConfig.UString(\"sortOrder\", \"hot\"),\n\t\ttopTimePeriod: ymlConfig.UString(\"topTimePeriod\", \"all\"),\n\t\tsubreddit:     subreddit,\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/subreddit/widget.go",
    "content": "package subreddit\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tsettings *Settings\n\terr      error\n\tlinks    []Link\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tlinks, err := GetLinks(widget.settings.subreddit, widget.settings.sortOrder, widget.settings.topTimePeriod)\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.links = nil\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\tif len(links) <= widget.settings.numberOfPosts {\n\t\t\twidget.links = links\n\t\t\twidget.SetItemCount(len(widget.links))\n\t\t\twidget.err = nil\n\t\t} else {\n\t\t\twidget.links = links[:widget.settings.numberOfPosts]\n\t\t\twidget.SetItemCount(len(widget.links))\n\t\t\twidget.err = nil\n\t\t}\n\t}\n\twidget.Render()\n}\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := \"/r/\" + widget.settings.subreddit + \" - \" + widget.settings.sortOrder\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tvar content string\n\tfor idx, link := range widget.links {\n\t\trow := fmt.Sprintf(\n\t\t\t`[%s]%2d. %s`,\n\t\t\twidget.RowColor(idx),\n\t\t\tidx+1,\n\t\t\ttview.Escape(link.Title),\n\t\t)\n\t\tcontent += utils.HighlightableHelper(widget.View, row, idx, len(link.Title))\n\t}\n\n\treturn title, content, false\n}\n\nfunc (widget *Widget) openLink() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.links != nil && sel < len(widget.links) {\n\t\tstory := &widget.links[sel]\n\t\tutils.OpenFile(story.ItemURL)\n\t}\n}\n\nfunc (widget *Widget) openReddit() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.links != nil && sel < len(widget.links) {\n\t\tstory := &widget.links[sel]\n\t\tfullLink := \"http://reddit.com\" + story.Permalink\n\t\tutils.OpenFile(fullLink)\n\t}\n}\n"
  },
  {
    "path": "modules/system/settings.go",
    "content": "package system\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"System\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/system/system_info.go",
    "content": "//go:build !windows\n\npackage system\n\nimport (\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\ntype SystemInfo struct {\n\tProductName    string\n\tProductVersion string\n\tBuildVersion   string\n}\n\nfunc NewSystemInfo() *SystemInfo {\n\tm := make(map[string]string)\n\n\targ := []string{}\n\n\tvar cmd *exec.Cmd\n\tswitch runtime.GOOS {\n\tcase \"linux\":\n\t\targ = append(arg, \"-a\")\n\t\tcmd = exec.Command(\"lsb_release\", arg...)\n\tcase \"darwin\":\n\t\tcmd = exec.Command(\"sw_vers\", arg...)\n\tdefault:\n\t\tcmd = exec.Command(\"sw_vers\", arg...)\n\t}\n\n\traw := utils.ExecuteCommand(cmd)\n\n\tfor _, row := range strings.Split(raw, \"\\n\") {\n\t\tparts := strings.Split(row, \":\")\n\t\tif len(parts) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tm[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])\n\t}\n\n\tvar sysInfo *SystemInfo\n\tswitch runtime.GOOS {\n\tcase \"linux\":\n\t\tsysInfo = &SystemInfo{\n\t\t\tProductName:    m[\"Distributor ID\"],\n\t\t\tProductVersion: m[\"Description\"],\n\t\t\tBuildVersion:   m[\"Release\"],\n\t\t}\n\tdefault:\n\t\tsysInfo = &SystemInfo{\n\t\t\tProductName:    m[\"ProductName\"],\n\t\t\tProductVersion: m[\"ProductVersion\"],\n\t\t\tBuildVersion:   m[\"BuildVersion\"],\n\t\t}\n\n\t}\n\treturn sysInfo\n}\n"
  },
  {
    "path": "modules/system/system_info_windows.go",
    "content": "//go:build windows\n\npackage system\n\nimport (\n\t\"os/exec\"\n\t\"strings\"\n)\n\ntype SystemInfo struct {\n\tProductName    string\n\tProductVersion string\n\tBuildVersion   string\n}\n\nfunc NewSystemInfo() *SystemInfo {\n\tm := make(map[string]string)\n\n\tcmd := exec.Command(\"powershell.exe\", \"(Get-CimInstance Win32_OperatingSystem).version\")\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\ts := strings.Split(string(out), \".\")\n\tm[\"ProductName\"] = \"Windows\"\n\tm[\"ProductVersion\"] = \"Windows \" + s[0] + \".\" + s[1]\n\tm[\"BuildVersion\"] = s[2]\n\n\tsysInfo := SystemInfo{\n\t\tProductName:    m[\"ProductName\"],\n\t\tProductVersion: m[\"ProductVersion\"],\n\t\tBuildVersion:   m[\"BuildVersion\"],\n\t}\n\n\treturn &sysInfo\n}\n"
  },
  {
    "path": "modules/system/widget.go",
    "content": "package system\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tDate    string\n\tVersion string\n\n\tsettings   *Settings\n\tsystemInfo *SystemInfo\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, date, version string, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tDate: date,\n\n\t\tsettings: settings,\n\t\tVersion:  version,\n\t}\n\n\twidget.systemInfo = NewSystemInfo()\n\n\treturn &widget\n}\n\nfunc (widget *Widget) display() (string, string, bool) {\n\tcontent := fmt.Sprintf(\n\t\t\"%8s: %s\\n%8s: %s\\n\\n%8s: %s\\n%8s: %s\",\n\t\t\"Built\",\n\t\twidget.prettyDate(),\n\t\t\"Vers\",\n\t\twidget.Version,\n\t\t\"OS\",\n\t\twidget.systemInfo.ProductVersion,\n\t\t\"Build\",\n\t\twidget.systemInfo.BuildVersion,\n\t)\n\n\treturn widget.CommonSettings().Title, content, false\n}\n\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.display)\n}\n\nfunc (widget *Widget) prettyDate() string {\n\tstr, err := time.Parse(utils.TimestampFormat, widget.Date)\n\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\treturn str.Format(\"Jan _2, 15:04\")\n}\n"
  },
  {
    "path": "modules/textfile/keyboard.go",
    "content": "package textfile\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(nil)\n\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next file\")\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous file\")\n\twidget.SetKeyboardChar(\"o\", widget.openFile, \"Open file\")\n\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next file\")\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous file\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openFile, \"Open file\")\n}\n\nfunc (widget *Widget) openFile() {\n\tsrc := widget.CurrentSource()\n\tutils.OpenFile(src)\n}\n"
  },
  {
    "path": "modules/textfile/settings.go",
    "content": "package textfile\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Textfile\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tfilePaths   []interface{}\n\tformat      bool\n\tformatStyle string\n\twrapText    bool\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tfilePaths:   ymlConfig.UList(\"filePaths\"),\n\t\tformat:      ymlConfig.UBool(\"format\", false),\n\t\tformatStyle: ymlConfig.UString(\"formatStyle\", \"vim\"),\n\t\twrapText:    ymlConfig.UBool(\"wrapText\", true),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/textfile/widget.go",
    "content": "package textfile\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/alecthomas/chroma/formatters\"\n\t\"github.com/alecthomas/chroma/lexers\"\n\t\"github.com/alecthomas/chroma/styles\"\n\t\"github.com/radovskyb/watcher\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst (\n\tpollingIntervalms = 100\n)\n\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tsettings    *Settings\n\tfileWatcher *watcher.Watcher\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"filePath\", \"filePaths\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\t// Don't use a timer for this widget, watch for filesystem changes instead\n\twidget.settings.RefreshInterval = 0\n\n\twidget.initializeKeyboardControls()\n\n\twidget.SetDisplayFunction(widget.Refresh)\n\twidget.View.SetWordWrap(true)\n\twidget.View.SetWrap(settings.wrapText)\n\n\tgo widget.watchForFileChanges()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh is only called once on start-up. Its job is to display the\n// text files that first time. After that, the watcher takes over\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := fmt.Sprintf(\n\t\t\"[%s]%s[white]\",\n\t\twidget.settings.Colors.Title,\n\t\twidget.CurrentSource(),\n\t)\n\n\t_, _, width, _ := widget.View.GetRect()\n\ttext := widget.settings.PaginationMarker(len(widget.Sources), widget.Idx, width) + \"\\n\"\n\n\tif widget.settings.format {\n\t\ttext += widget.formattedText()\n\t} else {\n\t\ttext += widget.plainText()\n\t}\n\n\treturn title, text, widget.settings.wrapText\n}\n\nfunc (widget *Widget) formattedText() string {\n\tfilePath, _ := utils.ExpandHomeDir(widget.CurrentSource())\n\n\tfile, err := os.Open(filepath.Clean(filePath))\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\tdefer func() { _ = file.Close() }()\n\n\tlexer := lexers.Match(filePath)\n\tif lexer == nil {\n\t\tlexer = lexers.Fallback\n\t}\n\n\tstyle := styles.Get(widget.settings.formatStyle)\n\tif style == nil {\n\t\tstyle = styles.Fallback\n\t}\n\tformatter := formatters.Get(\"terminal256\")\n\tif formatter == nil {\n\t\tformatter = formatters.Fallback\n\t}\n\n\tcontents, _ := io.ReadAll(file)\n\tstr := string(contents)\n\tstr = tview.Escape(str)\n\titerator, _ := lexer.Tokenise(nil, str)\n\n\tvar buf bytes.Buffer\n\terr = formatter.Format(&buf, style, iterator)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\treturn tview.TranslateANSI(buf.String())\n}\n\nfunc (widget *Widget) plainText() string {\n\tfilePath, _ := utils.ExpandHomeDir(filepath.Clean(widget.CurrentSource()))\n\n\ttext, err := os.ReadFile(filepath.Clean(filePath))\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\treturn tview.Escape(string(text))\n}\n\nfunc (widget *Widget) watchForFileChanges() {\n\twidget.fileWatcher = watcher.New()\n\twatch := widget.fileWatcher\n\twatch.FilterOps(watcher.Write)\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-watch.Event:\n\t\t\t\twidget.Refresh()\n\t\t\tcase err := <-watch.Error:\n\t\t\t\tfmt.Println(err)\n\t\t\t\tos.Exit(1)\n\t\t\tcase <-watch.Closed:\n\t\t\t\treturn\n\t\t\tcase quit := <-widget.QuitChan():\n\t\t\t\tif quit {\n\t\t\t\t\twatch.Close()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Watch each textfile for changes\n\tfor _, source := range widget.Sources {\n\t\tfullPath, err := utils.ExpandHomeDir(source)\n\t\tif err == nil {\n\t\t\te := watch.Add(fullPath)\n\t\t\tif e != nil {\n\t\t\t\tfmt.Println(e)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Start the watching process - it'll check for changes every pollingIntervalms.\n\tif err := watch.Start(time.Millisecond * pollingIntervalms); err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "modules/todo/display.go",
    "content": "package todo\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/checklist\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tstr := \"\"\n\thidden := 0\n\n\tswitch widget.settings.checkedPos {\n\tcase \"last\":\n\t\tstr, hidden = widget.sortListByChecked(widget.list.UncheckedItems(), widget.list.CheckedItems())\n\tcase \"first\":\n\t\tstr, hidden = widget.sortListByChecked(widget.list.CheckedItems(), widget.list.UncheckedItems())\n\tdefault:\n\t\tstr, hidden = widget.sortListByChecked(widget.list.Items, []*checklist.ChecklistItem{})\n\t}\n\n\tif widget.Error != \"\" {\n\t\tstr = widget.Error\n\t}\n\n\ttitle := widget.CommonSettings().Title\n\tif widget.showTagPrefix != \"\" {\n\t\ttitle += \" #\" + widget.showTagPrefix\n\t}\n\tif widget.showFilter != \"\" {\n\t\ttitle += fmt.Sprintf(\" /%s\", widget.showFilter)\n\t}\n\tif widget.settings.hiddenNumInTitle {\n\t\ttitle += fmt.Sprintf(\" (%d hidden)\", hidden)\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) sortListByChecked(firstGroup []*checklist.ChecklistItem, secondGroup []*checklist.ChecklistItem) (string, int) {\n\tstr := \"\"\n\thidden := 0\n\tnewList := checklist.NewChecklist(\n\t\twidget.settings.Checkbox.Checked,\n\t\twidget.settings.Checkbox.Unchecked,\n\t)\n\n\toffset := 0\n\tselectedItem := widget.SelectedItem()\n\tfor idx, item := range firstGroup {\n\t\tif widget.shouldShowItem(item) {\n\t\t\tstr += widget.formattedItemLine(idx, hidden, item)\n\t\t} else {\n\t\t\thidden = hidden + 1\n\t\t}\n\t\tnewList.Items = append(newList.Items, item)\n\t\toffset++\n\t}\n\n\tfor idx, item := range secondGroup {\n\t\tif widget.shouldShowItem(item) {\n\t\t\tstr += widget.formattedItemLine(idx+offset, hidden, item)\n\t\t} else {\n\t\t\thidden = hidden + 1\n\t\t}\n\t\tnewList.Items = append(newList.Items, item)\n\t}\n\tif idx, ok := newList.IndexByItem(selectedItem); ok {\n\t\twidget.Selected = idx\n\t}\n\n\twidget.SetList(newList)\n\treturn str, hidden\n}\n\nfunc (widget *Widget) shouldShowItem(item *checklist.ChecklistItem) bool {\n\tif widget.showFilter != \"\" && !strings.Contains(strings.ToLower(item.Text), widget.showFilter) {\n\t\treturn false\n\t}\n\n\tif !widget.settings.parseTags {\n\t\treturn true\n\t}\n\n\tif len(item.Tags) == 0 {\n\t\treturn widget.showTagPrefix == \"\"\n\t}\n\n\tfor _, tag := range item.Tags {\n\t\tfor _, hideTag := range widget.settings.hideTags {\n\t\t\tif widget.showTagPrefix == \"\" && tag == hideTag {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\tif widget.showTagPrefix == \"\" || strings.HasPrefix(tag, widget.showTagPrefix) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (widget *Widget) RowColor(idx int, hidden int, checked bool) string {\n\tif widget.View.HasFocus() && (idx == widget.Selected) {\n\t\tforeground := widget.CommonSettings().Colors.HighlightedForeground\n\t\tif checked {\n\t\t\tforeground = widget.settings.Colors.Checked\n\t\t}\n\t\treturn fmt.Sprintf(\n\t\t\t\"%s:%s\",\n\t\t\tforeground,\n\t\t\twidget.CommonSettings().Colors.HighlightedBackground,\n\t\t)\n\t}\n\n\tif checked {\n\t\treturn widget.settings.Colors.Checked\n\t} else {\n\t\treturn widget.CommonSettings().RowColor(idx - hidden)\n\t}\n}\n\nfunc (widget *Widget) formattedItemLine(idx int, hidden int, currItem *checklist.ChecklistItem) string {\n\trowColor := widget.RowColor(idx, hidden, currItem.Checked)\n\n\ttodoDate := currItem.Date\n\trow := fmt.Sprintf(\n\t\t` [%s]|%s| `,\n\t\trowColor,\n\t\tcurrItem.CheckMark(),\n\t)\n\n\tif widget.settings.parseDates && todoDate != nil {\n\t\trow += fmt.Sprintf(\n\t\t\t`[%s]%s `,\n\t\t\twidget.settings.dateColor,\n\t\t\twidget.getDateString(todoDate),\n\t\t)\n\t}\n\n\ttagsPart := \"\"\n\tif len(currItem.Tags) > 0 {\n\t\ttagsPart = fmt.Sprintf(\n\t\t\t`[%s]%s[white]`,\n\t\t\twidget.settings.tagColor,\n\t\t\tcurrItem.TagString(),\n\t\t)\n\t}\n\n\ttextPart := fmt.Sprintf(\n\t\t`[%s]%s[white]`,\n\t\trowColor,\n\t\ttview.Escape(currItem.Text),\n\t)\n\n\tif widget.settings.parseTags && widget.settings.tagsAtEnd {\n\t\trow += textPart + \" \" + tagsPart\n\t} else if widget.settings.parseTags {\n\t\trow += tagsPart + textPart\n\t} else {\n\t\trow += textPart\n\t}\n\n\treturn utils.HighlightableHelper(widget.View, row, idx-hidden, len(currItem.Text))\n}\n\nfunc (widget *Widget) getDateString(date *time.Time) string {\n\tnow := getNowDate()\n\tdiff := int(date.Sub(now).Hours() / 24)\n\tif diff == 0 {\n\t\treturn \"today\"\n\t} else if diff == 1 {\n\t\treturn \"tomorrow\"\n\t} else if diff <= widget.settings.switchToInDaysIn {\n\t\treturn fmt.Sprintf(\"in %d days\", diff)\n\t} else {\n\t\tdateStr := \"\"\n\t\ty, m, d := date.Year(), date.Month(), date.Day()\n\t\tswitch widget.settings.dateFormat {\n\t\tcase \"yyyy-mm-dd\":\n\t\t\tdateStr = fmt.Sprintf(\"%d-%02d-%02d\", y, m, d)\n\t\tcase \"yy-mm-dd\":\n\t\t\tdateStr = fmt.Sprintf(\"%d-%02d-%02d\", y-2000, m, d)\n\t\tcase \"dd-mm-yyyy\":\n\t\t\tdateStr = fmt.Sprintf(\"%02d-%02d-%d\", d, m, y)\n\t\tcase \"dd-mm-yy\":\n\t\t\tdateStr = fmt.Sprintf(\"%02d-%02d-%d\", d, m, y-2000)\n\t\tcase \"dd M yyyy\":\n\t\t\tdateStr = fmt.Sprintf(\"%02d %s %d\", d, date.Month().String()[:3], y)\n\t\t\t// date\n\t\tcase \"dd M yy\":\n\t\t\tdateStr = fmt.Sprintf(\"%02d %s %d\", d, date.Month().String()[:3], y-2000)\n\t\t\t// dateStr = \"aaasdada\"\n\t\tdefault:\n\t\t\tdateStr = fmt.Sprintf(\"%d-%02d-%02d\", y, m, d)\n\t\t\t// dateStr = fmt.Sprintf(\"%d-%02d-%02d\", y, m, d)\n\t\t}\n\t\tif widget.settings.hideYearIfCurrent && date.Year() == now.Year() {\n\t\t\tif widget.settings.dateFormat[:1] == \"y\" {\n\t\t\t\tdateStr = dateStr[strings.Index(dateStr, \"-\")+1:]\n\t\t\t} else if widget.settings.dateFormat[3:4] == \"-\" {\n\t\t\t\tdateStr = dateStr[:5]\n\t\t\t} else {\n\t\t\t\tparts := strings.Split(dateStr, \" \")\n\t\t\t\tdateStr = parts[0] + \" \" + parts[1]\n\t\t\t}\n\t\t}\n\t\treturn dateStr\n\t}\n}\n\nfunc getNowDate() time.Time {\n\tnow := time.Now()\n\tnow = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Now().Location())\n\treturn now\n}\n"
  },
  {
    "path": "modules/todo/keyboard.go",
    "content": "package todo\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.NextTodo, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.PrevTodo, \"Select previous item\")\n\twidget.SetKeyboardChar(\" \", widget.toggleChecked, \"Toggle checkmark\")\n\twidget.SetKeyboardChar(\"n\", widget.newItem, \"Create new item\")\n\twidget.SetKeyboardChar(\"o\", widget.openFile, \"Open file\")\n\twidget.SetKeyboardChar(\"#\", widget.setTag, \"Set tag(s) to show\")\n\twidget.SetKeyboardChar(\"f\", widget.setFilter, \"Filter shown items\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.NextTodo, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.PrevTodo, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.unselect, \"Clear selection\")\n\twidget.SetKeyboardKey(tcell.KeyCtrlD, widget.deleteSelected, \"Delete item\")\n\twidget.SetKeyboardKey(tcell.KeyCtrlJ, widget.demoteSelected, \"Demote item\")\n\twidget.SetKeyboardKey(tcell.KeyCtrlL, widget.makeSelectedLast, \"Make item last\")\n\twidget.SetKeyboardKey(tcell.KeyCtrlK, widget.promoteSelected, \"Promote item\")\n\twidget.SetKeyboardKey(tcell.KeyCtrlF, widget.makeSelectedFirst, \"Make item first\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.updateSelected, \"Edit item\")\n\n}\n\nfunc (widget *Widget) NextTodo() {\n\tnewIndex := widget.Selected + 1\n\tfor newIndex < len(widget.list.Items) && !widget.shouldShowItem(widget.list.Items[newIndex]) {\n\t\tnewIndex = newIndex + 1\n\t}\n\tif newIndex < len(widget.list.Items) {\n\t\twidget.Selected = newIndex\n\t}\n\twidget.display()\n}\n\nfunc (widget *Widget) PrevTodo() {\n\tnewIndex := widget.Selected - 1\n\tfor newIndex >= 0 && !widget.shouldShowItem(widget.list.Items[newIndex]) {\n\t\tnewIndex = newIndex - 1\n\t}\n\tif newIndex >= 0 {\n\t\twidget.Selected = newIndex\n\t}\n\twidget.display()\n}\n\nfunc (widget *Widget) deleteSelected() {\n\n\tif !widget.isItemSelected() {\n\t\treturn\n\t}\n\n\twidget.list.Delete(widget.Selected)\n\twidget.SetItemCount(len(widget.list.Items))\n\twidget.Prev()\n\twidget.persist()\n\twidget.display()\n}\n\nfunc (widget *Widget) demoteSelected() {\n\tif !widget.isItemSelected() {\n\t\treturn\n\t}\n\n\tj := widget.Selected + 1\n\tif j >= len(widget.list.Items) {\n\t\tj = 0\n\t}\n\n\twidget.list.Swap(widget.Selected, j)\n\twidget.Selected = j\n\n\twidget.persist()\n\twidget.display()\n}\n\nfunc (widget *Widget) makeSelectedLast() {\n\tif !widget.isItemSelected() {\n\t\treturn\n\t}\n\n\tj := widget.Selected + 1\n\tif j >= len(widget.list.Items) {\n\t\treturn\n\t}\n\n\tfor j < len(widget.list.Items) {\n\t\twidget.list.Swap(widget.Selected, j)\n\t\twidget.Selected = j\n\t\tj = j + 1\n\t}\n\n\tif widget.settings.parseDates {\n\t\twidget.Selected = widget.placeItemBasedOnDate(widget.Selected)\n\t}\n\n\twidget.persist()\n\twidget.display()\n}\n\nfunc (widget *Widget) openFile() {\n\tconfDir, _ := cfg.WtfConfigDir()\n\tutils.OpenFile(fmt.Sprintf(\"%s/%s\", confDir, widget.filePath))\n}\n\nfunc (widget *Widget) setTag() {\n\tif !widget.settings.parseTags {\n\t\treturn\n\t}\n\n\twidget.processFormInput(\"Tag prefix:\", \"\", func(filter string) {\n\t\twidget.showTagPrefix = filter\n\t})\n}\n\nfunc (widget *Widget) setFilter() {\n\twidget.processFormInput(\"Filter:\", \"\", func(filter string) {\n\t\twidget.showFilter = strings.ToLower(filter)\n\t})\n}\n\nfunc (widget *Widget) promoteSelected() {\n\tif !widget.isItemSelected() {\n\t\treturn\n\t}\n\n\tk := widget.Selected - 1\n\tif k < 0 {\n\t\tk = len(widget.list.Items) - 1\n\t}\n\n\twidget.list.Swap(widget.Selected, k)\n\twidget.Selected = k\n\twidget.persist()\n\twidget.display()\n}\n\nfunc (widget *Widget) makeSelectedFirst() {\n\tif !widget.isItemSelected() {\n\t\treturn\n\t}\n\n\tj := widget.Selected - 1\n\tif j < 0 {\n\t\treturn\n\t}\n\n\tfor j >= 0 {\n\t\twidget.list.Swap(widget.Selected, j)\n\t\twidget.Selected = j\n\t\tj = j - 1\n\t}\n\n\tif widget.settings.parseDates {\n\t\twidget.Selected = widget.placeItemBasedOnDate(widget.Selected)\n\t}\n\n\twidget.persist()\n\twidget.display()\n}\n\nfunc (widget *Widget) toggleChecked() {\n\tselectedItem := widget.SelectedItem()\n\tif selectedItem == nil {\n\t\treturn\n\t}\n\n\tselectedItem.Toggle()\n\n\tif !selectedItem.Checked {\n\t\twidget.Selected = widget.placeItemBasedOnDate(widget.Selected)\n\t}\n\n\twidget.persist()\n\twidget.display()\n}\n\nfunc (widget *Widget) unselect() {\n\tif widget.showFilter != \"\" {\n\t\twidget.showFilter = \"\"\n\t} else {\n\t\twidget.Selected = -1\n\t}\n\twidget.display()\n}\n"
  },
  {
    "path": "modules/todo/settings.go",
    "content": "package todo\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Todo\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\tfilePath          string\n\tchecked           string\n\tunchecked         string\n\tnewPos            string\n\tcheckedPos        string\n\tparseDates        bool\n\tdateColor         string\n\tswitchToInDaysIn  int\n\tundatedAsDays     int\n\thideYearIfCurrent bool\n\tdateFormat        string\n\tparseTags         bool\n\ttagColor          string\n\ttagsAtEnd         bool\n\thideTags          []interface{}\n\thiddenNumInTitle  bool\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tcommon := cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig)\n\n\tsettings := Settings{\n\t\tCommon: common,\n\n\t\tfilePath:          ymlConfig.UString(\"filename\"),\n\t\tchecked:           ymlConfig.UString(\"checkedIcon\", common.Checkbox.Checked),\n\t\tunchecked:         ymlConfig.UString(\"uncheckedIcon\", common.Checkbox.Unchecked),\n\t\tnewPos:            ymlConfig.UString(\"newPos\", \"first\"),\n\t\tcheckedPos:        ymlConfig.UString(\"checkedPos\", \"last\"),\n\t\tparseDates:        ymlConfig.UBool(\"dates.enabled\", true),\n\t\tdateColor:         ymlConfig.UString(\"colors.date\", \"chartreuse\"),\n\t\tswitchToInDaysIn:  ymlConfig.UInt(\"dates.switchToInDaysIn\", 7),\n\t\tundatedAsDays:     ymlConfig.UInt(\"dates.undatedAsDays\", 7),\n\t\thideYearIfCurrent: ymlConfig.UBool(\"dates.hideYearIfCurrent\", true),\n\t\tdateFormat:        ymlConfig.UString(\"dates.format\", \"yyyy-mm-dd\"),\n\t\tparseTags:         ymlConfig.UBool(\"tags.enabled\", true),\n\t\ttagColor:          ymlConfig.UString(\"colors.tags\", \"khaki\"),\n\t\ttagsAtEnd:         ymlConfig.UString(\"tags.pos\", \"end\") == \"end\",\n\t\thideTags:          ymlConfig.UList(\"tags.hide\"),\n\t\thiddenNumInTitle:  ymlConfig.UBool(\"tags.hiddenInTitle\", true),\n\t}\n\n\tswitch settings.newPos {\n\tcase \"first\", \"last\":\n\tdefault:\n\t\tsettings.newPos = \"last\"\n\t}\n\tswitch settings.checkedPos {\n\tcase \"first\", \"last\", \"none\":\n\tdefault:\n\t\tsettings.checkedPos = \"last\"\n\t}\n\tswitch settings.dateFormat {\n\tcase \"yyyy-mm-dd\", \"yy-mm-dd\", \"dd-mm-yyyy\", \"dd-mm-yy\", \"dd M yy\", \"dd M yyyy\":\n\tdefault:\n\t\tsettings.dateFormat = \"yyyy-mm-dd\"\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/todo/widget.go",
    "content": "package todo\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/checklist\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n\t\"github.com/wtfutil/wtf/wtf\"\n\t\"gopkg.in/yaml.v2\"\n)\n\nconst (\n\tmodalHeight = 7\n\tmodalWidth  = 80\n\toffscreen   = -1000\n)\n\n// A Widget represents a Todo widget\ntype Widget struct {\n\tfilePath      string\n\tlist          checklist.Checklist\n\tpages         *tview.Pages\n\tsettings      *Settings\n\tshowTagPrefix string\n\tshowFilter    string\n\ttviewApp      *tview.Application\n\tError         string\n\n\tview.ScrollableWidget\n\n\t// redrawChan chan bool\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\ttviewApp:      tviewApp,\n\t\tsettings:      settings,\n\t\tfilePath:      settings.filePath,\n\t\tshowTagPrefix: \"\",\n\t\tlist:          checklist.NewChecklist(settings.Checkbox.Checked, settings.Checkbox.Unchecked),\n\t\tpages:         pages,\n\n\t\t// redrawChan: redrawChan,\n\t}\n\n\twidget.init()\n\n\twidget.initializeKeyboardControls()\n\n\twidget.View.SetRegions(true)\n\twidget.View.SetScrollable(true)\n\n\twidget.SetRenderFunction(widget.display)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// SelectedItem returns the currently-selected checklist item or nil if no item is selected\nfunc (widget *Widget) SelectedItem() *checklist.ChecklistItem {\n\tvar selectedItem *checklist.ChecklistItem\n\tif widget.isItemSelected() {\n\t\tselectedItem = widget.list.Items[widget.Selected]\n\t}\n\n\treturn selectedItem\n}\n\n// Refresh updates the data for this widget and displays it onscreen\nfunc (widget *Widget) Refresh() {\n\twidget.Error = \"\"\n\terr := widget.load()\n\tif err != nil {\n\t\twidget.Error = err.Error()\n\t}\n\twidget.display()\n}\n\nfunc (widget *Widget) SetList(list checklist.Checklist) {\n\twidget.list = list\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) init() {\n\t_, err := cfg.CreateFile(widget.filePath)\n\tif err != nil {\n\t\treturn\n\t}\n}\n\n// isItemSelected returns whether any item of the todo is selected or not\nfunc (widget *Widget) isItemSelected() bool {\n\treturn widget.Selected >= 0 && widget.Selected < len(widget.list.Items)\n}\n\n// Loads the todo list from3 Yaml file\nfunc (widget *Widget) load() error {\n\tconfDir, _ := cfg.WtfConfigDir()\n\tfilePath := fmt.Sprintf(\"%s/%s\", confDir, widget.filePath)\n\n\tfileData, err := utils.ReadFileBytes(filePath)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = yaml.Unmarshal(fileData, &widget.list)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// do initial sort based on dates to make sure everything is correct\n\tif widget.settings.parseDates {\n\t\ti := 0\n\t\tfor i < widget.list.Len() {\n\t\t\tfor {\n\t\t\t\tnewIndex := widget.placeItemBasedOnDate(i)\n\t\t\t\tif newIndex == i {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\ti += 1\n\t\t}\n\t}\n\n\twidget.SetItemCount(len(widget.list.Items))\n\twidget.setItemChecks()\n\treturn nil\n}\n\nfunc (widget *Widget) newItem() {\n\twidget.processFormInput(\"New Todo:\", \"\", func(t string) {\n\t\ttext, date, tags := widget.getTextComponents(t)\n\n\t\twidget.list.Add(false, date, tags, text, widget.settings.newPos)\n\t\twidget.SetItemCount(len(widget.list.Items))\n\t\tif widget.settings.parseDates {\n\t\t\tif widget.settings.newPos == \"first\" {\n\t\t\t\twidget.placeItemBasedOnDate(0)\n\t\t\t} else {\n\t\t\t\twidget.placeItemBasedOnDate(widget.list.Len() - 1)\n\t\t\t}\n\t\t}\n\t\twidget.persist()\n\t})\n}\n\nfunc (widget *Widget) getTextComponents(text string) (string, *time.Time, []string) {\n\tvar date *time.Time = nil\n\tif widget.settings.parseDates {\n\t\ttext, date = widget.getTextAndDate(text)\n\t}\n\n\ttags := make([]string, 0)\n\tif widget.settings.parseTags {\n\t\ttext, tags = getTodoTags(text)\n\t}\n\n\ttext = strings.TrimSpace(text)\n\treturn text, date, tags\n}\n\nfunc getTodoTags(text string) (string, []string) {\n\ttags := make([]string, 0)\n\tr, _ := regexp.Compile(`(?i)(^|\\s)#[a-z0-9]+`)\n\tmatches := r.FindAllString(text, -1)\n\n\tfor _, tag := range matches {\n\t\ttag = strings.TrimSpace(tag)\n\t\tsuffix := \" \"\n\t\tif strings.HasSuffix(text, tag) {\n\t\t\tsuffix = \"\"\n\t\t}\n\t\ttext = strings.Replace(text, tag+suffix, \"\", 1)\n\t\ttags = append(tags, tag[1:])\n\t}\n\n\treturn text, tags\n}\n\ntype PatternDuration struct {\n\tpattern string\n\td       int\n\tm       int\n\ty       int\n}\n\nfunc (widget *Widget) getTextAndDate(text string) (string, *time.Time) {\n\tnow := time.Now()\n\ttextLower := strings.ToLower(text)\n\t// check for \"in X days/weeks/months/years\" pattern\n\tr, _ := regexp.Compile(\"(?i)^in [0-9]+ (day|week|month|year)(s|)\")\n\tmatch := r.FindString(text)\n\tif len(match) > 0 && len(text) > len(match) {\n\t\tparts := strings.Split(text, \" \")\n\t\tn, _ := strconv.Atoi(parts[1])\n\t\tunit := parts[2][:1]\n\t\tvar target time.Time\n\n\t\tswitch unit {\n\t\tcase \"d\":\n\t\t\ttarget = now.AddDate(0, 0, n)\n\t\tcase \"w\":\n\t\t\ttarget = now.AddDate(0, 0, 7*n)\n\t\tcase \"m\":\n\t\t\ttarget = now.AddDate(0, n, 0)\n\t\tdefault:\n\t\t\ttarget = now.AddDate(n, 0, 0)\n\t\t}\n\n\t\treturn text[len(match):], &target\n\t}\n\n\t// check for \"today / tomorrow / next X\"\n\tpatterns := [...]PatternDuration{\n\t\t{pattern: \"today\", d: 0, m: 0, y: 0},\n\t\t{pattern: \"tomorrow\", d: 1, m: 0, y: 0},\n\t\t{pattern: \"next week\", d: 7, m: 0, y: 0},\n\t\t{pattern: \"next month\", d: 0, m: 1, y: 0},\n\t\t{pattern: \"next year\", d: 0, m: 0, y: 1},\n\t}\n\tfor _, pd := range patterns {\n\t\tif strings.HasPrefix(textLower, pd.pattern) && len(text) > len(pd.pattern) {\n\t\t\tdate := now.AddDate(pd.y, pd.m, pd.d)\n\t\t\treturn text[len(pd.pattern):], &date\n\t\t}\n\t}\n\n\t// check for \"next X\" where X is name of a day (monday, etc)\n\tif strings.HasPrefix(textLower, \"next\") {\n\t\tparts := strings.Split(textLower, \" \")\n\t\tif parts[0] == \"next\" && len(parts) > 2 {\n\t\t\tfor i, d := range []string{\"sunday\", \"monday\", \"tuesday\", \"wednesday\", \"thursday\", \"friday\", \"saturday\"} {\n\t\t\t\tif strings.ToLower(parts[1]) == d {\n\t\t\t\t\tdate := now.AddDate(0, 0, int(now.Weekday())+7-i)\n\t\t\t\t\treturn text[len(d)+5:], &date\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// check for YYYY-MM-DD prefix\n\tif len(text) > 10 {\n\t\tdate, err := time.Parse(\"2006-01-02\", text[:10])\n\t\tif err == nil {\n\t\t\treturn text[10:], &date\n\t\t}\n\t}\n\n\t// check for MM-DD prefix\n\tif len(text) > 5 {\n\t\tdate, err := time.Parse(\"2006-01-02\", strconv.FormatInt(int64(now.Year()), 10)+\"-\"+text[:5])\n\t\tif err == nil {\n\t\t\treturn text[5:], &date\n\t\t}\n\t}\n\n\treturn text, nil\n}\n\n// persist writes the todo list to Yaml file\nfunc (widget *Widget) persist() {\n\tconfDir, _ := cfg.WtfConfigDir()\n\tfilePath := fmt.Sprintf(\"%s/%s\", confDir, widget.filePath)\n\n\tfileData, _ := yaml.Marshal(&widget.list)\n\n\terr := os.WriteFile(filePath, fileData, 0644)\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// setItemChecks rolls through the checklist and ensures that all checklist\n// items have the correct checked/unchecked icon per the user's preferences\nfunc (widget *Widget) setItemChecks() {\n\tfor _, item := range widget.list.Items {\n\t\titem.CheckedIcon = widget.settings.checked\n\t\titem.UncheckedIcon = widget.settings.unchecked\n\t}\n}\n\n// updateSelected sets the text of the currently-selected item to the provided text\nfunc (widget *Widget) updateSelected() {\n\tif !widget.isItemSelected() {\n\t\treturn\n\t}\n\n\twidget.processFormInput(\"Edit:\", widget.SelectedItem().EditText(), func(t string) {\n\t\ttext, date, tags := widget.getTextComponents(t)\n\n\t\twidget.updateSelectedItem(text, date, tags)\n\t\tif widget.settings.parseDates {\n\t\t\twidget.Selected = widget.placeItemBasedOnDate(widget.Selected)\n\t\t}\n\t\twidget.persist()\n\t})\n}\n\n// processFormInput is a helper function that creates a form and calls onSave on the received input\nfunc (widget *Widget) processFormInput(prompt string, initValue string, onSave func(string)) {\n\tform := widget.modalForm(prompt, initValue)\n\n\tsaveFctn := func() {\n\t\tonSave(form.GetFormItem(0).(*tview.InputField).GetText())\n\n\t\twidget.pages.RemovePage(\"modal\")\n\t\twidget.tviewApp.SetFocus(widget.View)\n\t\twidget.display()\n\t}\n\n\twidget.addButtons(form, saveFctn)\n\twidget.modalFocus(form)\n\n\t// Tell the app to force redraw the screen\n\twidget.RedrawChan <- true\n}\n\n// updateSelectedItem update the text of the selected item.\nfunc (widget *Widget) updateSelectedItem(text string, date *time.Time, tags []string) {\n\tselectedItem := widget.SelectedItem()\n\tif selectedItem == nil {\n\t\treturn\n\t}\n\n\tselectedItem.Text = text\n\tselectedItem.Date = date\n\tselectedItem.Tags = tags\n}\n\nfunc (widget *Widget) placeItemBasedOnDate(index int) int {\n\t// potentially move todo up\n\tfor index > 0 && widget.todoDateIsEarlier(index, index-1) {\n\t\twidget.list.Swap(index, index-1)\n\t\tindex -= 1\n\t}\n\t// potentially move todo down\n\tfor index < widget.list.Len()-1 && widget.todoDateIsEarlier(index+1, index) {\n\t\twidget.list.Swap(index, index+1)\n\t\tindex += 1\n\t}\n\treturn index\n}\n\nfunc (widget *Widget) todoDateIsEarlier(i, j int) bool {\n\tif widget.list.Items[i].Date == nil && widget.list.Items[j].Date == nil {\n\t\treturn false\n\t}\n\tdefaultVal := getNowDate().AddDate(0, 0, widget.settings.undatedAsDays)\n\tif widget.list.Items[i].Date == nil {\n\t\treturn defaultVal.Before(*widget.list.Items[j].Date)\n\t} else if widget.list.Items[j].Date == nil {\n\t\treturn widget.list.Items[i].Date.Before(defaultVal)\n\t} else {\n\t\treturn widget.list.Items[i].Date.Before(*widget.list.Items[j].Date)\n\t}\n}\n\n/* -------------------- Modal Form -------------------- */\n\nfunc (widget *Widget) addButtons(form *tview.Form, saveFctn func()) {\n\twidget.addSaveButton(form, saveFctn)\n\twidget.addCancelButton(form)\n}\n\nfunc (widget *Widget) addCancelButton(form *tview.Form) {\n\tcancelFn := func() {\n\t\twidget.pages.RemovePage(\"modal\")\n\t\twidget.tviewApp.SetFocus(widget.View)\n\t\twidget.display()\n\t}\n\n\tform.AddButton(\"Cancel\", cancelFn)\n\tform.SetCancelFunc(cancelFn)\n}\n\nfunc (widget *Widget) addSaveButton(form *tview.Form, fctn func()) {\n\tform.AddButton(\"Save\", fctn)\n}\n\nfunc (widget *Widget) modalFocus(form *tview.Form) {\n\tframe := widget.modalFrame(form)\n\twidget.pages.AddPage(\"modal\", frame, false, true)\n\twidget.tviewApp.SetFocus(frame)\n\n\t// Tell the app to force redraw the screen\n\twidget.RedrawChan <- true\n}\n\nfunc (widget *Widget) modalForm(lbl, text string) *tview.Form {\n\tform := tview.NewForm()\n\tform.SetFieldBackgroundColor(wtf.ColorFor(widget.settings.Colors.Background))\n\tform.SetButtonsAlign(tview.AlignCenter)\n\tform.SetButtonTextColor(wtf.ColorFor(widget.settings.Colors.Text))\n\n\tform.AddInputField(lbl, text, 60, nil, nil)\n\n\treturn form\n}\n\nfunc (widget *Widget) modalFrame(form *tview.Form) *tview.Frame {\n\tframe := tview.NewFrame(form)\n\tframe.SetBorders(0, 0, 0, 0, 0, 0)\n\tframe.SetRect(offscreen, offscreen, modalWidth, modalHeight)\n\tframe.SetBorder(true)\n\tframe.SetBorders(1, 1, 0, 0, 1, 1)\n\n\tdrawFunc := func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {\n\t\tw, h := screen.Size()\n\t\tframe.SetRect((w/2)-(width/2), (h/2)-(height/2), width, height)\n\t\treturn x, y, width, height\n\t}\n\n\tframe.SetDrawFunc(drawFunc)\n\n\treturn frame\n}\n"
  },
  {
    "path": "modules/todo_plus/backend/backend.go",
    "content": "package backend\n\nimport (\n\t\"github.com/olebedev/config\"\n)\n\ntype Backend interface {\n\tTitle() string\n\tSetup(*config.Config)\n\tBuildProjects() []*Project\n\tGetProject(string) *Project\n\tLoadTasks(string) ([]Task, error)\n\tCloseTask(*Task) error\n\tDeleteTask(*Task) error\n\tSources() []string\n}\n"
  },
  {
    "path": "modules/todo_plus/backend/project.go",
    "content": "package backend\n\ntype Task struct {\n\tID        string\n\tCompleted bool\n\tName      string\n}\n\ntype Project struct {\n\tID   string\n\tName string\n\n\tIndex   int\n\tTasks   []Task\n\tErr     error\n\tbackend Backend\n}\n\nfunc (proj *Project) IsLast() bool {\n\treturn proj.Index >= len(proj.Tasks)-1\n}\n\nfunc (proj *Project) loadTasks() {\n\tTasks, err := proj.backend.LoadTasks(proj.ID)\n\tproj.Err = err\n\tproj.Tasks = Tasks\n}\n\nfunc (proj *Project) LongestLine() int {\n\tmaxLen := 0\n\n\tfor _, task := range proj.Tasks {\n\t\tif len(task.Name) > maxLen {\n\t\t\tmaxLen = len(task.Name)\n\t\t}\n\t}\n\n\treturn maxLen\n}\n\nfunc (proj *Project) currentTask() *Task {\n\tif proj.Index < 0 {\n\t\treturn nil\n\t}\n\n\treturn &proj.Tasks[proj.Index]\n}\n\nfunc (proj *Project) CloseSelectedTask() {\n\tcurrTask := proj.currentTask()\n\n\tif currTask != nil {\n\t\t_ = proj.backend.CloseTask(currTask)\n\t\tproj.loadTasks()\n\t}\n}\n\nfunc (proj *Project) DeleteSelectedTask() {\n\tcurrTask := proj.currentTask()\n\n\tif currTask != nil {\n\t\t_ = proj.backend.DeleteTask(currTask)\n\n\t\tproj.loadTasks()\n\t}\n}\n"
  },
  {
    "path": "modules/todo_plus/backend/todoist.go",
    "content": "package backend\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gopherlibs/todoist/api\"\n\t\"github.com/olebedev/config\"\n)\n\ntype Todoist struct {\n\tclient   *api.Client\n\tprojects []interface{}\n}\n\nfunc (todo *Todoist) Title() string {\n\treturn \"Todoist\"\n}\n\nfunc (todo *Todoist) Setup(config *config.Config) {\n\n\ttodo.client = api.New(config.UString(\"apiKey\"))\n\ttodo.projects = config.UList(\"projects\")\n}\n\nfunc (todo *Todoist) BuildProjects() []*Project {\n\tprojects := []*Project{}\n\n\tfor _, id := range todo.projects {\n\t\ti := fmt.Sprintf(\"%v\", id)\n\t\tproj := todo.GetProject(i)\n\t\tprojects = append(projects, proj)\n\t}\n\treturn projects\n}\n\nfunc (todo *Todoist) GetProject(id string) *Project {\n\t// Todoist seems to experience a lot of network issues on their side\n\t// If we can't connect, handle it with an empty project until we can\n\tproj := &Project{\n\t\tIndex:   -1,\n\t\tbackend: todo,\n\t}\n\n\tproj.ID = id\n\tproj.Name = \"Error\"\n\n\tp, err := todo.client.Project(id)\n\tif err != nil {\n\t\treturn proj\n\t}\n\n\tproj.Name = p.Name\n\n\ttasks, err := todo.LoadTasks(proj.ID)\n\tproj.Err = err\n\tproj.Tasks = tasks\n\n\treturn proj\n}\n\nfunc toTask(task api.Task) Task {\n\treturn Task{\n\t\tID:        task.ID,\n\t\tCompleted: task.Checked,\n\t\tName:      task.Content,\n\t}\n}\n\nfunc (todo *Todoist) LoadTasks(id string) ([]Task, error) {\n\n\ttasks, err := todo.client.Tasks(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar finalTasks []Task\n\tfor _, item := range tasks.Results {\n\t\tfinalTasks = append(finalTasks, toTask(item))\n\t}\n\treturn finalTasks, nil\n}\n\nfunc (todo *Todoist) CloseTask(task *Task) error {\n\tif task != nil {\n\t\t_, err := todo.client.TaskClose(task.ID)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (todo *Todoist) DeleteTask(task *Task) error {\n\tif task != nil {\n\t\t_, err := todo.client.TaskDelete(task.ID)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (todo *Todoist) Sources() []string {\n\tvar result []string\n\tfor _, id := range todo.projects {\n\t\ti := fmt.Sprintf(\"%v\", id)\n\t\tresult = append(result, i)\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "modules/todo_plus/backend/trello.go",
    "content": "package backend\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/adlio/trello\"\n\t\"github.com/olebedev/config\"\n)\n\ntype Trello struct {\n\tusername  string\n\tboardName string\n\tclient    *trello.Client\n\tboard     string\n\tprojects  []interface{}\n}\n\nfunc (todo *Trello) Title() string {\n\treturn \"Trello\"\n}\n\nfunc (todo *Trello) Setup(config *config.Config) {\n\ttodo.username = config.UString(\"username\")\n\ttodo.boardName = config.UString(\"board\")\n\ttodo.client = trello.NewClient(\n\t\tconfig.UString(\"apiKey\"),\n\t\tconfig.UString(\"accessToken\"),\n\t)\n\tboard, err := getBoardID(todo.client, todo.username, todo.boardName)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\ttodo.board = board\n\ttodo.projects = config.UList(\"lists\")\n}\n\nfunc getBoardID(client *trello.Client, username, boardName string) (string, error) {\n\tmember, err := client.GetMember(username, trello.Defaults())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tboards, err := member.GetBoards(trello.Defaults())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, board := range boards {\n\t\tif board.Name == boardName {\n\t\t\treturn board.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"could not find board with name %s\", boardName)\n}\n\nfunc getListId(client *trello.Client, boardID string, listName string) (string, error) {\n\tboard, err := client.GetBoard(boardID, trello.Defaults())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tboardLists, err := board.GetLists(trello.Defaults())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, list := range boardLists {\n\t\tif list.Name == listName {\n\t\t\treturn list.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\nfunc getCardsOnList(client *trello.Client, listID string) ([]*trello.Card, error) {\n\tlist, err := client.GetList(listID, trello.Defaults())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcards, err := list.GetCards(trello.Defaults())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cards, nil\n}\n\nfunc (todo *Trello) BuildProjects() []*Project {\n\tprojects := []*Project{}\n\n\tfor _, id := range todo.projects {\n\t\tproj := todo.GetProject(id.(string))\n\t\tprojects = append(projects, proj)\n\t}\n\treturn projects\n}\n\nfunc (todo *Trello) GetProject(id string) *Project {\n\tproj := &Project{\n\t\tIndex:   -1,\n\t\tbackend: todo,\n\t}\n\n\tlistId, err := getListId(todo.client, todo.board, id)\n\tif err != nil {\n\t\tproj.Err = err\n\t\treturn proj\n\t}\n\tproj.ID = listId\n\tproj.Name = id\n\n\ttasks, err := todo.LoadTasks(listId)\n\tproj.Err = err\n\tproj.Tasks = tasks\n\n\treturn proj\n}\n\nfunc fromTrello(task *trello.Card) Task {\n\treturn Task{\n\t\tID:        task.ID,\n\t\tCompleted: task.Closed,\n\t\tName:      task.Name,\n\t}\n}\n\nfunc (todo *Trello) LoadTasks(id string) ([]Task, error) {\n\ttasks, err := getCardsOnList(todo.client, id)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar finalTasks []Task\n\tfor _, item := range tasks {\n\t\tfinalTasks = append(finalTasks, fromTrello(item))\n\t}\n\treturn finalTasks, nil\n}\n\nfunc (todo *Trello) CloseTask(task *Task) error {\n\targs := trello.Arguments{\n\t\t\"closed\": \"true\",\n\t}\n\tif task != nil {\n\t\t// Card has an internal client rep which we can't access\n\t\t// Just force a lookup\n\t\tinternal, err := todo.client.GetCard(task.ID, trello.Arguments{})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn internal.Update(args)\n\t}\n\treturn nil\n}\n\nfunc (todo *Trello) DeleteTask(_ *Task) error {\n\treturn nil\n}\n\nfunc (todo *Trello) Sources() []string {\n\tvar result []string\n\tfor _, id := range todo.projects {\n\t\tresult = append(result, id.(string))\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "modules/todo_plus/display.go",
    "content": "package todo_plus\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tproj := widget.CurrentProject()\n\n\tif proj == nil {\n\t\treturn widget.CommonSettings().Title, \"\", false\n\t}\n\n\tif proj.Err != nil {\n\t\treturn widget.CommonSettings().Title, proj.Err.Error(), true\n\t}\n\n\ttitle := fmt.Sprintf(\n\t\t\"[%s]%s[white]\",\n\t\twidget.settings.Colors.Title,\n\t\tproj.Name)\n\n\tstr := \"\"\n\n\tfor idx, item := range proj.Tasks {\n\t\trow := fmt.Sprintf(\n\t\t\t`[%s]| | %s[%s]`,\n\t\t\twidget.RowColor(idx),\n\t\t\ttview.Escape(item.Name),\n\t\t\twidget.RowColor(idx),\n\t\t)\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(item.Name))\n\t}\n\treturn title, str, false\n}\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n"
  },
  {
    "path": "modules/todo_plus/keyboard.go",
    "content": "package todo_plus\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"d\", widget.Delete, \"Delete item\")\n\twidget.SetKeyboardChar(\"j\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"k\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous project\")\n\twidget.SetKeyboardChar(\"c\", widget.Close, \"Close item\")\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next project\")\n\twidget.SetKeyboardChar(\"u\", widget.Unselect, \"Clear selection\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous project\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next project\")\n}\n"
  },
  {
    "path": "modules/todo_plus/settings.go",
    "content": "package todo_plus\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultTitle     = \"Todo\"\n\tdefaultFocusable = true\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tbackendType     string\n\tbackendSettings *config.Config\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tbackend, _ := ymlConfig.Get(\"backendSettings\")\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tbackendType:     ymlConfig.UString(\"backendType\"),\n\t\tbackendSettings: backend,\n\t}\n\n\treturn &settings\n}\n\nfunc FromTodoist(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tapiKey := ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_TODOIST_TOKEN\")))\n\tcfg.ModuleSecret(name, globalConfig, &apiKey).Load()\n\tprojects := ymlConfig.UList(\"projects\")\n\tbackend, _ := config.ParseYaml(\"apiKey: \" + apiKey)\n\t_ = backend.Set(\".projects\", projects)\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tbackendType:     \"todoist\",\n\t\tbackendSettings: backend,\n\t}\n\n\treturn &settings\n}\n\nfunc FromTrello(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\taccessToken := ymlConfig.UString(\"accessToken\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_TRELLO_ACCESS_TOKEN\")))\n\tapiKey := ymlConfig.UString(\"apiKey\", os.Getenv(\"WTF_TRELLO_API_KEY\"))\n\tcfg.ModuleSecret(name, globalConfig, &apiKey).Load()\n\tboard := ymlConfig.UString(\"board\")\n\tusername := ymlConfig.UString(\"username\")\n\tvar lists []interface{}\n\tlist, err := ymlConfig.String(\"list\")\n\tif err == nil {\n\t\tlists = append(lists, list)\n\t} else {\n\t\tlists = ymlConfig.UList(\"list\")\n\t}\n\tbackend, _ := config.ParseYaml(\"apiKey: \" + apiKey)\n\t_ = backend.Set(\".accessToken\", accessToken)\n\t_ = backend.Set(\".board\", board)\n\t_ = backend.Set(\".username\", username)\n\t_ = backend.Set(\".lists\", lists)\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tbackendType:     \"trello\",\n\t\tbackendSettings: backend,\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/todo_plus/widget.go",
    "content": "package todo_plus\n\nimport (\n\t\"log\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/modules/todo_plus/backend\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// A Widget represents a Todoist widget\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.ScrollableWidget\n\n\tprojects []*backend.Project\n\tsettings *Settings\n\tbackend  backend.Backend\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"project\", \"projects\"),\n\t\tScrollableWidget:  view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.backend = getBackend(settings.backendType)\n\twidget.backend.Setup(settings.backendSettings)\n\twidget.CommonSettings().Title = widget.backend.Title()\n\n\twidget.SetRenderFunction(widget.display)\n\twidget.initializeKeyboardControls()\n\twidget.SetDisplayFunction(widget.display)\n\n\treturn &widget\n}\n\nfunc getBackend(backendType string) backend.Backend {\n\tswitch backendType {\n\tcase \"trello\":\n\t\tbackend := &backend.Trello{}\n\t\treturn backend\n\tcase \"todoist\":\n\t\tbackend := &backend.Todoist{}\n\t\treturn backend\n\tdefault:\n\t\tlog.Fatal(backendType + \" is not a supported backend\")\n\t\treturn nil\n\t}\n\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) CurrentProject() *backend.Project {\n\treturn widget.ProjectAt(widget.Idx)\n}\n\nfunc (widget *Widget) ProjectAt(idx int) *backend.Project {\n\tif len(widget.projects) == 0 {\n\t\treturn nil\n\t}\n\n\treturn widget.projects[idx]\n}\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\twidget.projects = widget.backend.BuildProjects()\n\twidget.Sources = widget.backend.Sources()\n\twidget.SetItemCount(len(widget.CurrentProject().Tasks))\n\twidget.display()\n}\n\nfunc (widget *Widget) NextSource() {\n\twidget.MultiSourceWidget.NextSource()\n\twidget.Selected = widget.CurrentProject().Index\n\twidget.SetItemCount(len(widget.CurrentProject().Tasks))\n\twidget.RenderFunction()\n}\n\nfunc (widget *Widget) PrevSource() {\n\twidget.MultiSourceWidget.PrevSource()\n\twidget.Selected = widget.CurrentProject().Index\n\twidget.SetItemCount(len(widget.CurrentProject().Tasks))\n\twidget.RenderFunction()\n}\n\nfunc (widget *Widget) Prev() {\n\twidget.ScrollableWidget.Prev()\n\twidget.CurrentProject().Index = widget.Selected\n}\n\nfunc (widget *Widget) Next() {\n\twidget.ScrollableWidget.Next()\n\twidget.CurrentProject().Index = widget.Selected\n}\n\nfunc (widget *Widget) Unselect() {\n\twidget.ScrollableWidget.Unselect()\n\twidget.CurrentProject().Index = -1\n\twidget.RenderFunction()\n}\n\n/* -------------------- Keyboard Movement -------------------- */\n\n// Close closes the currently-selected task in the currently-selected project\nfunc (w *Widget) Close() {\n\tw.CurrentProject().CloseSelectedTask()\n\tw.SetItemCount(len(w.CurrentProject().Tasks))\n\n\tif w.CurrentProject().IsLast() {\n\t\tw.Prev()\n\t\treturn\n\t}\n\tw.CurrentProject().Index = w.Selected\n\tw.RenderFunction()\n}\n\n// Delete deletes the currently-selected task in the currently-selected project\nfunc (w *Widget) Delete() {\n\tw.CurrentProject().DeleteSelectedTask()\n\tw.SetItemCount(len(w.CurrentProject().Tasks))\n\n\tif w.CurrentProject().IsLast() {\n\t\tw.Prev()\n\t}\n\tw.CurrentProject().Index = w.Selected\n\tw.RenderFunction()\n}\n"
  },
  {
    "path": "modules/transmission/display.go",
    "content": "package transmission\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/hekmon/transmissionrpc/v2\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\twidget.mu.Lock()\n\tdefer widget.mu.Unlock()\n\n\ttitle := widget.CommonSettings().Title\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif len(widget.torrents) == 0 {\n\t\treturn title, \"No data\", false\n\t}\n\n\tstr := \"\"\n\n\tfor idx, torrent := range widget.torrents {\n\t\ttorrName := *torrent.Name\n\n\t\trow := fmt.Sprintf(\n\t\t\t\"[%s] %s %s %s%s[white]\",\n\t\t\twidget.RowColor(idx),\n\t\t\twidget.torrentPercentDone(torrent),\n\t\t\twidget.torrentSeedRatio(torrent),\n\t\t\twidget.torrentState(torrent),\n\t\t\ttview.Escape(widget.prettyTorrentName(torrName)),\n\t\t)\n\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(torrName))\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) prettyTorrentName(name string) string {\n\tstr := strings.ReplaceAll(name, \"[\", \"(\")\n\tstr = strings.ReplaceAll(str, \"]\", \")\")\n\n\treturn str\n}\n\nfunc (widget *Widget) torrentPercentDone(torrent transmissionrpc.Torrent) string {\n\tpctDone := *torrent.PercentDone\n\tstr := fmt.Sprintf(\"%3d%%↓\", int(pctDone*100))\n\n\tswitch pctDone {\n\tcase 0.0:\n\t\tstr = \"[gray::b]\" + str\n\tcase 1.0:\n\t\tstr = \"[green::b]\" + str\n\tdefault:\n\t\tstr = \"[lightblue::b]\" + str\n\t}\n\n\treturn str + \"[white]\"\n}\n\nfunc (widget *Widget) torrentSeedRatio(torrent transmissionrpc.Torrent) string {\n\tseedRatio := *torrent.UploadRatio\n\n\tif seedRatio < 0 {\n\t\tseedRatio = 0\n\t}\n\n\treturn fmt.Sprintf(\"[green]%3d%%↑\", int(seedRatio*100))\n}\n\nfunc (widget *Widget) torrentState(torrent transmissionrpc.Torrent) string {\n\tstr := \"\"\n\n\tswitch *torrent.Status {\n\tcase transmissionrpc.TorrentStatusStopped:\n\t\tstr += \"[gray]\"\n\tcase transmissionrpc.TorrentStatusDownload:\n\t\tstr += \"[lightblue]\"\n\tcase transmissionrpc.TorrentStatusSeed:\n\t\tstr += \"[green]\"\n\t}\n\n\treturn str\n}\n"
  },
  {
    "path": "modules/transmission/keyboard.go",
    "content": "package transmission\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(nil)\n\n\twidget.SetKeyboardChar(\"j\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"k\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"u\", widget.Unselect, \"Clear selection\")\n\n\twidget.SetKeyboardKey(tcell.KeyCtrlD, widget.deleteSelectedTorrent, \"Delete the selected torrent\")\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.pauseUnpauseTorrent, \"Pause/unpause torrent\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n}\n"
  },
  {
    "path": "modules/transmission/settings.go",
    "content": "package transmission\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Transmission\"\n)\n\n// Settings defines the configuration properties for this module\ntype Settings struct {\n\t*cfg.Common\n\n\thost         string `help:\"The address of the machine the Transmission daemon is running on\"`\n\thttps        bool   `help:\"Whether or not to connect to the host via HTTPS\"`\n\tpassword     string `help:\"The password for the Transmission user\"`\n\tport         uint16 `help:\"The port to connect to the Transmission daemon on\"`\n\turl          string `help:\"The RPC URI that the daemon is accessible at\"`\n\tusername     string `help:\"The username of the Transmission user\"`\n\thideComplete bool   `help:\"Hide the torrents that are finished downloading\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\thost:         ymlConfig.UString(\"host\"),\n\t\thttps:        ymlConfig.UBool(\"https\", false),\n\t\tpassword:     ymlConfig.UString(\"password\"),\n\t\tport:         uint16(ymlConfig.UInt(\"port\", 9091)),\n\t\turl:          ymlConfig.UString(\"url\", \"\"),\n\t\tusername:     ymlConfig.UString(\"username\", \"\"),\n\t\thideComplete: ymlConfig.UBool(\"hideComplete\", false),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/transmission/widget.go",
    "content": "package transmission\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sync\"\n\n\t\"github.com/hekmon/transmissionrpc/v2\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget is the container for transmission data\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tclient   *transmissionrpc.Client\n\tsettings *Settings\n\tmu       sync.Mutex\n\ttorrents []transmissionrpc.Torrent\n\terr      error\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.display)\n\twidget.initializeKeyboardControls()\n\n\tgo buildClient(&widget)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Fetch retrieves torrent data from the Transmission daemon\nfunc (widget *Widget) Fetch() ([]transmissionrpc.Torrent, error) {\n\tif widget.client == nil {\n\t\treturn nil, errors.New(\"client was not initialized\")\n\t}\n\n\ttorrents, err := widget.client.TorrentGetAll(context.Background())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tout := make([]transmissionrpc.Torrent, 0)\n\tfor _, torrent := range torrents {\n\t\tif widget.settings.hideComplete {\n\t\t\tif *torrent.PercentDone == 1.0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tout = append(out, torrent)\n\t}\n\n\treturn out, nil\n}\n\n// Refresh updates the data for this widget and displays it onscreen\nfunc (widget *Widget) Refresh() {\n\ttorrents, err := widget.Fetch()\n\tcount := 0\n\n\tif err == nil {\n\t\tcount = len(torrents)\n\t}\n\n\twidget.mu.Lock()\n\twidget.err = err\n\twidget.torrents = torrents\n\twidget.SetItemCount(count)\n\twidget.mu.Unlock()\n\n\twidget.display()\n}\n\n// Next selects the next item in the list\nfunc (widget *Widget) Next() {\n\twidget.ScrollableWidget.Next()\n}\n\n// Prev selects the previous item in the list\nfunc (widget *Widget) Prev() {\n\twidget.ScrollableWidget.Prev()\n}\n\n// Unselect clears the selection of list items\nfunc (widget *Widget) Unselect() {\n\twidget.ScrollableWidget.Unselect()\n\twidget.RenderFunction()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// buildClient creates a persisten transmission client\nfunc buildClient(widget *Widget) {\n\twidget.mu.Lock()\n\tdefer widget.mu.Unlock()\n\n\tclient, err := transmissionrpc.New(widget.settings.host, widget.settings.username, widget.settings.password,\n\t\t&transmissionrpc.AdvancedConfig{\n\t\t\tPort:   widget.settings.port,\n\t\t\tRPCURI: widget.settings.url,\n\t\t\tHTTPS:  widget.settings.https,\n\t\t})\n\tif err != nil {\n\t\tclient = nil\n\t}\n\n\twidget.client = client\n}\n\nfunc (widget *Widget) currentTorrent() *transmissionrpc.Torrent {\n\tif len(widget.torrents) == 0 {\n\t\treturn nil\n\t}\n\n\tif len(widget.torrents) <= widget.Selected {\n\t\treturn nil\n\t}\n\n\treturn &widget.torrents[widget.Selected]\n}\n\n// deleteSelected removes the selected torrent from transmission\n// This action is non-destructive, it does not delete the files on the host\nfunc (widget *Widget) deleteSelectedTorrent() {\n\tif widget.client == nil {\n\t\treturn\n\t}\n\n\tcurrTorrent := widget.currentTorrent()\n\tif currTorrent == nil {\n\t\treturn\n\t}\n\n\tids := []int64{*currTorrent.ID}\n\n\tremovePayload := transmissionrpc.TorrentRemovePayload{\n\t\tIDs:             ids,\n\t\tDeleteLocalData: false,\n\t}\n\n\terr := widget.client.TorrentRemove(context.Background(), removePayload)\n\tif err != nil {\n\t\treturn\n\t}\n\n\twidget.display()\n}\n\n// pauseUnpauseTorrent either pauses or unpauses the downloading and seeding of the selected torrent\nfunc (widget *Widget) pauseUnpauseTorrent() {\n\tif widget.client == nil {\n\t\treturn\n\t}\n\n\tcurrTorrent := widget.currentTorrent()\n\tif currTorrent == nil {\n\t\treturn\n\t}\n\n\tids := []int64{*currTorrent.ID}\n\n\tvar err error\n\tif *currTorrent.Status == transmissionrpc.TorrentStatusStopped {\n\t\terr = widget.client.TorrentStartIDs(context.Background(), ids)\n\t} else {\n\t\terr = widget.client.TorrentStopIDs(context.Background(), ids)\n\t}\n\n\tif err != nil {\n\t\treturn\n\t}\n\n\twidget.display()\n}\n"
  },
  {
    "path": "modules/travisci/client.go",
    "content": "package travisci\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nvar TRAVIS_HOSTS = map[bool]string{\n\tfalse: \"travis-ci.org\",\n\ttrue:  \"travis-ci.com\",\n}\n\nfunc BuildsFor(settings *Settings) (*Builds, error) {\n\tbuilds := &Builds{}\n\n\ttravisAPIURL.Host = \"api.\" + TRAVIS_HOSTS[settings.pro]\n\tif settings.baseURL != \"\" {\n\t\ttravisAPIURL.Host = settings.baseURL\n\t}\n\n\tresp, err := travisBuildRequest(settings)\n\tif err != nil {\n\t\treturn builds, err\n\t}\n\n\terr = utils.ParseJSON(&builds, resp.Body)\n\tif err != nil {\n\t\treturn builds, err\n\t}\n\n\treturn builds, nil\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nvar (\n\ttravisAPIURL = &url.URL{Scheme: \"https\", Path: \"/\"}\n)\n\nfunc travisBuildRequest(settings *Settings) (*http.Response, error) {\n\n\tpath := \"builds\"\n\tif settings.baseURL != \"\" {\n\t\ttravisAPIURL.Path = \"/api/\"\n\t}\n\tparams := url.Values{}\n\tparams.Add(\"limit\", settings.limit)\n\tparams.Add(\"sort_by\", settings.sort_by)\n\n\trequestUrl := travisAPIURL.ResolveReference(&url.URL{Path: path, RawQuery: params.Encode()})\n\n\treq, err := http.NewRequest(\"GET\", requestUrl.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Header.Add(\"Travis-API-Version\", \"3\")\n\n\tbearer := fmt.Sprintf(\"token %s\", settings.apiKey)\n\treq.Header.Add(\"Authorization\", bearer)\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "modules/travisci/keyboard.go",
    "content": "package travisci\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openBuild, \"Open item in browser\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openBuild, \"Open item in browser\")\n}\n"
  },
  {
    "path": "modules/travisci/settings.go",
    "content": "package travisci\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"TravisCI\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey  string\n\tbaseURL string `help:\"Your TravisCI Enterprise API URL.\" optional:\"true\"`\n\tcompact bool\n\tlimit   string\n\tpro     bool\n\tsort_by string\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:  ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_TRAVIS_API_TOKEN\"))),\n\t\tbaseURL: ymlConfig.UString(\"baseURL\", ymlConfig.UString(\"baseURL\", os.Getenv(\"WTF_TRAVIS_BASE_URL\"))),\n\t\tpro:     ymlConfig.UBool(\"pro\", false),\n\t\tcompact: ymlConfig.UBool(\"compact\", false),\n\t\tlimit:   ymlConfig.UString(\"limit\", \"10\"),\n\t\tsort_by: ymlConfig.UString(\"sort_by\", \"id:desc\"),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).\n\t\tService(settings.baseURL).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/travisci/travis.go",
    "content": "package travisci\n\ntype Builds struct {\n\tBuilds []Build `json:\"builds\"`\n}\n\ntype Build struct {\n\tID         int        `json:\"id\"`\n\tCreatedBy  Owner      `json:\"created_by\"`\n\tBranch     Branch     `json:\"branch\"`\n\tNumber     string     `json:\"number\"`\n\tRepository Repository `json:\"repository\"`\n\tCommit     Commit     `json:\"commit\"`\n\tState      string     `json:\"state\"`\n}\n\ntype Owner struct {\n\tLogin string `json:\"login\"`\n}\n\ntype Branch struct {\n\tName string `json:\"name\"`\n}\n\ntype Repository struct {\n\tName string `json:\"name\"`\n\tSlug string `json:\"slug\"`\n}\n\ntype Commit struct {\n\tMessage string `json:\"message\"`\n}\n"
  },
  {
    "path": "modules/travisci/widget.go",
    "content": "package travisci\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tbuilds   *Builds\n\tsettings *Settings\n\terr      error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\tbuilds, err := BuildsFor(widget.settings)\n\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.builds = nil\n\t\twidget.SetItemCount(0)\n\t} else {\n\t\twidget.err = nil\n\t\twidget.builds = builds\n\t\twidget.SetItemCount(len(builds.Builds))\n\t}\n\twidget.Render()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := fmt.Sprintf(\"%s - Builds\", widget.CommonSettings().Title)\n\tvar str string\n\tif widget.err != nil {\n\t\tstr = widget.err.Error()\n\t} else {\n\t\tvar rowFormat = \"[%s] [%s] %s-%s (%s) [%s]%s - [blue]%s\"\n\t\tif !widget.settings.compact {\n\t\t\trowFormat += \"\\n\"\n\t\t}\n\n\t\tfor idx, build := range widget.builds.Builds {\n\t\t\trow := fmt.Sprintf(\n\t\t\t\trowFormat,\n\t\t\t\twidget.RowColor(idx),\n\t\t\t\tbuildColor(build),\n\t\t\t\tbuild.Repository.Name,\n\t\t\t\tbuild.Number,\n\t\t\t\tbuild.Branch.Name,\n\t\t\t\twidget.RowColor(idx),\n\t\t\t\tstrings.Split(build.Commit.Message, \"\\n\")[0],\n\t\t\t\tbuild.CreatedBy.Login,\n\t\t\t)\n\t\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(build.Branch.Name))\n\t\t}\n\t}\n\n\treturn title, str, false\n}\n\nfunc buildColor(build Build) string {\n\tswitch build.State {\n\tcase \"broken\":\n\t\treturn \"red\"\n\tcase \"failed\":\n\t\treturn \"red\"\n\tcase \"failing\":\n\t\treturn \"red\"\n\tcase \"pending\":\n\t\treturn \"yellow\"\n\tcase \"started\":\n\t\treturn \"yellow\"\n\tcase \"fixed\":\n\t\treturn \"green\"\n\tcase \"passed\":\n\t\treturn \"green\"\n\tdefault:\n\t\treturn \"white\"\n\t}\n}\n\nfunc (widget *Widget) openBuild() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.builds != nil && sel < len(widget.builds.Builds) {\n\t\tbuild := &widget.builds.Builds[sel]\n\t\ttravisHost := TRAVIS_HOSTS[widget.settings.pro]\n\t\tutils.OpenFile(fmt.Sprintf(\"https://%s/%s/%s/%d\", travisHost, build.Repository.Slug, \"builds\", build.ID))\n\t}\n}\n"
  },
  {
    "path": "modules/twitch/client.go",
    "content": "package twitch\n\nimport (\n\thelix \"github.com/nicklaw5/helix/v2\"\n)\n\ntype Twitch struct {\n\tclient           *helix.Client\n\tUserRefreshToken string\n\tUserID           string\n\tStreams          string\n}\n\ntype ClientOpts struct {\n\tClientID         string\n\tClientSecret     string\n\tAppAccessToken   string\n\tUserAccessToken  string\n\tUserRefreshToken string\n\tRedirectURI      string\n\tStreams          string\n\tUserID           string\n}\n\nfunc NewClient(opts *ClientOpts) (*Twitch, error) {\n\tclient, err := helix.NewClient(&helix.Options{\n\t\tClientID:       opts.ClientID,\n\t\tClientSecret:   opts.ClientSecret,\n\t\tAppAccessToken: opts.AppAccessToken,\n\t\tRedirectURI:    opts.RedirectURI,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Only set user access token if user has selected followed streams. Otherwise it will supercede app access token.\n\t// https://github.com/nicklaw5/helix/pull/131 Seems like it should be fixed in this PR of the helix API, but it hasnt been merged for a long time.\n\tif opts.Streams == \"followed\" {\n\t\tclient.SetUserAccessToken(opts.UserAccessToken)\n\t}\n\n\tt := &Twitch{client: client}\n\tt.UserRefreshToken = opts.UserRefreshToken\n\tt.UserID = opts.UserID\n\tt.Streams = opts.Streams\n\tif opts.AppAccessToken == \"\" && opts.ClientSecret != \"\" {\n\t\tif err := t.RefreshOAuthToken(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn t, nil\n}\n\nfunc (t *Twitch) RefreshOAuthToken() error {\n\n\tswitch t.Streams {\n\tcase \"followed\":\n\t\tuserResp, err := t.client.RefreshUserAccessToken(t.UserRefreshToken)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tt.client.SetUserAccessToken(userResp.Data.AccessToken)\n\t\tt.UserRefreshToken = userResp.Data.RefreshToken\n\tcase \"top\":\n\t\tappResp, err := t.client.RequestAppAccessToken([]string{})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tt.client.SetAppAccessToken(appResp.Data.AccessToken)\n\t}\n\n\treturn nil\n}\n\nfunc (t *Twitch) TopStreams(params *helix.StreamsParams) (*helix.StreamsResponse, error) {\n\tif params == nil {\n\t\tparams = &helix.StreamsParams{}\n\t}\n\treturn t.client.GetStreams(params)\n}\n\nfunc (t *Twitch) FollowedStreams(params *helix.FollowedStreamsParams) (*helix.StreamsResponse, error) {\n\tif params == nil {\n\t\tparams = &helix.FollowedStreamsParams{}\n\t}\n\treturn t.client.GetFollowedStream(params)\n}\n"
  },
  {
    "path": "modules/twitch/keyboard.go",
    "content": "package twitch\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openTwitch, \"Open target URL in browser\")\n\twidget.SetKeyboardChar(\"s\", widget.openStreamlink, \"Open target stream via streamlink (github.com/streamlink/streamlink)\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openTwitch, \"Open stream in browser\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n}\n"
  },
  {
    "path": "modules/twitch/settings.go",
    "content": "package twitch\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = true\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tnumberOfResults  int      `help:\"Number of results to show. Default is 10.\" optional:\"true\"`\n\tclientId         string   `help:\"Client Id (default is env var TWITCH_CLIENT_ID)\"`\n\tclientSecret     string   `help:\"Client secret (default is env var TWITCH_CLIENT_SECRET)\"`\n\tappAccessToken   string   `help:\"App access token (default is env var TWITCH_APP_ACCESS_TOKEN)\"`\n\tuserAccessToken  string   `help:\"User access token (default is env var TWITCH_USER_ACCESS_TOKEN)\"`\n\tuserRefreshToken string   `help:\"User refresh token (default is env var TWITCH_USER_REFRESH_TOKEN)\"`\n\tstreams          string   `help:\"Which streams to display. Options: 'top' and 'followed'. Followed requires user access token, user refresh token and user id. Defaults to top.\"`\n\tuserId           string   `help:\"Your twitch user ID\"`\n\tredirectURI      string   `help:\"The redirect URI of your twitch app, mandatory if you wish to see followed streams (default is env var TWITCH_REDIRECT_URI)\"`\n\tlanguages        []string `help:\"Stream languages\" optional:\"true\"`\n\tgameIds          []string `help:\"Twitch Game IDs\" optional:\"true\"`\n\tstreamType       string   `help:\"Type of stream 'live' (default), 'all', 'vodcast'\" optional:\"true\"`\n\tuserIds          []string `help:\"Twitch user ids\" optional:\"true\"`\n\tuserLogins       []string `help:\"Twitch user names\" optional:\"true\"`\n}\n\nfunc defaultLanguage() []interface{} {\n\tvar defaults []interface{}\n\tdefaults = append(defaults, \"en\")\n\treturn defaults\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\ttwitch := ymlConfig.UString(\"twitch\")\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, twitch, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tnumberOfResults:  ymlConfig.UInt(\"numberOfResults\", 10),\n\t\tclientId:         ymlConfig.UString(\"clientId\", os.Getenv(\"TWITCH_CLIENT_ID\")),\n\t\tclientSecret:     ymlConfig.UString(\"clientSecret\", os.Getenv(\"TWITCH_CLIENT_SECRET\")),\n\t\tappAccessToken:   ymlConfig.UString(\"appAccessToken\", os.Getenv(\"TWITCH_APP_ACCESS_TOKEN\")),\n\t\tuserAccessToken:  ymlConfig.UString(\"userAccessToken\", os.Getenv(\"TWITCH_USER_ACCESS_TOKEN\")),\n\t\tuserRefreshToken: ymlConfig.UString(\"userRefreshToken\", os.Getenv(\"TWITCH_USER_REFRESH_TOKEN\")),\n\t\tstreams:          ymlConfig.UString(\"streams\", \"top\"),\n\t\tuserId:           ymlConfig.UString(\"userId\", \"\"),\n\t\tredirectURI:      ymlConfig.UString(\"redirectURI\", os.Getenv(\"TWITCH_REDIRECT_URI\")),\n\t\tlanguages:        utils.ToStrs(ymlConfig.UList(\"languages\", defaultLanguage())),\n\t\tstreamType:       ymlConfig.UString(\"streamType\", \"live\"),\n\t\tgameIds:          utils.ToStrs(ymlConfig.UList(\"gameIds\", make([]interface{}, 0))),\n\t\tuserIds:          utils.ToStrs(ymlConfig.UList(\"userIds\", make([]interface{}, 0))),\n\t\tuserLogins:       utils.ToStrs(ymlConfig.UList(\"userLogins\", make([]interface{}, 0))),\n\t}\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/twitch/widget.go",
    "content": "package twitch\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os/exec\"\n\n\thelix \"github.com/nicklaw5/helix/v2\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tsettings   *Settings\n\terr        error\n\ttwitch     *Twitch\n\ttopStreams []*Stream\n}\n\ntype Stream struct {\n\tStreamer    string\n\tViewerCount int\n\tLanguage    string\n\tGameID      string\n\tTitle       string\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\tclientOpts := &ClientOpts{\n\t\tClientID:         settings.clientId,\n\t\tClientSecret:     settings.clientSecret,\n\t\tAppAccessToken:   settings.appAccessToken,\n\t\tUserAccessToken:  settings.userAccessToken,\n\t\tUserRefreshToken: settings.userRefreshToken,\n\t\tRedirectURI:      settings.redirectURI,\n\t\tStreams:          settings.streams,\n\t\tUserID:           settings.userId,\n\t}\n\n\ttwitchClient, err := NewClient(clientOpts)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\t\tsettings:         settings,\n\t\ttwitch:           twitchClient,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\nfunc (widget *Widget) Refresh() {\n\tvar err error\n\tvar response *helix.StreamsResponse\n\t// Refresh the auth token on each refresh to be sure we aren't using an expired one.\n\tif err = widget.twitch.RefreshOAuthToken(); err != nil {\n\t\thandleError(widget, err)\n\t}\n\n\tswitch widget.twitch.Streams {\n\tcase \"followed\":\n\t\tresponse, err = widget.twitch.FollowedStreams(&helix.FollowedStreamsParams{\n\t\t\tUserID: widget.twitch.UserID,\n\t\t})\n\tcase \"top\":\n\t\tresponse, err = widget.twitch.TopStreams(&helix.StreamsParams{\n\t\t\tFirst:      widget.settings.numberOfResults,\n\t\t\tGameIDs:    widget.settings.gameIds,\n\t\t\tLanguage:   widget.settings.languages,\n\t\t\tType:       widget.settings.streamType,\n\t\t\tUserIDs:    widget.settings.userIds,\n\t\t\tUserLogins: widget.settings.userLogins,\n\t\t})\n\t}\n\n\tif err != nil {\n\t\thandleError(widget, err)\n\t} else if response.ErrorMessage != \"\" {\n\t\thandleError(widget, errors.New(response.ErrorMessage))\n\t} else {\n\t\tstreams := makeStreams(response)\n\t\twidget.topStreams = streams\n\t\twidget.err = nil\n\t\tif len(streams) <= widget.settings.numberOfResults {\n\t\t\twidget.SetItemCount(len(widget.topStreams))\n\t\t} else {\n\t\t\twidget.topStreams = streams[:widget.settings.numberOfResults]\n\t\t\twidget.SetItemCount(len(widget.topStreams))\n\t\t}\n\t}\n\n\twidget.Render()\n}\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\nfunc makeStreams(response *helix.StreamsResponse) []*Stream {\n\tstreams := make([]*Stream, len(response.Data.Streams))\n\tfor i, b := range response.Data.Streams {\n\t\tstreams[i] = &Stream{\n\t\t\tb.UserName,\n\t\t\tb.ViewerCount,\n\t\t\tb.Language,\n\t\t\tb.GameID,\n\t\t\tb.Title,\n\t\t}\n\t}\n\treturn streams\n}\n\nfunc handleError(widget *Widget, err error) {\n\twidget.err = err\n\twidget.topStreams = nil\n\twidget.SetItemCount(0)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tvar title = \"Twitch Streams\"\n\tif widget.CommonSettings().Title != \"\" {\n\t\ttitle = widget.CommonSettings().Title\n\t}\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\tif len(widget.topStreams) == 0 {\n\t\treturn title, \"No data\", false\n\t}\n\tvar str string\n\n\tlocPrinter, _ := widget.settings.LocalizedPrinter()\n\n\tfor idx, stream := range widget.topStreams {\n\t\trow := fmt.Sprintf(\n\t\t\t\"[%s]%2d. [red]%s [white]%s - %s\",\n\t\t\twidget.RowColor(idx),\n\t\t\tidx+1,\n\t\t\tutils.PrettyNumber(locPrinter, float64(stream.ViewerCount)),\n\t\t\tstream.Streamer,\n\t\t\tstream.Title,\n\t\t)\n\t\tstr += utils.HighlightableHelper(widget.View, row, idx, len(stream.Streamer))\n\t}\n\n\treturn title, str, false\n}\n\n// Opens stream in the browser\nfunc (widget *Widget) openTwitch() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.topStreams != nil && sel < len(widget.topStreams) {\n\t\tstream := widget.topStreams[sel]\n\t\tfullLink := \"https://twitch.com/\" + stream.Streamer\n\t\tutils.OpenFile(fullLink)\n\t}\n}\n\nfunc (widget *Widget) openStreamlink() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.topStreams != nil && sel < len(widget.topStreams) {\n\t\tstream := widget.topStreams[sel]\n\t\tfullLink := \"https://twitch.tv/\" + stream.Streamer\n\t\tcmd := exec.Command(\"streamlink\", fullLink, \"best\")\n\t\terr := cmd.Start()\n\t\tif err != nil {\n\t\t\thandleError(widget, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "modules/twitter/client.go",
    "content": "package twitter\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/clientcredentials\"\n)\n\n/* NOTE: Currently single application ONLY\n* bearer tokens are only supported for applications, not single-users\n */\n\n// Client represents the data required to connect to the Twitter API\ntype Client struct {\n\tapiBase    string\n\tcount      int\n\tscreenName string\n\thttpClient *http.Client\n}\n\n// NewClient creates and returns a new Twitter client\nfunc NewClient(settings *Settings) *Client {\n\tvar httpClient *http.Client\n\t// If a bearer token is supplied, use that directly.  Otherwise, let the Oauth client fetch a token\n\t// using the consumer key and secret.\n\tif settings.bearerToken == \"\" {\n\t\tconf := &clientcredentials.Config{\n\t\t\tClientID:     settings.consumerKey,\n\t\t\tClientSecret: settings.consumerSecret,\n\t\t\tTokenURL:     \"https://api.twitter.com/oauth2/token\",\n\t\t}\n\t\thttpClient = conf.Client(context.Background())\n\t} else {\n\t\tctx := context.Background()\n\t\thttpClient = oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{\n\t\t\tAccessToken: settings.bearerToken,\n\t\t\tTokenType:   \"Bearer\",\n\t\t}))\n\t}\n\n\tclient := Client{\n\t\tapiBase:    \"https://api.twitter.com/1.1/\",\n\t\tcount:      settings.count,\n\t\tscreenName: \"\",\n\t\thttpClient: httpClient,\n\t}\n\n\treturn &client\n}\n\n/* -------------------- Public Functions -------------------- */\n\n// Tweets returns a list of tweets of a user\nfunc (client *Client) Tweets() []Tweet {\n\ttweets, err := client.getTweets()\n\tif err != nil {\n\t\treturn []Tweet{}\n\t}\n\n\treturn tweets\n}\n\n/* -------------------- Private Functions -------------------- */\n\n// tweets is the private interface for retrieving the list of user tweets\nfunc (client *Client) getTweets() (tweets []Tweet, err error) {\n\tapiURL := fmt.Sprintf(\n\t\t\"%s/statuses/user_timeline.json?screen_name=%s&count=%s\",\n\t\tclient.apiBase,\n\t\tclient.screenName,\n\t\tstrconv.Itoa(client.count),\n\t)\n\n\tdata, err := Request(client.httpClient, apiURL)\n\tif err != nil {\n\t\treturn tweets, err\n\t}\n\terr = json.Unmarshal(data, &tweets)\n\n\treturn\n}\n"
  },
  {
    "path": "modules/twitter/keyboard.go",
    "content": "package twitter\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardChar(\"o\", widget.openFile, \"Open source\")\n\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next source\")\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous source\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openFile, \"Open source\")\n}\n\nfunc (widget *Widget) openFile() {\n\tsrc := widget.currentSourceURI()\n\tutils.OpenFile(src)\n}\n"
  },
  {
    "path": "modules/twitter/request.go",
    "content": "package twitter\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n)\n\nfunc Request(httpClient *http.Client, apiURL string) ([]byte, error) {\n\tresp, err := httpClient.Get(apiURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tdata, err := ParseBody(resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data, err\n}\n\nfunc ParseBody(resp *http.Response) ([]byte, error) {\n\tvar buffer bytes.Buffer\n\t_, err := buffer.ReadFrom(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn buffer.Bytes(), nil\n}\n"
  },
  {
    "path": "modules/twitter/settings.go",
    "content": "package twitter\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Twitter\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tbearerToken    string\n\tconsumerKey    string\n\tconsumerSecret string\n\tcount          int\n\tscreenNames    []interface{}\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tbearerToken:    ymlConfig.UString(\"bearerToken\", os.Getenv(\"WTF_TWITTER_BEARER_TOKEN\")),\n\t\tconsumerKey:    ymlConfig.UString(\"consumerKey\", os.Getenv(\"WTF_TWITTER_CONSUMER_KEY\")),\n\t\tconsumerSecret: ymlConfig.UString(\"consumerSecret\", os.Getenv(\"WTF_TWITTER_CONSUMER_SECRET\")),\n\t\tcount:          ymlConfig.UInt(\"count\", 5),\n\t\tscreenNames:    ymlConfig.UList(\"screenName\"),\n\t}\n\n\tsettings.SetDocumentationPath(\"twitter/tweets\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/twitter/tweet.go",
    "content": "package twitter\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\ntype Tweet struct {\n\tUser      User   `json:\"user\"`\n\tText      string `json:\"text\"`\n\tCreatedAt string `json:\"created_at\"`\n}\n\nfunc (tweet *Tweet) String() string {\n\treturn fmt.Sprintf(\"Tweet: %s at %s by %s\", tweet.Text, tweet.CreatedAt, tweet.User.ScreenName)\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (tweet *Tweet) Username() string {\n\treturn tweet.User.ScreenName\n}\n\nfunc (tweet *Tweet) Created() time.Time {\n\tnewTime, _ := time.Parse(time.RubyDate, tweet.CreatedAt)\n\treturn newTime\n}\n\nfunc (tweet *Tweet) PrettyCreatedAt() string {\n\tnewTime := tweet.Created()\n\treturn fmt.Sprint(newTime.Format(\"Jan 2, 2006\"))\n}\n"
  },
  {
    "path": "modules/twitter/user.go",
    "content": "package twitter\n\n// User is used as part of the Tweet struct to get user information\ntype User struct {\n\tScreenName string `json:\"screen_name\"`\n}\n"
  },
  {
    "path": "modules/twitter/widget.go",
    "content": "package twitter\n\nimport (\n\t\"fmt\"\n\t\"html\"\n\t\"regexp\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\tclient   *Client\n\tidx      int\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"screenName\", \"screenNames\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tidx:      0,\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\n\twidget.SetDisplayFunction(widget.Refresh)\n\n\twidget.client = NewClient(settings)\n\n\twidget.View.SetBorderPadding(1, 1, 1, 1)\n\twidget.View.SetWrap(true)\n\twidget.View.SetWordWrap(true)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh is called on the interval and refreshes the data\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\twidget.client.screenName = widget.CurrentSource()\n\ttweets := widget.client.Tweets()\n\n\ttitle := fmt.Sprintf(\"Twitter - [green]@%s[white]\", widget.CurrentSource())\n\n\tif len(tweets) == 0 {\n\t\tstr := fmt.Sprintf(\"\\n\\n\\n%s\", utils.CenterText(\"[lightblue]No Tweets[white]\", 50))\n\t\treturn title, str, true\n\t}\n\n\t_, _, width, _ := widget.View.GetRect()\n\tstr := widget.settings.PaginationMarker(len(widget.Sources), widget.Idx, width-2) + \"\\n\"\n\tfor _, tweet := range tweets {\n\t\tstr += widget.format(tweet)\n\t}\n\n\treturn title, str, true\n}\n\n// If the tweet's Username is the same as the account we're watching, no\n// need to display the username\nfunc (widget *Widget) displayName(tweet Tweet) string {\n\tif widget.CurrentSource() == tweet.User.ScreenName {\n\t\treturn \"\"\n\t}\n\treturn tweet.User.ScreenName\n}\n\nfunc (widget *Widget) formatText(text string) string {\n\tresult := text\n\n\t// Convert HTML entities\n\tresult = html.UnescapeString(result)\n\n\t// RT indicator\n\trtRegExp := regexp.MustCompile(`^RT`)\n\tresult = rtRegExp.ReplaceAllString(result, \"[olive]${0}[white::-]\")\n\n\t// @name mentions\n\tatRegExp := regexp.MustCompile(`@[0-9A-Za-z_]*`)\n\tresult = atRegExp.ReplaceAllString(result, \"[lightblue]${0}[white]\")\n\n\t// HTTP(S) links\n\tlinkRegExp := regexp.MustCompile(`http[s:\\/.0-9A-Za-z]*`)\n\tresult = linkRegExp.ReplaceAllString(result, \"[lightblue::u]${0}[white::-]\")\n\n\t// Hash tags\n\thashRegExp := regexp.MustCompile(`#[0-9A-Za-z_]*`)\n\tresult = hashRegExp.ReplaceAllString(result, \"[yellow]${0}[white]\")\n\n\treturn result\n}\n\nfunc (widget *Widget) format(tweet Tweet) string {\n\tbody := widget.formatText(tweet.Text)\n\tname := widget.displayName(tweet)\n\n\tvar attribution string\n\tif name == \"\" {\n\t\tattribution = humanize.Time(tweet.Created())\n\t} else {\n\t\tattribution = fmt.Sprintf(\n\t\t\t\"%s, %s\",\n\t\t\tname,\n\t\t\thumanize.Time(tweet.Created()),\n\t\t)\n\t}\n\n\treturn fmt.Sprintf(\"%s\\n[grey]%s[white]\\n\\n\", body, attribution)\n}\nfunc (widget *Widget) currentSourceURI() string {\n\n\tsrc := \"https://twitter.com/\" + widget.CurrentSource()\n\treturn src\n}\n"
  },
  {
    "path": "modules/twitterstats/client.go",
    "content": "package twitterstats\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/clientcredentials\"\n)\n\n// Client contains state that allows stats to be fetched about a list of Twitter users\ntype Client struct {\n\thttpClient  *http.Client\n\tscreenNames []string\n}\n\n// TwitterStats Represents a stats snapshot for a single Twitter user at a point in time\ntype TwitterStats struct {\n\tFollowerCount int64 `json:\"followers_count\"`\n\tTweetCount    int64 `json:\"statuses_count\"`\n}\n\nconst (\n\tuserTimelineURL = \"https://api.twitter.com/1.1/users/show.json\"\n)\n\n// NewClient creates a new twitterstats client that contains an OAuth2 HTTP client which can be used\nfunc NewClient(settings *Settings) *Client {\n\tusernames := make([]string, len(settings.screenNames))\n\tfor i, username := range settings.screenNames {\n\t\tvar ok bool\n\t\tif usernames[i], ok = username.(string); !ok {\n\t\t\tlog.Fatalf(\"All `screenName`s in twitterstats config must be of type string\")\n\t\t}\n\t}\n\n\tvar httpClient *http.Client\n\t// If a bearer token is supplied, use that directly.  Otherwise, let the Oauth client fetch a token\n\t// using the consumer key and secret.\n\tif settings.bearerToken == \"\" {\n\t\tconf := &clientcredentials.Config{\n\t\t\tClientID:     settings.consumerKey,\n\t\t\tClientSecret: settings.consumerSecret,\n\t\t\tTokenURL:     \"https://api.twitter.com/oauth2/token\",\n\t\t}\n\t\thttpClient = conf.Client(context.Background())\n\t} else {\n\t\tctx := context.Background()\n\t\thttpClient = oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{\n\t\t\tAccessToken: settings.bearerToken,\n\t\t\tTokenType:   \"Bearer\",\n\t\t}))\n\t}\n\n\tclient := Client{\n\t\thttpClient:  httpClient,\n\t\tscreenNames: usernames,\n\t}\n\n\treturn &client\n}\n\n// GetStatsForUser Fetches stats for a single user.  If there is an error fetching or parsing the response\n// from the Twitter API, an empty stats struct will be returned.\nfunc (client *Client) GetStatsForUser(username string) TwitterStats {\n\tstats := TwitterStats{\n\t\tFollowerCount: 0,\n\t\tTweetCount:    0,\n\t}\n\n\turl := fmt.Sprintf(\"%s?screen_name=%s\", userTimelineURL, username)\n\tresp, err := client.httpClient.Get(url)\n\tif err != nil {\n\t\treturn stats\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn stats\n\t}\n\n\t// If there is an error while parsing, just discard the error and return the empty stats\n\terr = json.Unmarshal(body, &stats)\n\tif err != nil {\n\t\treturn stats\n\t}\n\n\treturn stats\n}\n\n// GetStats Returns a slice of `TwitterStats` structs for each username in `client.screenNames` in the same\n// order of `client.screenNames`\nfunc (client *Client) GetStats() []TwitterStats {\n\tstats := make([]TwitterStats, len(client.screenNames))\n\n\tfor i, username := range client.screenNames {\n\t\tstats[i] = client.GetStatsForUser(username)\n\t}\n\n\treturn stats\n}\n"
  },
  {
    "path": "modules/twitterstats/settings.go",
    "content": "package twitterstats\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Twitter Stats\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tbearerToken    string\n\tconsumerKey    string\n\tconsumerSecret string\n\tscreenNames    []interface{}\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tbearerToken:    ymlConfig.UString(\"bearerToken\", os.Getenv(\"WTF_TWITTER_BEARER_TOKEN\")),\n\t\tconsumerKey:    ymlConfig.UString(\"consumerKey\", os.Getenv(\"WTF_TWITTER_CONSUMER_KEY\")),\n\t\tconsumerSecret: ymlConfig.UString(\"consumerSecret\", os.Getenv(\"WTF_TWITTER_CONSUMER_SECRET\")),\n\n\t\tscreenNames: ymlConfig.UList(\"screenNames\"),\n\t}\n\n\tsettings.SetDocumentationPath(\"twitter/stats\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/twitterstats/widget.go",
    "content": "package twitterstats\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tclient   *Client\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, _ *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tclient:   NewClient(settings),\n\t\tsettings: settings,\n\t}\n\n\twidget.View.SetBorderPadding(1, 1, 1, 1)\n\twidget.View.SetWrap(false)\n\twidget.View.SetWordWrap(true)\n\n\treturn &widget\n}\n\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\t// Add header row\n\tstr := fmt.Sprintf(\n\t\t\"[%s]%-12s %10s %8s[white]\\n\",\n\t\twidget.settings.Colors.Subheading,\n\t\t\"Username\",\n\t\t\"Followers\",\n\t\t\"Tweets\",\n\t)\n\n\tstats := widget.client.GetStats()\n\n\t// Add rows for each of the followed usernames\n\tfor i, username := range widget.client.screenNames {\n\t\tstr += fmt.Sprintf(\n\t\t\t\"%-12s %10d %8d\\n\",\n\t\t\tusername,\n\t\t\tstats[i].FollowerCount,\n\t\t\tstats[i].TweetCount,\n\t\t)\n\t}\n\n\treturn \"Twitter Stats\", str, false\n}\n"
  },
  {
    "path": "modules/unknown/settings.go",
    "content": "package unknown\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Unknown\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/unknown/widget.go",
    "content": "package unknown\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tcontent := fmt.Sprintf(\"Widget %s and/or type %s does not exist\", widget.Name(), widget.CommonSettings().Type)\n\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, content, true })\n}\n"
  },
  {
    "path": "modules/updown/keyboard.go",
    "content": "package updown\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n}\n"
  },
  {
    "path": "modules/updown/settings.go",
    "content": "package updown\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Updown.io\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey string   `help:\"An Updown API key.\" optional:\"false\"`\n\ttokens []string `help:\"Filters the checks and returns only the checks with the specified tokens\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey: ymlConfig.UString(\"apiKey\", os.Getenv(\"WTF_UPDOWN_APIKEY\")),\n\t\ttokens: utils.ToStrs(ymlConfig.UList(\"tokens\")),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/updown/widget.go",
    "content": "package updown\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\nconst (\n\tuserAgent = \"WTFUtil\"\n\n\tapiURLBase = \"https://updown.io\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\tchecks   []Check\n\tsettings *Settings\n\ttokenSet map[string]struct{}\n\terr      error\n}\n\n// Taken from https://github.com/AntoineAugusti/updown/blob/d590ab97f115302c73ecf21647909d8fd06ed6ac/checks.go#L17\ntype Check struct {\n\tToken             string            `json:\"token,omitempty\"`\n\tURL               string            `json:\"url,omitempty\"`\n\tAlias             string            `json:\"alias,omitempty\"`\n\tLastStatus        int               `json:\"last_status,omitempty\"`\n\tUptime            float64           `json:\"uptime,omitempty\"`\n\tDown              bool              `json:\"down\"`\n\tDownSince         string            `json:\"down_since,omitempty\"`\n\tError             string            `json:\"error,omitempty\"`\n\tPeriod            int               `json:\"period,omitempty\"`\n\tApdex             float64           `json:\"apdex_t,omitempty\"`\n\tEnabled           bool              `json:\"enabled\"`\n\tPublished         bool              `json:\"published\"`\n\tLastCheckAt       time.Time         `json:\"last_check_at,omitempty\"`\n\tNextCheckAt       time.Time         `json:\"next_check_at,omitempty\"`\n\tFaviconURL        string            `json:\"favicon_url,omitempty\"`\n\tSSL               SSL               `json:\"ssl,omitempty\"`\n\tStringMatch       string            `json:\"string_match,omitempty\"`\n\tMuteUntil         string            `json:\"mute_until,omitempty\"`\n\tDisabledLocations []string          `json:\"disabled_locations,omitempty\"`\n\tCustomHeaders     map[string]string `json:\"custom_headers,omitempty\"`\n}\n\n// Taken from https://github.com/AntoineAugusti/updown/blob/d590ab97f115302c73ecf21647909d8fd06ed6ac/checks.go#L10\ntype SSL struct {\n\tTestedAt string `json:\"tested_at,omitempty\"`\n\tValid    bool   `json:\"valid,omitempty\"`\n\tError    string `json:\"error,omitempty\"`\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\t\tsettings:         settings,\n\t\ttokenSet:         make(map[string]struct{}),\n\t}\n\n\tfor _, t := range settings.tokens {\n\t\twidget.tokenSet[t] = struct{}{}\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\tchecks, err := widget.getExistingChecks()\n\twidget.checks = checks\n\twidget.err = err\n\twidget.SetItemCount(len(checks))\n\twidget.Render()\n}\n\n// Render sets up the widget data for redrawing to the screen\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tnumUp := 0\n\tfor _, check := range widget.checks {\n\t\tif !check.Down {\n\t\t\tnumUp++\n\t\t}\n\t}\n\n\ttitle := fmt.Sprintf(\"Updown (%d/%d)\", numUp, len(widget.checks))\n\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif widget.checks == nil {\n\t\treturn title, \"No checks to display\", false\n\t}\n\n\tstr := widget.contentFrom(widget.checks)\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) contentFrom(checks []Check) string {\n\tvar str string\n\n\tfor _, check := range checks {\n\t\tprefix := \"\"\n\n\t\tif !check.Enabled {\n\t\t\tprefix += \"[yellow] ~ \"\n\t\t} else if check.Down {\n\t\t\tprefix += \"[red] - \"\n\t\t} else {\n\t\t\tprefix += \"[green] + \"\n\t\t}\n\n\t\tstr += fmt.Sprintf(`%s%s [gray](%0.2f|%s)[white]%s`,\n\t\t\tprefix,\n\t\t\tcheck.Alias,\n\t\t\tcheck.Uptime,\n\t\t\ttimeSincePing(check.LastCheckAt),\n\t\t\t\"\\n\",\n\t\t)\n\t}\n\n\treturn str\n}\n\nfunc timeSincePing(ts time.Time) string {\n\tdur := time.Since(ts)\n\treturn dur.Truncate(time.Second).String()\n}\n\nfunc makeURL(baseurl string, path string) (string, error) {\n\tu, err := url.Parse(baseurl)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tu.Path = path\n\treturn u.String(), nil\n}\n\nfunc filterChecks(checks []Check, tokenSet map[string]struct{}) []Check {\n\tj := 0\n\tfor i := 0; i < len(checks); i++ {\n\t\tif _, ok := tokenSet[checks[i].Token]; ok {\n\t\t\tchecks[j] = checks[i]\n\t\t\tj++\n\t\t}\n\t}\n\treturn checks[:j]\n}\n\nfunc (widget *Widget) getExistingChecks() ([]Check, error) {\n\t// See: https://updown.io/api#rest\n\tu, err := makeURL(apiURLBase, \"/api/checks\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq, err := http.NewRequest(\"GET\", u, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"User-Agent\", userAgent)\n\treq.Header.Set(\"X-API-KEY\", widget.settings.apiKey)\n\tresp, err := http.DefaultClient.Do(req)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tvar checks []Check\n\terr = utils.ParseJSON(&checks, resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(widget.tokenSet) > 0 {\n\t\tchecks = filterChecks(checks, widget.tokenSet)\n\t}\n\n\treturn checks, nil\n}\n"
  },
  {
    "path": "modules/uptimekuma/keyboard.go",
    "content": "package uptimekuma\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n}\n"
  },
  {
    "path": "modules/uptimekuma/settings.go",
    "content": "package uptimekuma\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Uptime Kuma\"\n)\n\ntype Settings struct {\n\tcommon *cfg.Common\n\n\turl string `help:\"Status page URL; e.g. https://uptimekuma.example.com/status/overview\"`\n}\n\n// NewSettingsFromYAML creates a new settings instance from a YAML config block\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tcommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\t// Configure your settings attributes here. See http://github.com/olebedev/config for type details\n\t\turl: ymlConfig.UString(\"url\"),\n\t}\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/uptimekuma/widget.go",
    "content": "package uptimekuma\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// HeartbeatStatus represents the status of a heartbeat\n// Matches JS: DOWN=0, UP=1, PENDING=2, MAINTENANCE=3\ntype HeartbeatStatus int\n\nconst (\n\tDOWN HeartbeatStatus = iota\n\tUP\n\tPENDING\n\tMAINTENANCE\n)\n\n// StatusPageData represents the data from the /api/status-page/<slug> endpoint\ntype StatusPageData struct {\n\tIncident *Incident `json:\"incident\"`\n}\n\n// Incident represents an incident in Uptime Kuma\ntype Incident struct {\n\tCreatedDate string `json:\"createdDate\"`\n}\n\n// HeartbeatData represents the data from the /api/status-page/heartbeat/<slug> endpoint\ntype HeartbeatData struct {\n\tHeartbeatList map[string][]*Heartbeat `json:\"heartbeatList\"`\n\tUptimeList    map[string]float64      `json:\"uptimeList\"`\n}\n\n// Heartbeat represents a single heartbeat event\ntype Heartbeat struct {\n\tStatus int `json:\"status\"`\n}\n\n// Widget is the container for your module's data\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings      *Settings\n\tstatusData    *StatusPageData\n\theartbeatData *HeartbeatData\n\terr           error\n}\n\n// NewWidget creates and returns an instance of Widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.common),\n\t\tsettings:   settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\twidget.err = nil\n\n\tbaseURL, slug, err := parseURL(widget.settings.url)\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.display()\n\t\treturn\n\t}\n\n\tstatusData, err := widget.fetchStatusData(baseURL, slug)\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.display()\n\t\treturn\n\t}\n\twidget.statusData = statusData\n\n\theartbeatData, err := widget.fetchHeartbeatData(baseURL, slug)\n\tif err != nil {\n\t\twidget.err = err\n\t\twidget.display()\n\t\treturn\n\t}\n\twidget.heartbeatData = heartbeatData\n\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() string {\n\tif widget.err != nil {\n\t\treturn fmt.Sprintf(\"[red]Error: %v\", widget.err)\n\t}\n\n\tif widget.statusData == nil || widget.heartbeatData == nil {\n\t\treturn \"Loading...\"\n\t}\n\n\t// Use a single indexed variable for status counts\n\tstatusCounts := [4]int{}\n\tfor _, siteList := range widget.heartbeatData.HeartbeatList {\n\t\tif len(siteList) > 0 {\n\t\t\tlastHeartbeat := siteList[len(siteList)-1]\n\t\t\tstatus := HeartbeatStatus(lastHeartbeat.Status)\n\t\t\tif status >= 0 && int(status) < len(statusCounts) {\n\t\t\t\tstatusCounts[status]++\n\t\t\t}\n\t\t}\n\t}\n\n\tvar totalUptime float64\n\tnumMonitors := len(widget.heartbeatData.UptimeList)\n\tif numMonitors > 0 {\n\t\tfor _, uptime := range widget.heartbeatData.UptimeList {\n\t\t\ttotalUptime += uptime\n\t\t}\n\t}\n\n\tvar avgUptime float64\n\tif numMonitors > 0 {\n\t\tavgUptime = (totalUptime / float64(numMonitors)) * 100\n\t}\n\n\t// Adapted from https://github.com/gethomepage/homepage/blob/00bb1a3f37940a0c3c681c3eef0a10d3e1fa0053/src/widgets/uptimekuma/component.jsx#L41C1-L48C1\n\tvar builder strings.Builder\n\tvar textColor = widget.settings.common.Colors.Text\n\tdownColor := \"red\"\n\tif statusCounts[DOWN] == 0 {\n\t\tdownColor = \"green\"\n\t}\n\tfmt.Fprintf(&builder, \"[%s] Up: [green]%d\", textColor, statusCounts[UP])\n\tfmt.Fprintf(&builder, \"[%s] (%.1f%%)\", textColor, avgUptime)\n\tfmt.Fprintf(&builder, \"[%s], Down: [%s]%d\", textColor, downColor, statusCounts[DOWN])\n\tif statusCounts[MAINTENANCE] > 0 {\n\t\tfmt.Fprintf(&builder, \"[%s], Maint: [%s]%d\", textColor, \"blue\", statusCounts[MAINTENANCE])\n\t}\n\tif statusCounts[PENDING] > 0 {\n\t\tfmt.Fprintf(&builder, \"[%s], Pend: [%s]%d\", textColor, \"orange\", statusCounts[PENDING])\n\t}\n\n\tif widget.statusData.Incident != nil {\n\t\t// Uptime Kuma's API returns dates like \"2023-10-27 10:30:00.123\"\n\t\tlayout := \"2006-01-02 15:04:05.999\"\n\t\tcreated, err := time.Parse(layout, widget.statusData.Incident.CreatedDate)\n\t\tif err == nil {\n\t\t\thoursAgo := time.Since(created).Hours()\n\t\t\tfmt.Fprintf(&builder, \"[%s]\\n Incident: %.0fh ago\", textColor, hoursAgo)\n\t\t} else {\n\t\t\tfmt.Fprintf(&builder, \"[%s]\\n Incident [unparsable date]\", textColor)\n\t\t}\n\t}\n\n\treturn builder.String()\n}\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(func() (string, string, bool) {\n\t\treturn widget.CommonSettings().Title, widget.content(), false\n\t})\n}\n\nfunc (*Widget) fetchStatusData(baseURL, slug string) (*StatusPageData, error) {\n\tapiURL := fmt.Sprintf(\"%s/api/status-page/%s\", baseURL, slug)\n\n\tresp, err := http.Get(apiURL)\n\tif resp != nil && resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\tif resp == nil || err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tvar data StatusPageData\n\tif err := json.NewDecoder(resp.Body).Decode(&data); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &data, nil\n}\n\nfunc (*Widget) fetchHeartbeatData(baseURL, slug string) (*HeartbeatData, error) {\n\tapiURL := fmt.Sprintf(\"%s/api/status-page/heartbeat/%s\", baseURL, slug)\n\n\tresp, err := http.Get(apiURL)\n\tif resp != nil && resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\tif resp == nil || err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tvar data HeartbeatData\n\tif err := json.NewDecoder(resp.Body).Decode(&data); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &data, nil\n}\n\nfunc parseURL(rawURL string) (string, string, error) {\n\tif rawURL == \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"URL is not defined\")\n\t}\n\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"invalid URL: %w\", err)\n\t}\n\n\tparts := strings.Split(strings.Trim(u.Path, \"/\"), \"/\")\n\tif len(parts) < 2 || parts[0] != \"status\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"invalid status page URL format. Expected '.../status/<slug>'\")\n\t}\n\n\tslug := parts[1]\n\tbaseURL := fmt.Sprintf(\"%s://%s\", u.Scheme, u.Host)\n\n\treturn baseURL, slug, nil\n}\n"
  },
  {
    "path": "modules/uptimerobot/keyboard.go",
    "content": "package uptimerobot\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n}\n"
  },
  {
    "path": "modules/uptimerobot/settings.go",
    "content": "package uptimerobot\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Uptime Robot\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey        string `help:\"An UptimeRobot API key.\"`\n\tuptimePeriods string `help:\"The periods over which to display uptime (in days, dash-separated).\" optional:\"true\"`\n\tofflineFirst  bool   `help:\"Display offline monitors at the top.\" optional:\"true\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:        ymlConfig.UString(\"apiKey\", os.Getenv(\"WTF_UPTIMEROBOT_APIKEY\")),\n\t\tuptimePeriods: ymlConfig.UString(\"uptimePeriods\", \"30\"),\n\t\tofflineFirst:  ymlConfig.UBool(\"offlineFirst\", false),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).\n\t\tService(\"https://api.uptimerobot.com\").Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/uptimerobot/widget.go",
    "content": "package uptimerobot\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tmonitors []Monitor\n\tsettings *Settings\n\terr      error\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := &Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\twidget.initializeKeyboardControls()\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\tmonitors, err := widget.getMonitors()\n\n\tif widget.settings.offlineFirst {\n\t\tvar tmp Monitor\n\t\tvar next int\n\t\tfor i := 0; i < len(monitors); i++ {\n\t\t\tif monitors[i].State != 2 {\n\t\t\t\ttmp = monitors[i]\n\t\t\t\tfor j := i; j > next; j-- {\n\t\t\t\t\tmonitors[j] = monitors[j-1]\n\t\t\t\t}\n\t\t\t\tmonitors[next] = tmp\n\t\t\t\tnext++\n\t\t\t}\n\t\t}\n\t}\n\n\twidget.monitors = monitors\n\twidget.err = err\n\twidget.SetItemCount(len(monitors))\n\n\twidget.Render()\n}\n\n// Render sets up the widget data for redrawing to the screen\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tnumUp := 0\n\tfor _, monitor := range widget.monitors {\n\t\tif monitor.State == 2 {\n\t\t\tnumUp++\n\t\t}\n\t}\n\n\ttitle := fmt.Sprintf(\"%s (%d/%d)\", widget.CommonSettings().Title, numUp, len(widget.monitors))\n\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\tif widget.monitors == nil {\n\t\treturn title, \"No monitors to display\", false\n\t}\n\n\tstr := widget.contentFrom(widget.monitors)\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) contentFrom(monitors []Monitor) string {\n\tvar str string\n\n\tfor _, monitor := range monitors {\n\t\tprefix := \"\"\n\n\t\tswitch monitor.State {\n\t\tcase 2:\n\t\t\tprefix += \"[green] + \"\n\t\tcase 8:\n\t\tcase 9:\n\t\t\tprefix += \"[red] - \"\n\t\tdefault:\n\t\t\tprefix += \"[yellow] ~ \"\n\t\t}\n\n\t\tstr += fmt.Sprintf(`%s%s [gray](%s)[white]\n`,\n\t\t\tprefix,\n\t\t\tmonitor.Name,\n\t\t\tformatUptimes(monitor.Uptime),\n\t\t)\n\t}\n\n\treturn str\n}\n\nfunc formatUptimes(str string) string {\n\tsplits := strings.Split(str, \"-\")\n\tstr = \"\"\n\tfor i, s := range splits {\n\t\tif i != 0 {\n\t\t\tstr += \"|\"\n\t\t}\n\t\ts = s[:5]\n\t\ts = strings.TrimRight(s, \"0\")\n\t\ts = strings.TrimRight(s, \".\") + \"%\"\n\t\tstr += s\n\t}\n\treturn str\n}\n\ntype Monitor struct {\n\tName string `json:\"friendly_name\"`\n\t// Monitor state, see: https://uptimerobot.com/api/#parameters\n\tState int8 `json:\"status\"`\n\t// Uptime ratio, preformatted, e.g.: 100.000-97.233-96.975\n\tUptime string `json:\"custom_uptime_ratio\"`\n}\n\nfunc (widget *Widget) getMonitors() ([]Monitor, error) {\n\t// See: https://uptimerobot.com/api/#getMonitorsWrap\n\tresp, errh := http.PostForm(\"https://api.uptimerobot.com/v2/getMonitors\",\n\t\turl.Values{\n\t\t\t\"api_key\":              {widget.settings.apiKey},\n\t\t\t\"format\":               {\"json\"},\n\t\t\t\"custom_uptime_ratios\": {widget.settings.uptimePeriods},\n\t\t},\n\t)\n\n\tif errh != nil {\n\t\treturn nil, errh\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\n\t// First pass to read the status\n\tc := make(map[string]json.RawMessage)\n\terrj1 := json.Unmarshal(body, &c)\n\n\tif errj1 != nil {\n\t\treturn nil, errj1\n\t}\n\n\tif string(c[\"stat\"]) != `\"ok\"` {\n\t\treturn nil, errors.New(string(body))\n\t}\n\n\t// Second pass to get the actual info\n\tvar monitors []Monitor\n\terrj2 := json.Unmarshal(c[\"monitors\"], &monitors)\n\n\tif errj2 != nil {\n\t\treturn nil, errj2\n\t}\n\n\treturn monitors, nil\n}\n"
  },
  {
    "path": "modules/urlcheck/client.go",
    "content": "package urlcheck\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/wtfutil/wtf/logger\"\n)\n\n// Perform the requet of the header for a given URL\nfunc DoRequest(urlRequest string, timeout time.Duration, client *http.Client) (int, string) {\n\n\t// Define a Context with the timeout for the request\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\t// Request\n\treq, err := http.NewRequest(http.MethodHead, urlRequest, nil)\n\tif err != nil {\n\t\tlogger.Log(fmt.Sprintf(\"[urlcheck] ERROR %s: %s\", urlRequest, err.Error()))\n\t\treturn InvalidResultCode, \"New Request Error\"\n\t}\n\treq = req.WithContext(ctx)\n\n\t// Send the request\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tif errors.Is(err, context.DeadlineExceeded) {\n\t\t\tstatus := \"Timeout\"\n\t\t\tlogger.Log(fmt.Sprintf(\"[urlcheck] %s: %s\", urlRequest, status))\n\t\t\treturn InvalidResultCode, status\n\t\t}\n\t\tlogger.Log(fmt.Sprintf(\"[urlcheck] %s: %s\", urlRequest, err.Error()))\n\t\treturn InvalidResultCode, \"Error\"\n\t}\n\n\tdefer res.Body.Close()\n\n\treturn res.StatusCode, res.Status\n}\n"
  },
  {
    "path": "modules/urlcheck/client_test.go",
    "content": "package urlcheck\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gotest.tools/assert\"\n)\n\nfunc TestTimeout(t *testing.T) {\n\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\ttime.Sleep(time.Second * 1)\n\t}))\n\tdefer ts.Close()\n\n\tclient := &http.Client{\n\t\tTimeout: time.Millisecond * 10,\n\t}\n\n\ttimeout := 1 * time.Microsecond\n\tstatusCode, statusMsg := DoRequest(ts.URL, timeout, client)\n\n\tassert.Equal(t, 999, statusCode)\n\tassert.Equal(t, \"Timeout\", statusMsg)\n\n}\n"
  },
  {
    "path": "modules/urlcheck/settings.go",
    "content": "package urlcheck\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"URLcheck\"\n)\n\ntype Settings struct {\n\tCommon *cfg.Common\n\n\trequestTimeout int      `help:\"Max Request duration in seconds\"`\n\turls           []string `help:\"A list of URL to check\"`\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\trequestTimeout: ymlConfig.UInt(\"timeout\", 30),\n\t}\n\tsettings.urls = cfg.ParseAsMapOrList(ymlConfig, \"urls\")\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/urlcheck/urlResult.go",
    "content": "package urlcheck\n\nimport (\n\t\"net/url\"\n)\n\nconst InvalidResultCode = 999\n\n// Collect useful properties of each given URL\ntype urlResult struct {\n\tUrl           string\n\tResultCode    int\n\tResultMessage string\n\tIsValid       bool\n}\n\n// Create a UrlResult instance from an urls occurence in the settings\nfunc newUrlResult(urlString string) *urlResult {\n\n\tuResult := urlResult{\n\t\tUrl: urlString,\n\t}\n\n\t_, err := url.ParseRequestURI(urlString)\n\tif err != nil {\n\t\tuResult.ResultMessage = err.Error()\n\t\tuResult.ResultCode = InvalidResultCode\n\t\tuResult.IsValid = false\n\t\treturn &uResult\n\t}\n\n\tuResult.IsValid = true\n\treturn &uResult\n}\n"
  },
  {
    "path": "modules/urlcheck/urlResult_test.go",
    "content": "package urlcheck\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc checkValid(t *testing.T, got *urlResult) {\n\tassert.True(t, got.IsValid)\n\tassert.Less(t, got.ResultCode, 500)\n\tassert.Len(t, got.ResultMessage, 0)\n}\n\nfunc checkInvalid(t *testing.T, got *urlResult) {\n\tassert.False(t, got.IsValid)\n\tassert.GreaterOrEqual(t, got.ResultCode, 500)\n\tassert.Greater(t, len(got.ResultMessage), 0)\n}\n\nfunc Test_newUrlResult(t *testing.T) {\n\ttype args struct {\n\t\turlString string\n\t}\n\ttype checks func(t *testing.T, res *urlResult)\n\n\ttests := []struct {\n\t\tname   string\n\t\targs   args\n\t\tchecks checks\n\t}{\n\t\t{\"good\", args{\"http://www.go.dev\"}, checkValid},\n\t\t{\"good_with_page\", args{\"https://go.dev/doc/install\"}, checkValid},\n\t\t{\"good_with_args\", args{\"https://mysite.com?var=1\"}, checkValid},\n\t\t{\"no_url\", args{\"\"}, checkInvalid},\n\t\t{\"no_escape_chars\", args{\"http://not\\nurl.com?var=1\"}, checkInvalid},\n\t\t{\"no_protocol\", args{\"go.dev\"}, checkInvalid},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.checks != nil {\n\t\t\t\ttt.checks(t, newUrlResult(tt.args.urlString))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/urlcheck/view.go",
    "content": "package urlcheck\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"text/template\"\n)\n\n// Prepare the text template at the moment of the widget creation and stores it in the widget instance\nfunc (widget *Widget) PrepareTemplate() {\n\n\ttextColor := fmt.Sprintf(\" [%s]\", widget.settings.Common.Colors.Text)\n\tlabelColor := fmt.Sprintf(\" [%s]\", widget.settings.Common.Colors.Label)\n\n\twidget.templateString = \"{{range .}} \" +\n\t\t\"{{. | getResultColor}}\" +\n\t\t\"[{{if eq .ResultCode 999}}---{{else}}{{.ResultCode}}{{end}}]\" +\n\t\ttextColor + \"{{.Url}}\" +\n\t\tlabelColor + \"{{.ResultMessage}}\" +\n\t\t\"\\n{{end}}\"\n\n\twidget.PreparedTemplate = template.New(\"tmpl\").Funcs(template.FuncMap{\"getResultColor\": getResultColor})\n}\n\n// Parse the results at each refresh of the widge\nfunc (widget *Widget) parseTemplate() *template.Template {\n\treturn template.Must(widget.PreparedTemplate.Parse(widget.templateString))\n}\n\n// Format the parsed results accordingly to the app style\nfunc (widget *Widget) FormatResult() string {\n\n\tif len(widget.urlList) < 1 {\n\t\treturn \"empty URL list\"\n\t}\n\n\tt := widget.parseTemplate()\n\tresultBuffer := new(bytes.Buffer)\n\terr := t.Execute(resultBuffer, widget.urlList)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\treturn resultBuffer.String()\n}\n\n// URLs with no issues will have their result code in green, otherways in red.\nfunc getResultColor(ur urlResult) string {\n\tif !ur.IsValid {\n\t\treturn \"[red]\"\n\t}\n\n\tif ur.ResultCode < http.StatusInternalServerError {\n\t\treturn \"[green]\"\n\t}\n\n\treturn \"[red]\"\n}\n"
  },
  {
    "path": "modules/urlcheck/widget.go",
    "content": "package urlcheck\n\nimport (\n\t\"net/http\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tsettings         *Settings          // settings from the configuration file\n\turlList          []*urlResult       // list of a collection of useful properies of the url\n\tclient           *http.Client       // the http client shared with all the requestes across all the refreshes\n\ttimeout          time.Duration      // the timeout for a single request\n\tPreparedTemplate *template.Template // the test template shared across the refreshes\n\ttemplateString   string             // the string needed to parse the template and shared across all the widget refreshes\n}\n\n// NewWidget creates and returns an instance of Widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\tmaxUrl := len(settings.urls)\n\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t\turlList:  make([]*urlResult, maxUrl),\n\t\tclient:   &http.Client{},\n\t\ttimeout:  time.Duration(settings.requestTimeout) + time.Second,\n\t}\n\n\twidget.init()\n\twidget.View.SetWrap(false)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Refresh updates the onscreen contents of the widget\nfunc (widget *Widget) Refresh() {\n\twidget.check()\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// The string passed from the settings are checked and prepared for processing\nfunc (widget *Widget) init() {\n\n\t// Prepare the template for the results\n\twidget.PrepareTemplate()\n\n\tfor i, urlString := range widget.settings.urls {\n\t\twidget.urlList[i] = newUrlResult(urlString)\n\t}\n}\n\n// Do the actual requests and check the responses at every widget refresh\nfunc (widget *Widget) check() {\n\tfor _, urlRes := range widget.urlList {\n\t\tif urlRes.IsValid {\n\t\t\turlRes.ResultCode, urlRes.ResultMessage = DoRequest(urlRes.Url, widget.timeout, widget.client)\n\t\t}\n\t}\n}\n\n// Format and displays the results at every refresh\nfunc (widget *Widget) display() {\n\twidget.Redraw(func() (string, string, bool) {\n\t\treturn widget.CommonSettings().Title, widget.FormatResult(), false\n\t})\n}\n"
  },
  {
    "path": "modules/victorops/client.go",
    "content": "package victorops\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/wtfutil/wtf/logger\"\n)\n\n// Fetch gets the current oncall users\nfunc Fetch(apiID, apiKey string) ([]OnCallTeam, error) {\n\tscheduleURL := \"https://api.victorops.com/api-public/v1/oncall/current\"\n\tresponse, err := victorOpsRequest(scheduleURL, apiID, apiKey)\n\n\treturn response, err\n}\n\n/* ---------------- Unexported Functions ---------------- */\n\nfunc victorOpsRequest(url string, apiID string, apiKey string) ([]OnCallTeam, error) {\n\treq, err := http.NewRequest(\"GET\", url, http.NoBody)\n\tif err != nil {\n\t\tlogger.Log(fmt.Sprintf(\"Failed to initialize sessions to VictorOps. ERROR: %s\", err))\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"X-VO-Api-Id\", apiID)\n\treq.Header.Set(\"X-VO-Api-Key\", apiKey)\n\tclient := &http.Client{}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tlogger.Log(fmt.Sprintf(\"Failed to make request to VictorOps. ERROR: %s\", err))\n\t\treturn nil, err\n\t}\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tresponse := &OnCallResponse{}\n\tif err := json.NewDecoder(resp.Body).Decode(response); err != nil {\n\t\tlogger.Log(fmt.Sprintf(\"Failed to decode JSON response. ERROR: %s\", err))\n\t\treturn nil, err\n\t}\n\n\tteams := parseTeams(response)\n\treturn teams, nil\n}\n\nfunc parseTeams(input *OnCallResponse) []OnCallTeam {\n\tvar teamResults []OnCallTeam\n\n\tfor _, data := range input.TeamsOnCall {\n\t\tvar team OnCallTeam\n\t\tteam.Name = data.Team.Name\n\t\tteam.Slug = data.Team.Slug\n\t\tvar userList []string\n\t\tfor _, userData := range data.OnCallNow {\n\t\t\tescalationPolicy := userData.EscalationPolicy.Name\n\t\t\tfor _, user := range userData.Users {\n\t\t\t\tuserList = append(userList, user.OnCallUser.Username)\n\t\t\t}\n\t\t\tteam.OnCall = append(team.OnCall, OnCall{escalationPolicy, strings.Join(userList, \", \")})\n\t\t}\n\t\tteamResults = append(teamResults, team)\n\t}\n\n\treturn teamResults\n}\n"
  },
  {
    "path": "modules/victorops/oncallresponse.go",
    "content": "package victorops\n\n// OnCallResponse object\ntype OnCallResponse struct {\n\tTeamsOnCall []struct {\n\t\tTeam struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tSlug string `json:\"slug\"`\n\t\t} `json:\"team\"`\n\t\tOnCallNow []struct {\n\t\t\tEscalationPolicy struct {\n\t\t\t\tName string `json:\"name\"`\n\t\t\t\tSlug string `json:\"slug\"`\n\t\t\t} `json:\"escalationPolicy\"`\n\t\t\tUsers []struct {\n\t\t\t\tOnCallUser struct {\n\t\t\t\t\tUsername string `json:\"username\"`\n\t\t\t\t} `json:\"onCalluser\"`\n\t\t\t} `json:\"users\"`\n\t\t} `json:\"oncallNow\"`\n\t} `json:\"teamsOnCall\"`\n}\n"
  },
  {
    "path": "modules/victorops/oncallteam.go",
    "content": "package victorops\n\n// OnCallTeam object to make\n// managing objects easier\ntype OnCallTeam struct {\n\tName   string\n\tSlug   string\n\tOnCall []OnCall\n}\n\n// OnCall object to handle\n// different on call policies\ntype OnCall struct {\n\tPolicy   string\n\tUserlist string\n}\n"
  },
  {
    "path": "modules/victorops/settings.go",
    "content": "package victorops\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"VictorOps\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiID  string\n\tapiKey string\n\tteam   string\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiID:  ymlConfig.UString(\"apiID\", os.Getenv(\"WTF_VICTOROPS_API_ID\")),\n\t\tapiKey: ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_VICTOROPS_API_KEY\"))),\n\t\tteam:   ymlConfig.UString(\"team\"),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/victorops/widget.go",
    "content": "package victorops\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget contains text info\ntype Widget struct {\n\tview.TextWidget\n\n\tteams    []OnCallTeam\n\tsettings *Settings\n\terr      error\n}\n\n// NewWidget creates a new widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\t\tsettings:   settings,\n\t}\n\n\twidget.View.SetScrollable(true)\n\twidget.View.SetRegions(true)\n\n\treturn &widget\n}\n\n// Refresh gets latest content for the widget\nfunc (widget *Widget) Refresh() {\n\tif widget.Disabled() {\n\t\treturn\n\t}\n\n\tteams, err := Fetch(widget.settings.apiID, widget.settings.apiKey)\n\n\twidget.err = err\n\twidget.teams = teams\n\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := widget.CommonSettings().Title\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\tteams := widget.teams\n\tvar str string\n\n\tif len(teams) == 0 {\n\t\treturn title, \"No teams specified\", false\n\t}\n\n\tfor _, team := range teams {\n\t\tif len(widget.settings.team) > 0 && widget.settings.team != team.Slug {\n\t\t\tcontinue\n\t\t}\n\n\t\tstr = fmt.Sprintf(\"%s[green]%s\\n\", str, team.Name)\n\t\tif len(team.OnCall) == 0 {\n\t\t\tstr = fmt.Sprintf(\"%s[grey]no one\\n\", str)\n\t\t}\n\t\tfor _, onCall := range team.OnCall {\n\t\t\tstr = fmt.Sprintf(\"%s[white]%s - %s\\n\", str, onCall.Policy, onCall.Userlist)\n\t\t}\n\n\t\tstr = fmt.Sprintf(\"%s\\n\", str)\n\t}\n\n\tif str == \"\" {\n\t\tstr = \"Could not find any teams to display\"\n\t}\n\treturn title, str, false\n}\n"
  },
  {
    "path": "modules/weatherservices/arpansagovau/client.go",
    "content": "package arpansagovau\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\ntype Stations struct {\n\tXMLName  xml.Name `xml:\"stations\"`\n\tText     string   `xml:\",chardata\"`\n\tLocation []struct {\n\t\tText        string  `xml:\",chardata\"`\n\t\tID          string  `xml:\"id,attr\"`\n\t\tName        string  `xml:\"name\"`        // adl, ali, bri, can, cas, ...\n\t\tIndex       float32 `xml:\"index\"`       // 0.0, 0.0, 0.0, 0.0, 0.0, ...\n\t\tTime        string  `xml:\"time\"`        // 7:24 PM, 7:24 PM, 7:54 PM...\n\t\tDate        string  `xml:\"date\"`        // 29/08/2019, 29/08/2019, 2...\n\t\tFulldate    string  `xml:\"fulldate\"`    // Thursday, 29 August 2019,...\n\t\tUtcdatetime string  `xml:\"utcdatetime\"` // 2019/08/29 09:54, 2019/08...\n\t\tStatus      string  `xml:\"status\"`      // ok, ok, ok, ok, ok, ok, o...\n\t} `xml:\"location\"`\n}\n\ntype location struct {\n\tname   string\n\tindex  float32\n\ttime   string\n\tdate   string\n\tstatus string\n}\n\nfunc getLocationData(cityname string) (*location, error) {\n\tvar locdata location\n\tresp, err := apiRequest()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstations, err := parseXML(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, city := range stations.Location {\n\t\tif city.ID == cityname {\n\t\t\tlocdata = location{name: city.ID, index: city.Index, time: city.Time, date: city.Date, status: city.Status}\n\t\t\tbreak\n\t\t}\n\t}\n\treturn &locdata, err\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc apiRequest() (*http.Response, error) {\n\treq, err := http.NewRequest(\"GET\", \"https://uvdata.arpansa.gov.au/xml/uvvalues.xml\", http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"%s\", resp.Status)\n\t}\n\n\treturn resp, nil\n}\nfunc parseXML(text io.Reader) (Stations, error) {\n\tdec := xml.NewDecoder(text)\n\tdec.Strict = false\n\n\tvar v Stations\n\terr := dec.Decode(&v)\n\treturn v, err\n}\n"
  },
  {
    "path": "modules/weatherservices/arpansagovau/settings.go",
    "content": "package arpansagovau\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"ARPANSA UV Data\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcity string\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\t\tcity:   ymlConfig.UString(\"locationid\"),\n\t}\n\n\tsettings.SetDocumentationPath(\"weather_services/arpansagovau\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/weatherservices/arpansagovau/widget.go",
    "content": "package arpansagovau\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tlocation  *location\n\tlastError error\n\tsettings  *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\tlocationData, err := getLocationData(settings.city)\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tlocation:  locationData,\n\t\tlastError: err,\n\t\tsettings:  settings,\n\t}\n\n\twidget.View.SetWrap(true)\n\n\treturn &widget\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\n\tlocationData, err := getLocationData(widget.settings.city)\n\twidget.location = locationData\n\twidget.lastError = err\n\n\tif widget.lastError != nil {\n\t\treturn widget.CommonSettings().Title, fmt.Sprintf(\"Err: %s\", widget.lastError.Error()), true\n\t}\n\n\treturn widget.CommonSettings().Title, formatLocationData(widget.location), true\n}\n\nfunc (widget *Widget) Refresh() {\n\twidget.Redraw(widget.content)\n}\n\nfunc formatLocationData(location *location) string {\n\tvar level string\n\tvar color string\n\tvar content string\n\n\tif location.name == \"\" {\n\t\treturn \"[red]No data?\"\n\t}\n\n\tif location.status != \"ok\" {\n\t\tcontent = \"[red]Data unavailable for \"\n\t\tcontent += location.name\n\t\treturn content\n\t}\n\n\tswitch {\n\tcase location.index < 2.5:\n\t\tcolor = \"[green]\"\n\t\tlevel = \" (LOW)\"\n\tcase location.index >= 2.5 && location.index < 5.5:\n\t\tcolor = \"[yellow]\"\n\t\tlevel = \" (MODERATE)\"\n\tcase location.index >= 5.5 && location.index < 7.5:\n\t\tcolor = \"[orange]\"\n\t\tlevel = \" (HIGH)\"\n\tcase location.index >= 7.5 && location.index < 10.5:\n\t\tcolor = \"[red]\"\n\t\tlevel = \" (VERY HIGH)\"\n\tcase location.index >= 10.5:\n\t\tcolor = \"[fuchsia]\"\n\t\tlevel = \" (EXTREME)\"\n\t}\n\n\tcontent = \"Location: \"\n\tcontent += location.name\n\tcontent += \"\\nUV index: \"\n\tcontent += color\n\tcontent += fmt.Sprintf(\"%.2f\", location.index)\n\tcontent += level\n\tcontent += \"[white]\\nLocal time: \"\n\tcontent += location.time\n\tcontent += \" \"\n\tcontent += location.date\n\tcontent += \"\\nDetector status: \"\n\tcontent += location.status\n\n\treturn content\n}\n"
  },
  {
    "path": "modules/weatherservices/prettyweather/settings.go",
    "content": "package prettyweather\n\nimport (\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = false\n\tdefaultTitle     = \"Pretty Weather\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tcity     string\n\tunit     string\n\tview     string\n\tlanguage string\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tcity:     ymlConfig.UString(\"city\", \"Barcelona\"),\n\t\tlanguage: ymlConfig.UString(\"language\", \"en\"),\n\t\tunit:     ymlConfig.UString(\"unit\", \"m\"),\n\t\tview:     ymlConfig.UString(\"view\", \"0\"),\n\t}\n\n\tsettings.SetDocumentationPath(\"weather_services/prettyweather\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/weatherservices/prettyweather/widget.go",
    "content": "package prettyweather\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/view\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\ntype Widget struct {\n\tview.TextWidget\n\n\tresult   string\n\tsettings *Settings\n}\n\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tTextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\treturn &widget\n}\n\nfunc (widget *Widget) Refresh() {\n\twidget.prettyWeather()\n\n\twidget.Redraw(func() (string, string, bool) { return widget.CommonSettings().Title, widget.result, false })\n}\n\n// this method reads the config and calls wttr.in for pretty weather\nfunc (widget *Widget) prettyWeather() {\n\tclient := &http.Client{}\n\n\tcity := widget.settings.city\n\tunit := widget.settings.unit\n\tview := widget.settings.view\n\n\treq, err := http.NewRequest(\"GET\", \"https://wttr.in/\"+city+\"?\"+view+\"?\"+unit, http.NoBody)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\n\treq.Header.Set(\"Accept-Language\", widget.settings.language)\n\treq.Header.Set(\"User-Agent\", \"curl\")\n\tresponse, err := client.Do(req)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\n\t}\n\tdefer func() { _ = response.Body.Close() }()\n\n\tcontents, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\twidget.result = err.Error()\n\t\treturn\n\t}\n\n\twidget.result = strings.TrimSpace(wtf.ASCIItoTviewColors(string(contents)))\n}\n"
  },
  {
    "path": "modules/weatherservices/weather/display.go",
    "content": "package weather\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\towm \"github.com/briandowns/openweathermap\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\nfunc (widget *Widget) display() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\tvar err string\n\n\tif !widget.apiKeyValid() {\n\t\terr = \" Environment variable WTF_OWM_API_KEY is not set\\n\"\n\t}\n\n\tcityData := widget.currentData()\n\tif err == \"\" && cityData == nil {\n\t\terr += \" Weather data is unavailable: no city data\\n\"\n\t}\n\n\tif err == \"\" && len(cityData.Weather) == 0 {\n\t\terr += \" Weather data is unavailable: no weather data\\n\"\n\t}\n\n\ttitle := widget.CommonSettings().Title\n\tsetWrap := false\n\n\tvar content string\n\tif err != \"\" {\n\t\tsetWrap = true\n\t\tcontent = err\n\t} else {\n\n\t\ttitle = widget.buildTitle(cityData)\n\t\t_, _, width, _ := widget.View.GetRect()\n\t\tcontent = widget.settings.PaginationMarker(len(widget.Data), widget.Idx, width) + \"\\n\"\n\n\t\tif widget.settings.compact {\n\t\t\tcontent += widget.description(cityData) + \"\\n\"\n\t\t} else {\n\t\t\tcontent += widget.description(cityData) + \"\\n\\n\"\n\t\t}\n\n\t\tcontent += widget.temperatures(cityData) + \"\\n\"\n\t\tcontent += widget.sunInfo(cityData)\n\t}\n\n\treturn title, content, setWrap\n}\n\nfunc (widget *Widget) description(cityData *owm.CurrentWeatherData) string {\n\tdescs := []string{}\n\tfor _, weather := range cityData.Weather {\n\t\tdescs = append(descs, fmt.Sprintf(\" %s\", weather.Description))\n\t}\n\n\treturn strings.Join(descs, \",\")\n}\n\nfunc (widget *Widget) sunInfo(cityData *owm.CurrentWeatherData) string {\n\n\tsunriseTime := wtf.UnixTime(int64(cityData.Sys.Sunrise))\n\tsunsetTime := wtf.UnixTime(int64(cityData.Sys.Sunset))\n\n\trenderStr := fmt.Sprintf(\" Rise: %s   Set: %s\", sunriseTime.Format(\"15:04 MST\"), sunsetTime.Format(\"15:04 MST\"))\n\n\tif widget.settings.compact {\n\t\trenderStr = fmt.Sprintf(\" Sun: %s / %s\", sunriseTime.Format(\"15:04\"), sunsetTime.Format(\"15:04\"))\n\t}\n\n\treturn renderStr\n}\n\nfunc (widget *Widget) temperatures(cityData *owm.CurrentWeatherData) string {\n\tstr := fmt.Sprintf(\"%8s: %4.1f° %s\\n\", \"High\", cityData.Main.TempMax, widget.settings.tempUnit)\n\n\tstr += fmt.Sprintf(\n\t\t\"%8s: [%s]%4.1f° %s[white]\\n\",\n\t\t\"Current\",\n\t\twidget.settings.current,\n\t\tcityData.Main.Temp,\n\t\twidget.settings.tempUnit,\n\t)\n\n\tif widget.settings.compact {\n\t\tstr += fmt.Sprintf(\"%8s: %4.1f° %s\", \"Low\", cityData.Main.TempMin, widget.settings.tempUnit)\n\t} else {\n\t\tstr += fmt.Sprintf(\"%8s: %4.1f° %s\\n\", \"Low\", cityData.Main.TempMin, widget.settings.tempUnit)\n\t}\n\n\treturn str\n}\n\nfunc (widget *Widget) buildTitle(cityData *owm.CurrentWeatherData) string {\n\tif widget.settings.useEmoji {\n\t\treturn fmt.Sprintf(\"%s %s\", widget.emojiFor(cityData), cityData.Name)\n\t}\n\n\treturn cityData.Name\n}\n"
  },
  {
    "path": "modules/weatherservices/weather/emoji.go",
    "content": "package weather\n\nimport (\n\towm \"github.com/briandowns/openweathermap\"\n)\n\nvar weatherEmoji = map[string]string{\n\t\"default\":                     \"💥\",\n\t\"broken clouds\":               \"🌤\",\n\t\"clear\":                       \"🌎\",\n\t\"clear sky\":                   \"🌎\",\n\t\"cloudy\":                      \"⛅️\",\n\t\"few clouds\":                  \"🌤\",\n\t\"fog\":                         \"🌫\",\n\t\"haze\":                        \"🌫\",\n\t\"heavy intensity rain\":        \"💦\",\n\t\"heavy rain\":                  \"💦\",\n\t\"heavy snow\":                  \"⛄️\",\n\t\"light intensity drizzle\":     \"🌧\",\n\t\"light intensity shower rain\": \"☔️\",\n\t\"light rain\":                  \"🌦\",\n\t\"light shower snow\":           \"🌦⛄️\",\n\t\"light snow\":                  \"🌨\",\n\t\"mist\":                        \"🌬\",\n\t\"moderate rain\":               \"🌧\",\n\t\"moderate snow\":               \"🌨\",\n\t\"overcast\":                    \"🌥\",\n\t\"overcast clouds\":             \"🌥\",\n\t\"partly cloudy\":               \"🌤\",\n\t\"scattered clouds\":            \"🌤\",\n\t\"shower rain\":                 \"☔️\",\n\t\"smoke\":                       \"🔥\",\n\t\"snow\":                        \"❄️\",\n\t\"sunny\":                       \"☀️\",\n\t\"thunderstorm\":                \"⛈\",\n}\n\nfunc (widget *Widget) emojiFor(data *owm.CurrentWeatherData) string {\n\tif len(data.Weather) == 0 {\n\t\treturn \"\"\n\t}\n\n\temoji := weatherEmoji[data.Weather[0].Description]\n\tif emoji == \"\" {\n\t\temoji = weatherEmoji[\"default\"]\n\t}\n\n\treturn emoji\n}\n"
  },
  {
    "path": "modules/weatherservices/weather/keyboard.go",
    "content": "package weather\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"h\", widget.PrevSource, \"Select previous city\")\n\twidget.SetKeyboardChar(\"l\", widget.NextSource, \"Select next city\")\n\n\twidget.SetKeyboardKey(tcell.KeyLeft, widget.PrevSource, \"Select previous city\")\n\twidget.SetKeyboardKey(tcell.KeyRight, widget.NextSource, \"Select next city\")\n}\n"
  },
  {
    "path": "modules/weatherservices/weather/settings.go",
    "content": "package weather\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Weather\"\n)\n\ntype colors struct {\n\tcurrent string\n}\n\ntype Settings struct {\n\tcolors\n\t*cfg.Common\n\n\tapiKey   string\n\tcityIDs  []interface{}\n\tlanguage string\n\ttempUnit string\n\tuseEmoji bool\n\tcompact  bool\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:   ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"WTF_OWM_API_KEY\"))),\n\t\tcityIDs:  ymlConfig.UList(\"cityids\"),\n\t\tlanguage: ymlConfig.UString(\"language\", \"EN\"),\n\t\ttempUnit: ymlConfig.UString(\"tempUnit\", \"C\"),\n\t\tuseEmoji: ymlConfig.UBool(\"useEmoji\", true),\n\t\tcompact:  ymlConfig.UBool(\"compact\", false),\n\t}\n\n\tsettings.SetDocumentationPath(\"weather_services/weather/\")\n\n\tsettings.current = ymlConfig.UString(\"colors.current\", \"green\")\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/weatherservices/weather/widget.go",
    "content": "package weather\n\nimport (\n\towm \"github.com/briandowns/openweathermap\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// Widget is the container for weather data.\ntype Widget struct {\n\tview.MultiSourceWidget\n\tview.TextWidget\n\n\t// APIKey   string\n\tData []*owm.CurrentWeatherData\n\n\tpages    *tview.Pages\n\tsettings *Settings\n}\n\n// NewWidget creates and returns a new instance of the weather Widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tMultiSourceWidget: view.NewMultiSourceWidget(settings.Common, \"cityid\", \"cityids\"),\n\t\tTextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tpages:    pages,\n\t\tsettings: settings,\n\t}\n\n\twidget.initializeKeyboardControls()\n\n\twidget.SetDisplayFunction(widget.display)\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Fetch retrieves OpenWeatherMap data from the OpenWeatherMap API.\n// It takes a list of OpenWeatherMap city IDs.\n// It returns a list of OpenWeatherMap CurrentWeatherData structs, one per valid city code.\nfunc (widget *Widget) Fetch(cityIDs []int) []*owm.CurrentWeatherData {\n\tdata := []*owm.CurrentWeatherData{}\n\n\tfor _, cityID := range cityIDs {\n\t\tresult, err := widget.currentWeather(cityID)\n\t\tif err == nil {\n\t\t\tdata = append(data, result)\n\t\t}\n\t}\n\n\treturn data\n}\n\n// Refresh fetches new data from the OpenWeatherMap API and loads the new data into the.\n// widget's view for rendering\nfunc (widget *Widget) Refresh() {\n\tif widget.apiKeyValid() {\n\t\twidget.Data = widget.Fetch(utils.ToInts(widget.settings.cityIDs))\n\t}\n\n\twidget.display()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) apiKeyValid() bool {\n\tif widget.settings.apiKey == \"\" {\n\t\treturn false\n\t}\n\n\tif len(widget.settings.apiKey) != 32 {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc (widget *Widget) currentData() *owm.CurrentWeatherData {\n\tif len(widget.Data) == 0 {\n\t\treturn nil\n\t}\n\n\tif widget.Idx < 0 || widget.Idx >= len(widget.Data) {\n\t\treturn nil\n\t}\n\n\treturn widget.Data[widget.Idx]\n}\n\nfunc (widget *Widget) currentWeather(cityCode int) (*owm.CurrentWeatherData, error) {\n\tweather, err := owm.NewCurrent(\n\t\twidget.settings.tempUnit,\n\t\twidget.settings.language,\n\t\twidget.settings.apiKey,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = weather.CurrentByID(cityCode)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn weather, nil\n}\n"
  },
  {
    "path": "modules/zendesk/client.go",
    "content": "package zendesk\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\ntype Resource struct {\n\tResponse interface{}\n\tRaw      string\n}\n\nfunc (widget *Widget) api(meth string) (*Resource, error) {\n\ttrn := &http.Transport{}\n\n\tclient := &http.Client{\n\t\tTransport: trn,\n\t}\n\n\tbaseURL := fmt.Sprintf(\"https://%v.zendesk.com/api/v2\", widget.settings.subdomain)\n\tURL := baseURL + \"/tickets.json?sort_by=status\"\n\n\treq, err := http.NewRequest(meth, URL, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\tapiUser := fmt.Sprintf(\"%v/token\", widget.settings.username)\n\treq.SetBasicAuth(apiUser, widget.settings.apiKey)\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tdata, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Resource{Response: &resp, Raw: string(data)}, nil\n}\n"
  },
  {
    "path": "modules/zendesk/keyboard.go",
    "content": "package zendesk\n\nimport \"github.com/gdamore/tcell/v2\"\n\nfunc (widget *Widget) initializeKeyboardControls() {\n\twidget.InitializeHelpTextKeyboardControl(widget.ShowHelp)\n\twidget.InitializeRefreshKeyboardControl(widget.Refresh)\n\n\twidget.SetKeyboardChar(\"j\", widget.Next, \"Select next item\")\n\twidget.SetKeyboardChar(\"k\", widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardChar(\"o\", widget.openTicket, \"Open item\")\n\n\twidget.SetKeyboardKey(tcell.KeyDown, widget.Next, \"Select next item\")\n\twidget.SetKeyboardKey(tcell.KeyUp, widget.Prev, \"Select previous item\")\n\twidget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, \"Clear selection\")\n\twidget.SetKeyboardKey(tcell.KeyEnter, widget.openTicket, \"Open item\")\n}\n"
  },
  {
    "path": "modules/zendesk/settings.go",
    "content": "package zendesk\n\nimport (\n\t\"os\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nconst (\n\tdefaultFocusable = true\n\tdefaultTitle     = \"Zendesk\"\n)\n\ntype Settings struct {\n\t*cfg.Common\n\n\tapiKey    string\n\tstatus    string\n\tsubdomain string\n\tusername  string\n}\n\nfunc NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {\n\tsettings := Settings{\n\t\tCommon: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),\n\n\t\tapiKey:    ymlConfig.UString(\"apiKey\", ymlConfig.UString(\"apikey\", os.Getenv(\"ZENDESK_API\"))),\n\t\tstatus:    ymlConfig.UString(\"status\"),\n\t\tsubdomain: ymlConfig.UString(\"subdomain\", os.Getenv(\"ZENDESK_SUBDOMAIN\")),\n\t\tusername:  ymlConfig.UString(\"username\"),\n\t}\n\n\tcfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()\n\n\treturn &settings\n}\n"
  },
  {
    "path": "modules/zendesk/tickets.go",
    "content": "package zendesk\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n)\n\ntype TicketArray struct {\n\tCount         int    `json:\"count\"`\n\tCreated       string `json:\"created\"`\n\tNext_page     string `json:\"next_page\"`\n\tPrevious_page string `json:\"previous_page\"`\n\tTickets       []Ticket\n}\n\ntype Ticket struct {\n\tId                    uint64      `json:\"id\"`\n\tURL                   string      `json:\"url\"`\n\tExternalId            string      `json:\"external_id\"`\n\tCreatedAt             string      `json:\"created_at\"`\n\tUpdatedAt             string      `json:\"updated_at\"`\n\tType                  string      `json:\"type\"`\n\tSubject               string      `json:\"subject\"`\n\tRawSubject            string      `json:\"raw_subject\"`\n\tDescription           string      `json:\"description\"`\n\tPriority              string      `json:\"priority\"`\n\tStatus                string      `json:\"status\"`\n\tRecipient             string      `json:\"recipient\"`\n\tRequesterId           uint64      `json:\"requester_id\"`\n\tSubmitterId           uint64      `json:\"submitter_id\"`\n\tAssigneeId            uint64      `json:\"assignee_id\"`\n\tOrganizationId        uint32      `json:\"organization_id\"`\n\tGroupId               uint32      `json:\"group_id\"`\n\tCollaboratorIds       []int64     `json:\"collaborator_ids\"`\n\tForumTopicId          uint32      `json:\"forum_topic_id\"`\n\tProblemId             uint32      `json:\"problem_id\"`\n\tHasIncidents          bool        `json:\"has_incidents\"`\n\tDueAt                 string      `json:\"due_at\"`\n\tTags                  []string    `json:\"tags\"`\n\tSatisfaction_rating   string      `json:\"satisfaction_rating\"`\n\tTicket_form_id        uint32      `json:\"ticket_form_id\"`\n\tSharing_agreement_ids interface{} `json:\"sharing_agreement_ids\"`\n\tVia                   interface{} `json:\"via\"`\n\tCustom_Fields         interface{} `json:\"custom_fields\"`\n\tFields                interface{} `json:\"fields\"`\n}\n\nfunc (widget *Widget) listTickets(pag ...string) (*TicketArray, error) {\n\ttickets := &TicketArray{}\n\n\tresource, err := widget.api(\"GET\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = json.Unmarshal([]byte(resource.Raw), tickets)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn tickets, err\n}\n\nfunc (widget *Widget) newTickets() (*TicketArray, error) {\n\tnewTicketArray := &TicketArray{}\n\ttickets, err := widget.listTickets(widget.settings.apiKey)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfor _, Ticket := range tickets.Tickets {\n\t\tif Ticket.Status == widget.settings.status && Ticket.Status != \"closed\" && Ticket.Status != \"solved\" {\n\t\t\tnewTicketArray.Tickets = append(newTicketArray.Tickets, Ticket)\n\t\t}\n\t}\n\n\treturn newTicketArray, nil\n}\n"
  },
  {
    "path": "modules/zendesk/widget.go",
    "content": "package zendesk\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"github.com/wtfutil/wtf/view\"\n)\n\n// A Widget represents a Zendesk widget\ntype Widget struct {\n\tview.ScrollableWidget\n\n\tresult   *TicketArray\n\tsettings *Settings\n\terr      error\n}\n\n// NewWidget creates a new instance of a widget\nfunc NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {\n\twidget := Widget{\n\t\tScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),\n\n\t\tsettings: settings,\n\t}\n\n\twidget.SetRenderFunction(widget.Render)\n\n\twidget.initializeKeyboardControls()\n\n\treturn &widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *Widget) Refresh() {\n\tticketArray, err := widget.newTickets()\n\tticketArray.Count = len(ticketArray.Tickets)\n\twidget.err = err\n\twidget.result = ticketArray\n\twidget.Render()\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *Widget) Render() {\n\twidget.Redraw(widget.content)\n}\n\nfunc (widget *Widget) content() (string, string, bool) {\n\ttitle := fmt.Sprintf(\"%s (%d)\", widget.CommonSettings().Title, widget.result.Count)\n\tif widget.err != nil {\n\t\treturn title, widget.err.Error(), true\n\t}\n\n\titems := widget.result.Tickets\n\tif len(items) == 0 {\n\t\treturn title, \"No unassigned tickets in queue - woop!!\", false\n\t}\n\n\tstr := \"\"\n\tfor idx, data := range items {\n\t\tstr += widget.format(data, idx)\n\t}\n\n\treturn title, str, false\n}\n\nfunc (widget *Widget) format(ticket Ticket, idx int) string {\n\ttextColor := widget.settings.Colors.Background\n\tif idx == widget.GetSelected() {\n\t\ttextColor = widget.settings.Colors.Focused\n\t}\n\n\trequesterName := widget.parseRequester(ticket)\n\tstr := fmt.Sprintf(\" [%s:]%d - %s\\n %s\\n\\n\", textColor, ticket.Id, requesterName, ticket.Subject)\n\treturn str\n}\n\n// this is a nasty means of extracting the actual name of the requester from the Via interface of the Ticket.\n// very very open to improvements on this\nfunc (widget *Widget) parseRequester(ticket Ticket) interface{} {\n\tviaMap := ticket.Via\n\tvia := viaMap.(map[string]interface{})\n\tsource := via[\"source\"]\n\tfromMap, _ := source.(map[string]interface{})\n\tfrom := fromMap[\"from\"]\n\tfromValMap := from.(map[string]interface{})\n\tfromName := fromValMap[\"name\"]\n\treturn fromName\n}\n\nfunc (widget *Widget) openTicket() {\n\tsel := widget.GetSelected()\n\tif sel >= 0 && widget.result != nil && sel < len(widget.result.Tickets) {\n\t\tissue := &widget.result.Tickets[sel]\n\t\tticketURL := fmt.Sprintf(\"https://%s.zendesk.com/agent/tickets/%d\", widget.settings.subdomain, issue.Id)\n\t\tutils.OpenFile(ticketURL)\n\t}\n}\n"
  },
  {
    "path": "scripts/check-uncommitted-vendor-files.sh",
    "content": "#!/bin/bash\n\nset -euo pipefail\n\nGOPROXY=\"https://proxy.golang.org,direct\" GOSUMDB=off GO111MODULE=on go mod tidy\n\nuntracked_files=$(git ls-files --others --exclude-standard | wc -l)\n\ndiff_stat=$(git diff --shortstat)\n\nif [[ \"${untracked_files}\" -ne 0 || -n \"${diff_stat}\" ]]; then\n  echo 'Untracked or diff in tracked vendor files found. Please run \"go mod tidy\" and commit the changes'\n  exit 1\nfi\n"
  },
  {
    "path": "support/github.go",
    "content": "package support\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\n\tghb \"github.com/google/go-github/v32/github\"\n\t\"github.com/shurcooL/githubv4\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nvar sponsorQuery struct {\n\tUser struct {\n\t\tSponsorshipsAsSponsor struct {\n\t\t\tNodes []struct {\n\t\t\t\tSponsorable struct {\n\t\t\t\t\tSponsorsListing struct {\n\t\t\t\t\t\tSlug string\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} `graphql:\"sponsorshipsAsSponsor(first: 10)\"`\n\t} `graphql:\"user(login: $loginName)\"`\n}\n\n// GitHubUser represents a GitHub user account as defined by a GitHub API access key\n// This is used to determine whether or not the WTF user is a sponsor (via GitHub sponsors)\n// and/or a contributor to WTF\ntype GitHubUser struct {\n\tapiKey string\n\n\tloginName string\n\n\tclientV3 *ghb.Client\n\tclientV4 *githubv4.Client\n\n\tIsContributor bool\n\tIsSponsor     bool\n}\n\n// NewGitHubUser creates and returns an instance of GitHub user with the boolean fields\n// populated\nfunc NewGitHubUser(githubAPIKey string) *GitHubUser {\n\tghUser := GitHubUser{\n\t\tapiKey: githubAPIKey,\n\n\t\tclientV3: nil,\n\t\tclientV4: nil,\n\n\t\tloginName: \"\",\n\n\t\tIsContributor: false,\n\t\tIsSponsor:     false,\n\t}\n\n\tif ghUser.hasAPIKey() {\n\t\t// Use the v3 API to get the contributors because this doesn't seem to be supported by the v4 API yet\n\t\tclientV3, _ := ghUser.authenticateV3()\n\t\tghUser.clientV3 = clientV3\n\n\t\t// Use the v4 API to get sponsors because this doesn't seem to be supported in v3\n\t\tclientV4, _ := ghUser.authenticateV4()\n\t\tghUser.clientV4 = clientV4\n\t}\n\n\treturn &ghUser\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Load loads the user's data from GitHub\nfunc (ghUser *GitHubUser) Load() error {\n\terr := ghUser.verifyGitHubClients()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = ghUser.loadGitHubData()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (ghUser *GitHubUser) authenticateV3() (*ghb.Client, error) {\n\tsrc := oauth2.StaticTokenSource(\n\t\t&oauth2.Token{AccessToken: ghUser.apiKey},\n\t)\n\n\toauthClient := oauth2.NewClient(context.Background(), src)\n\tclient := ghb.NewClient(oauthClient)\n\n\treturn client, nil\n}\n\nfunc (ghUser *GitHubUser) authenticateV4() (*githubv4.Client, error) {\n\tsrc := oauth2.StaticTokenSource(\n\t\t&oauth2.Token{AccessToken: ghUser.apiKey},\n\t)\n\n\toauthClient := oauth2.NewClient(context.Background(), src)\n\tclient := githubv4.NewClient(oauthClient)\n\n\treturn client, nil\n}\n\n// hasAPIKey returns TRUE if the user has put a GitHub API key into their\n// configuration and we've managed to find and read it\nfunc (ghUser *GitHubUser) hasAPIKey() bool {\n\treturn ghUser.apiKey != \"\"\n}\n\nfunc (ghUser *GitHubUser) loadGitHubData() error {\n\tvar err error\n\n\tlogin, err := ghUser.loadLoginName()\n\tif err != nil {\n\t\treturn err\n\t}\n\tghUser.loginName = login\n\n\tvar isContrib, isSponsor bool\n\n\tctx := context.Background()\n\tg, ctx := errgroup.WithContext(ctx)\n\n\tg.Go(func() error {\n\t\tisContrib, err = ghUser.loadContributorStatus(ctx)\n\t\treturn err\n\t})\n\n\tg.Go(func() error {\n\t\tisSponsor, err = ghUser.loadSponsorStatus(ctx)\n\t\treturn err\n\t})\n\n\terr = g.Wait()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tghUser.IsContributor = isContrib\n\tghUser.IsSponsor = isSponsor\n\n\treturn nil\n}\n\n// loadLoginName figures out the GitHub user's login name from their API key\nfunc (ghUser *GitHubUser) loadLoginName() (string, error) {\n\tuser, _, err := ghUser.clientV3.Users.Get(context.Background(), \"\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tlogin := user.GetLogin()\n\n\treturn login, nil\n}\n\n// loadContributorStatus figures out if this GitHub account has contributed to WTF\nfunc (ghUser *GitHubUser) loadContributorStatus(ctx context.Context) (bool, error) {\n\tpage := 1\n\tisContributor := false\n\n\tfor {\n\t\topts := &ghb.ListContributorsOptions{\n\t\t\tListOptions: ghb.ListOptions{\n\t\t\t\tPage:    page,\n\t\t\t\tPerPage: 100,\n\t\t\t},\n\t\t}\n\n\t\tcontributors, resp, err := ghUser.clientV3.Repositories.ListContributors(ctx, \"wtfutil\", \"wtf\", opts)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif resp.StatusCode != http.StatusOK || len(contributors) < 1 {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, contrib := range contributors {\n\t\t\tif contrib.GetLogin() == ghUser.loginName {\n\t\t\t\tisContributor = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tpage++\n\t}\n\n\treturn isContributor, nil\n}\n\n// loadSponsorStatus figures out if this GitHub account has sponsored WTF\nfunc (ghUser *GitHubUser) loadSponsorStatus(ctx context.Context) (bool, error) {\n\tvars := map[string]interface{}{\n\t\t\"loginName\": githubv4.String(ghUser.loginName),\n\t}\n\n\terr := ghUser.clientV4.Query(ctx, &sponsorQuery, vars)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tisSponsor := false\n\n\tfor _, spon := range sponsorQuery.User.SponsorshipsAsSponsor.Nodes {\n\t\tif spon.Sponsorable.SponsorsListing.Slug == \"sponsors-felicianotech\" {\n\t\t\tisSponsor = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn isSponsor, nil\n}\n\nfunc (ghUser *GitHubUser) verifyGitHubClients() error {\n\tif ghUser.clientV3 == nil {\n\t\treturn errors.New(\"github client v3 failed to load\")\n\t}\n\n\tif ghUser.clientV4 == nil {\n\t\treturn errors.New(\"github client v4 failed to load\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "utils/colors.go",
    "content": "package utils\n\nimport \"fmt\"\n\n// ColorizePercent provides a standard way to colorize percentages for which\n// large numbers are good (green) and small numbers are bad (red).\nfunc ColorizePercent(percent float64) string {\n\tvar color string\n\n\tswitch {\n\tcase percent >= 70:\n\t\tcolor = \"green\"\n\tcase percent >= 35:\n\t\tcolor = \"yellow\"\n\tcase percent < 0:\n\t\tcolor = \"grey\"\n\tdefault:\n\t\tcolor = \"red\"\n\t}\n\n\treturn fmt.Sprintf(\"[%s]%v[%s]\", color, percent, \"white\")\n}\n"
  },
  {
    "path": "utils/colors_test.go",
    "content": "package utils\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_ColorizePercent(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpercent  float64\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"with high percent\",\n\t\t\tpercent:  70,\n\t\t\texpected: \"[green]70[white]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with medium percent\",\n\t\t\tpercent:  35,\n\t\t\texpected: \"[yellow]35[white]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with low percent\",\n\t\t\tpercent:  1,\n\t\t\texpected: \"[red]1[white]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with negative percent\",\n\t\t\tpercent:  -5,\n\t\t\texpected: \"[grey]-5[white]\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := ColorizePercent(tt.percent)\n\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "utils/conversions.go",
    "content": "package utils\n\nimport (\n\t\"strconv\"\n)\n\n/* -------------------- Map Conversion -------------------- */\n\n// MapToStrs takes a map of interfaces and returns a map of strings\nfunc MapToStrs(aMap map[string]interface{}) map[string]string {\n\tresults := make(map[string]string, len(aMap))\n\n\tfor key, val := range aMap {\n\t\tresults[key] = val.(string)\n\t}\n\n\treturn results\n}\n\n/* -------------------- Slice Conversion -------------------- */\n\n// IntsToUints takes a slice of ints and returns a slice of uints\nfunc IntsToUints(slice []int) []uint {\n\tresults := make([]uint, len(slice))\n\n\tfor i, val := range slice {\n\t\tresults[i] = uint(val)\n\t}\n\n\treturn results\n}\n\n// ToInts takes a slice of interfaces and returns a slice of ints\nfunc ToInts(slice []interface{}) []int {\n\tresults := make([]int, len(slice))\n\n\tfor i, val := range slice {\n\t\tresults[i] = val.(int)\n\t}\n\n\treturn results\n}\n\n// ToStrs takes a slice of interfaces and returns a slice of strings\nfunc ToStrs(slice []interface{}) []string {\n\tresults := make([]string, len(slice))\n\n\tfor i, val := range slice {\n\t\tswitch t := val.(type) {\n\t\tcase int:\n\t\t\tresults[i] = strconv.Itoa(t)\n\t\tcase string:\n\t\t\tresults[i] = t\n\t\t}\n\t}\n\n\treturn results\n}\n\n// ToUints takes a slice of interfaces and returns a slice of ints\nfunc ToUints(slice []interface{}) []uint {\n\tresults := make([]uint, len(slice))\n\n\tfor i, val := range slice {\n\t\tresults[i] = val.(uint)\n\t}\n\n\treturn results\n}\n"
  },
  {
    "path": "utils/conversions_test.go",
    "content": "package utils\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_MapToStrs(t *testing.T) {\n\texpected := map[string]string{\n\t\t\"a\": \"a\",\n\t\t\"b\": \"b\",\n\t\t\"c\": \"c\",\n\t}\n\n\tsource := make(map[string]interface{})\n\tfor _, val := range expected {\n\t\tsource[val] = val\n\t}\n\n\tassert.Equal(t, expected, MapToStrs(source))\n}\n\nfunc Test_IntsToUints(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tsrc      []int\n\t\texpected []uint\n\t}{\n\t\t{\n\t\t\tname:     \"empty set\",\n\t\t\tsrc:      []int{},\n\t\t\texpected: []uint{},\n\t\t},\n\t\t{\n\t\t\tname:     \"full set\",\n\t\t\tsrc:      []int{1, 2, 3},\n\t\t\texpected: []uint{1, 2, 3},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := IntsToUints(tt.src)\n\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n\nfunc Test_ToInts(t *testing.T) {\n\texpected := []int{1, 2, 3}\n\n\tsource := make([]interface{}, len(expected))\n\tfor idx, val := range expected {\n\t\tsource[idx] = val\n\t}\n\n\tassert.Equal(t, expected, ToInts(source))\n}\n\nfunc Test_ToStrs(t *testing.T) {\n\texpectedInts := []int{1, 2, 3}\n\texpectedStrs := []string{\"1\", \"2\", \"3\"}\n\n\tfromInts := make([]interface{}, 3)\n\tfor idx, val := range expectedInts {\n\t\tfromInts[idx] = val\n\t}\n\n\tfromStrs := make([]interface{}, 3)\n\tfor idx, val := range expectedStrs {\n\t\tfromStrs[idx] = val\n\t}\n\n\tassert.Equal(t, expectedStrs, ToStrs(fromInts))\n\tassert.Equal(t, expectedStrs, ToStrs(fromStrs))\n}\n\nfunc Test_ToUints(t *testing.T) {\n\texpected := []uint{1, 2, 3}\n\n\tsource := make([]interface{}, len(expected))\n\tfor idx, val := range expected {\n\t\tsource[idx] = val\n\t}\n\n\tassert.Equal(t, expected, ToUints(source))\n}\n"
  },
  {
    "path": "utils/email_addresses.go",
    "content": "package utils\n\nimport (\n\t\"strings\"\n\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\n// NameFromEmail takes an email address and returns the part that comes before the @ symbol\n//\n// Example:\n//\n//\tNameFromEmail(\"test_user@example.com\")\n//\t> \"Test_user\"\nfunc NameFromEmail(email string) string {\n\tparts := strings.Split(email, \"@\")\n\tname := strings.ReplaceAll(parts[0], \".\", \" \")\n\n\tc := cases.Title(language.English)\n\treturn c.String(name)\n}\n\n// NamesFromEmails takes a slice of email addresses and returns a slice of the parts that\n// come before the @ symbol\n//\n// Example:\n//\n//\tNamesFromEmail(\"test_user@example.com\", \"other_user@example.com\")\n//\t> []string{\"Test_user\", \"Other_user\"}\nfunc NamesFromEmails(emails []string) []string {\n\tnames := make([]string, len(emails))\n\n\tfor i, email := range emails {\n\t\tnames[i] = NameFromEmail(email)\n\t}\n\n\treturn names\n}\n"
  },
  {
    "path": "utils/email_addresses_test.go",
    "content": "package utils\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_NameFromEmail(t *testing.T) {\n\tassert.Equal(t, \"\", NameFromEmail(\"\"))\n\tassert.Equal(t, \"Chris Cummer\", NameFromEmail(\"chris.cummer@me.com\"))\n}\n\nfunc Test_NamesFromEmails(t *testing.T) {\n\tvar result []string\n\n\tresult = NamesFromEmails([]string{})\n\tassert.Equal(t, []string{}, result)\n\n\tresult = NamesFromEmails([]string{\"chris.cummer@me.com\", \"chriscummer@me.com\"})\n\tassert.Equal(t, []string{\"Chris Cummer\", \"Chriscummer\"}, result)\n}\n"
  },
  {
    "path": "utils/help_parser.go",
    "content": "package utils\n\nimport (\n\t\"reflect\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc HelpFromInterface(item interface{}) string {\n\tresult := \"\"\n\tt := reflect.TypeOf(item)\n\n\tfor i := 0; i < t.NumField(); i++ {\n\t\tfield := t.Field(i)\n\n\t\tkind := field.Type.Kind()\n\t\tif field.Type.Kind() == reflect.Ptr {\n\t\t\tkind = field.Type.Elem().Kind()\n\t\t}\n\n\t\tif field.Name == \"Common\" {\n\t\t\tresult += HelpFromInterface(cfg.Common{})\n\t\t}\n\n\t\tswitch kind {\n\t\tcase reflect.Interface:\n\t\t\tresult += HelpFromInterface(field.Type.Elem())\n\t\tdefault:\n\t\t\tresult += helpFromValue(field)\n\t\t}\n\t}\n\n\treturn result\n}\n\n// StripColorTags removes tcell color tags from a given string\nfunc StripColorTags(input string) string {\n\topenColorRegex := regexp.MustCompile(`\\[.*?\\]`)\n\treturn openColorRegex.ReplaceAllString(input, \"\")\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc helpFromValue(field reflect.StructField) string {\n\tresult := \"\"\n\n\toptional, err := strconv.ParseBool(field.Tag.Get(\"optional\"))\n\tif err != nil {\n\t\toptional = false\n\t}\n\n\thelp := field.Tag.Get(\"help\")\n\tif optional {\n\t\thelp = \"Optional \" + help\n\t}\n\n\tvalues := field.Tag.Get(\"values\")\n\tif help != \"\" {\n\t\tresult += \"\\n\\n \" + lowercaseTitle(field.Name)\n\t\tresult += \"\\n \" + help\n\n\t\tif values != \"\" {\n\t\t\tresult += \"\\n Values: \" + values\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc lowercaseTitle(title string) string {\n\tif title == \"\" {\n\t\treturn \"\"\n\t}\n\tr, n := utf8.DecodeRuneInString(title)\n\treturn string(unicode.ToLower(r)) + title[n:]\n}\n"
  },
  {
    "path": "utils/homedir.go",
    "content": "// Package homedir helps with detecting and expanding the user's home directory\n\n// Copied (mostly) verbatim from https://github.com/Atrox/homedir\n\npackage utils\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// ExpandHomeDir expands the path to include the home directory if the path\n// is prefixed with `~`. If it isn't prefixed with `~`, the path is\n// returned as-is.\nfunc ExpandHomeDir(path string) (string, error) {\n\tif path == \"\" {\n\t\treturn path, nil\n\t}\n\n\tif path[0] != '~' {\n\t\treturn path, nil\n\t}\n\n\tif len(path) > 1 && path[1] != '/' && path[1] != '\\\\' {\n\t\treturn \"\", errors.New(\"cannot expand user-specific home dir\")\n\t}\n\n\tdir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn filepath.Join(dir, path[1:]), nil\n}\n"
  },
  {
    "path": "utils/homedir_test.go",
    "content": "package utils\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_ExpandHomeDir(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tpath             string\n\t\texpectedStart    string\n\t\texpectedContains string\n\t\texpectedError    error\n\t}{\n\t\t{\n\t\t\tname:             \"with empty path\",\n\t\t\tpath:             \"\",\n\t\t\texpectedStart:    \"\",\n\t\t\texpectedContains: \"\",\n\t\t\texpectedError:    nil,\n\t\t},\n\t\t{\n\t\t\tname:             \"with relative path\",\n\t\t\tpath:             \"~/test\",\n\t\t\texpectedStart:    \"/\",\n\t\t\texpectedContains: \"/test\",\n\t\t\texpectedError:    nil,\n\t\t},\n\t\t{\n\t\t\tname:             \"with absolute path\",\n\t\t\tpath:             \"/Users/test\",\n\t\t\texpectedStart:    \"/\",\n\t\t\texpectedContains: \"/test\",\n\t\t\texpectedError:    nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual, err := ExpandHomeDir(tt.path)\n\n\t\t\tif len(tt.path) > 0 {\n\t\t\t\tassert.Equal(t, tt.expectedStart, string(actual[0]))\n\t\t\t}\n\n\t\t\tassert.Contains(t, actual, tt.expectedContains)\n\t\t\tassert.Equal(t, tt.expectedError, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "utils/init.go",
    "content": "package utils\n\n// OpenFileUtil defines the system utility to use to open files\nvar OpenFileUtil = \"open\"\nvar OpenUrlUtil = []string{}\n\n// Init initializes global settings in the wtf package\nfunc Init(openFileUtil string, openUrlUtil []string) {\n\tOpenFileUtil = openFileUtil\n\tOpenUrlUtil = openUrlUtil\n}\n"
  },
  {
    "path": "utils/init_test.go",
    "content": "package utils\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_Init(t *testing.T) {\n\tInit(\"cats\", []string{\"dogs\"})\n\n\tassert.Equal(t, OpenFileUtil, \"cats\")\n\tassert.Equal(t, OpenUrlUtil, []string{\"dogs\"})\n}\n"
  },
  {
    "path": "utils/reflective.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\n// StringValueForProperty returns a string value for the given property\n// If the property doesn't exist, it returns an error\nfunc StringValueForProperty(ref interface{}, propName string) (string, error) {\n\tv := reflect.ValueOf(ref)\n\trefVal := reflect.Indirect(v).FieldByName(propName)\n\n\tif !refVal.IsValid() {\n\t\treturn \"\", fmt.Errorf(\"invalid property name: %s\", propName)\n\t}\n\n\tstrVal := fmt.Sprintf(\"%v\", refVal)\n\n\treturn strVal, nil\n}\n"
  },
  {
    "path": "utils/sums.go",
    "content": "package utils\n\n// SumInts takes a slice of ints and returns the sum of them\nfunc SumInts(vals []int) int {\n\tsum := 0\n\n\tfor _, a := range vals {\n\t\tsum += a\n\t}\n\n\treturn sum\n}\n"
  },
  {
    "path": "utils/sums_test.go",
    "content": "package utils\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_SumInts(t *testing.T) {\n\texpected := 6\n\tresult := SumInts([]int{1, 3, 2})\n\n\tassert.Equal(t, expected, result)\n\n\texpected = 46\n\tresult = SumInts([]int{4, 6, 7, 23, 6})\n\n\tassert.Equal(t, expected, result)\n\n\texpected = 4\n\tresult = SumInts([]int{4})\n\n\tassert.Equal(t, expected, result)\n}\n"
  },
  {
    "path": "utils/text.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\n\t\"golang.org/x/text/message\"\n\n\t\"github.com/rivo/tview\"\n)\n\n// CenterText takes a string and a width and pads the left and right of the string with\n// empty spaces to ensure that the string is in the middle of the returned value\n//\n// Example:\n//\n//\tx := CenterText(\"cat\", 11)\n//\t> \"    cat    \"\nfunc CenterText(str string, width int) string {\n\tif width < 0 {\n\t\twidth = 0\n\t}\n\n\treturn fmt.Sprintf(\"%[1]*s\", -width, fmt.Sprintf(\"%[1]*s\", (width+len(str))/2, str))\n}\n\n// FindBetween finds and returns the text between two strings\n//\n// Example:\n//\n//\ta := \"{ cat } { dog }\"\n//\tb := FindBetween(a, \"{\", \"}\")\n//\t> [\" cat \", \" dog \"]\nfunc FindBetween(input string, left string, right string) []string {\n\tout := []string{}\n\n\ti := 0\n\tfor i >= 0 {\n\t\ti = strings.Index(input, left)\n\t\tif i == -1 {\n\t\t\tbreak\n\t\t}\n\n\t\ti += len(left)\n\n\t\te := strings.Index(input[i:], right)\n\t\tif e == -1 {\n\t\t\tbreak\n\t\t}\n\n\t\tif e <= i {\n\t\t\tbreak\n\t\t}\n\n\t\tchunk := input[i : e+1]\n\t\tinput = input[i+e+1:]\n\n\t\tout = append(out, chunk)\n\n\t\ti = i + e\n\n\t}\n\n\treturn out\n}\n\n// HighlightableHelper pads the given text with blank spaces to the width of the view\n// containing it. This is helpful for extending row highlighting across the entire width\n// of the view\nfunc HighlightableHelper(view *tview.TextView, input string, idx, offset int) string {\n\t_, _, w, _ := view.GetInnerRect()\n\n\tfmtStr := fmt.Sprintf(`[\"%d\"][\"\"]`, idx)\n\tfmtStr += input\n\tfmtStr += RowPadding(offset, w)\n\tfmtStr += `[\"\"]` + \"\\n\"\n\n\treturn fmtStr\n}\n\n// RowPadding returns a padding for a row to make it the full width of the containing widget.\n// Useful for ensuring row highlighting spans the full width (I suspect tcell has a better\n// way to do this, but I haven't yet found it)\nfunc RowPadding(offset int, max int) string {\n\tpadSize := max - offset\n\tif padSize < 0 {\n\t\tpadSize = 0\n\t}\n\n\treturn strings.Repeat(\" \", padSize)\n}\n\n// Truncate chops a given string at len length. Appends an ellipse character if warranted\nfunc Truncate(src string, maxLen int, withEllipse bool) string {\n\tif len(src) < 1 || maxLen < 1 {\n\t\treturn \"\"\n\t}\n\n\tif maxLen == 1 {\n\t\treturn src[:1]\n\t}\n\n\tvar runeCount = 0\n\tfor idx := range src {\n\t\truneCount++\n\t\tif runeCount > maxLen {\n\t\t\tif withEllipse {\n\t\t\t\treturn src[:idx-1] + \"…\"\n\t\t\t}\n\n\t\t\treturn src[:idx]\n\t\t}\n\t}\n\treturn src\n}\n\n// PrettyNumber formats number as string with 1000 delimiters and, if necessary, rounds it to 2 decimals\nfunc PrettyNumber(prtr *message.Printer, number float64) string {\n\tif number == math.Trunc(number) {\n\t\treturn prtr.Sprintf(\"%.0f\", number)\n\t}\n\n\treturn prtr.Sprintf(\"%.2f\", number)\n}\n"
  },
  {
    "path": "utils/text_test.go",
    "content": "package utils\n\nimport (\n\t\"testing\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/text/language\"\n\t\"golang.org/x/text/message\"\n)\n\nfunc Test_CenterText(t *testing.T) {\n\tassert.Equal(t, \"cat\", CenterText(\"cat\", -9))\n\tassert.Equal(t, \"cat\", CenterText(\"cat\", 0))\n\tassert.Equal(t, \"   cat   \", CenterText(\"cat\", 9))\n}\n\nfunc Test_FindBetween(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\tleft     string\n\t\tright    string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"with empty params\",\n\t\t\tinput:    \"\",\n\t\t\tleft:     \"\",\n\t\t\tright:    \"\",\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"with empty input\",\n\t\t\tinput:    \"\",\n\t\t\tleft:     \"{\",\n\t\t\tright:    \"}\",\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"with empty bounds\",\n\t\t\tinput:    \"{cat}{dog}\",\n\t\t\tleft:     \"\",\n\t\t\tright:    \"\",\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"with no match left\",\n\t\t\tinput:    \"{cat}{dog}\",\n\t\t\tleft:     \"[\",\n\t\t\tright:    \"}\",\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"with no match right\",\n\t\t\tinput:    \"{cat}{dog}\",\n\t\t\tleft:     \"{\",\n\t\t\tright:    \"]\",\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"with right before left\",\n\t\t\tinput:    \"{cat}{dog}\",\n\t\t\tleft:     \"}\",\n\t\t\tright:    \"{\",\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"with no match\",\n\t\t\tinput:    \"{cat}{dog}\",\n\t\t\tleft:     \"[\",\n\t\t\tright:    \"]\",\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"with valid input\",\n\t\t\tinput:    \"{cat}{dog}\",\n\t\t\tleft:     \"{\",\n\t\t\tright:    \"}\",\n\t\t\texpected: []string{\"cat\", \"dog\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := FindBetween(tt.input, tt.left, tt.right)\n\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n\nfunc Test_HighlightableHelper(t *testing.T) {\n\tview := tview.NewTextView()\n\tactual := HighlightableHelper(view, \"cats\", 0, 5)\n\n\tassert.Equal(t, \"[\\\"0\\\"][\\\"\\\"]cats          [\\\"\\\"]\\n\", actual)\n}\n\nfunc Test_RowPadding(t *testing.T) {\n\tassert.Equal(t, \"\", RowPadding(0, 0))\n\tassert.Equal(t, \"\", RowPadding(5, 2))\n\tassert.Equal(t, \" \", RowPadding(1, 2))\n\tassert.Equal(t, \"     \", RowPadding(0, 5))\n}\n\nfunc Test_Truncate(t *testing.T) {\n\tassert.Equal(t, \"\", Truncate(\"cat\", 0, false))\n\tassert.Equal(t, \"c\", Truncate(\"cat\", 1, false))\n\tassert.Equal(t, \"ca\", Truncate(\"cat\", 2, false))\n\tassert.Equal(t, \"cat\", Truncate(\"cat\", 3, false))\n\tassert.Equal(t, \"cat\", Truncate(\"cat\", 4, false))\n\n\tassert.Equal(t, \"\", Truncate(\"cat\", 0, true))\n\tassert.Equal(t, \"c\", Truncate(\"cat\", 1, true))\n\tassert.Equal(t, \"c…\", Truncate(\"cat\", 2, true))\n\tassert.Equal(t, \"cat\", Truncate(\"cat\", 3, true))\n\tassert.Equal(t, \"cat\", Truncate(\"cat\", 4, true))\n\n\t// Only supports non-ellipsed emoji\n\tassert.Equal(t, \"🌮🚙\", Truncate(\"🌮🚙💥👾\", 2, false))\n}\n\nfunc Test_PrettyNumber(t *testing.T) {\n\tlocPrinter := message.NewPrinter(language.English)\n\n\tassert.Equal(t, \"1,000,000\", PrettyNumber(locPrinter, 1000000))\n\tassert.Equal(t, \"1,000,000.99\", PrettyNumber(locPrinter, 1000000.99))\n\tassert.Equal(t, \"1,000,000\", PrettyNumber(locPrinter, 1000000.00))\n\tassert.Equal(t, \"100,000\", PrettyNumber(locPrinter, 100000))\n\tassert.Equal(t, \"100,000.01\", PrettyNumber(locPrinter, 100000.009))\n\tassert.Equal(t, \"10,000\", PrettyNumber(locPrinter, 10000))\n\tassert.Equal(t, \"1,000\", PrettyNumber(locPrinter, 1000))\n\tassert.Equal(t, \"1,000\", PrettyNumber(locPrinter, 1000))\n\tassert.Equal(t, \"100\", PrettyNumber(locPrinter, 100))\n\tassert.Equal(t, \"0\", PrettyNumber(locPrinter, 0))\n\tassert.Equal(t, \"0.10\", PrettyNumber(locPrinter, 0.1))\n}\n"
  },
  {
    "path": "utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/logrusorgru/aurora/v4\"\n\t\"github.com/olebedev/config\"\n)\n\nconst (\n\tSimpleDateFormat    = \"Jan 2\"\n\tSimpleTimeFormat    = \"15:04 MST\"\n\tMinimumTimeFormat12 = \"3:04 PM\"\n\tMinimumTimeFormat24 = \"15:04\"\n\n\tFullDateFormat         = \"Monday, Jan 2\"\n\tFriendlyDateFormat     = \"Mon, Jan 2\"\n\tFriendlyDateTimeFormat = \"Mon, Jan 2, 15:04\"\n\n\tTimestampFormat = \"2006-01-02T15:04:05-0700\"\n)\n\n// DoesNotInclude takes a slice of strings and a target string and returns\n// TRUE if the slice does not include the target, FALSE if it does\n//\n// Example:\n//\n//\tx := DoesNotInclude([]string{\"cat\", \"dog\", \"rat\"}, \"dog\")\n//\t> false\n//\n//\tx := DoesNotInclude([]string{\"cat\", \"dog\", \"rat\"}, \"pig\")\n//\t> true\nfunc DoesNotInclude(strs []string, val string) bool {\n\treturn !Includes(strs, val)\n}\n\n// ExecuteCommand executes an external command on the local machine as the current user\nfunc ExecuteCommand(cmd *exec.Cmd) string {\n\tif cmd == nil {\n\t\treturn \"\"\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tcmd.Stdout = buf\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn err.Error()\n\t}\n\n\treturn buf.String()\n}\n\n// FindMatch takes a regex pattern and a string of data and returns back all the matches\n// in that string\nfunc FindMatch(pattern string, data string) [][]string {\n\tr := regexp.MustCompile(pattern)\n\treturn r.FindAllStringSubmatch(data, -1)\n}\n\n// Includes takes a slice of strings and a target string and returns\n// TRUE if the slice includes the target, FALSE if it does not\n//\n// Example:\n//\n//\tx := Includes([]string{\"cat\", \"dog\", \"rat\"}, \"dog\")\n//\t> true\n//\n//\tx := Includes([]string{\"cat\", \"dog\", \"rat\"}, \"pig\")\n//\t> false\nfunc Includes(strs []string, val string) bool {\n\tfor _, str := range strs {\n\t\tif val == str {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// OpenFile opens the file defined in `path` via the operating system\nfunc OpenFile(path string) {\n\tif (strings.HasPrefix(path, \"http://\")) || (strings.HasPrefix(path, \"https://\")) {\n\t\tif len(OpenUrlUtil) > 0 {\n\t\t\tcommands := append(OpenUrlUtil, path)\n\t\t\tcmd := exec.Command(commands[0], commands[1:]...)\n\t\t\terr := cmd.Start()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tvar cmd *exec.Cmd\n\t\tswitch runtime.GOOS {\n\t\tcase \"linux\":\n\t\t\tcmd = exec.Command(\"xdg-open\", path)\n\t\tcase \"windows\":\n\t\t\tcmd = exec.Command(\"rundll32\", \"url.dll,FileProtocolHandler\", path)\n\t\tcase \"darwin\":\n\t\t\tcmd = exec.Command(\"open\", path)\n\t\tdefault:\n\t\t\t// for the BSDs\n\t\t\tcmd = exec.Command(\"xdg-open\", path)\n\t\t}\n\n\t\terr := cmd.Start()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\treturn\n\t}\n\n\tfilePath, _ := ExpandHomeDir(path)\n\tcmd := exec.Command(OpenFileUtil, filePath)\n\tExecuteCommand(cmd)\n}\n\n// ReadFileBytes reads the contents of a file and returns those contents as a slice of bytes\nfunc ReadFileBytes(filePath string) ([]byte, error) {\n\tfileData, err := os.ReadFile(filepath.Clean(filePath))\n\tif err != nil {\n\t\treturn []byte{}, err\n\t}\n\n\treturn fileData, nil\n}\n\n// ParseJSON is a standard JSON reader from text\nfunc ParseJSON(obj interface{}, text io.Reader) error {\n\td := json.NewDecoder(text)\n\treturn d.Decode(obj)\n}\n\n// CalculateDimensions reads the module dimensions from the module and global config. The border is already subtracted.\nfunc CalculateDimensions(moduleConfig, globalConfig *config.Config) (int, int, error) {\n\tgrid, err := globalConfig.Get(\"wtf.grid\")\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\n\tcols := ToInts(grid.UList(\"columns\"))\n\trows := ToInts(grid.UList(\"rows\"))\n\n\t// If they're defined in the config, they cannot be empty\n\tif len(cols) == 0 || len(rows) == 0 {\n\t\tdisplayGridConfigError()\n\t\tos.Exit(1)\n\t}\n\n\t// Read the source data from the config\n\tleft := moduleConfig.UInt(\"position.left\", 0)\n\ttop := moduleConfig.UInt(\"position.top\", 0)\n\twidth := moduleConfig.UInt(\"position.width\", 0)\n\theight := moduleConfig.UInt(\"position.height\", 0)\n\n\t// Make sure the values are in bounds\n\tleft = Clamp(left, 0, len(cols)-1)\n\ttop = Clamp(top, 0, len(rows)-1)\n\twidth = Clamp(width, 0, len(cols)-left)\n\theight = Clamp(height, 0, len(rows)-top)\n\n\t// Start with the border subtracted and add all the spanned rows and cols\n\tw, h := -2, -2\n\tfor _, x := range cols[left : left+width] {\n\t\tw += x\n\t}\n\tfor _, y := range rows[top : top+height] {\n\t\th += y\n\t}\n\n\t// The usable space may be empty\n\tw = MaxInt(w, 0)\n\th = MaxInt(h, 0)\n\n\treturn w, h, nil\n}\n\n// MaxInt returns the larger of x or y\n//\n// Examples:\n//\n//\tMaxInt(3, 2) => 3\n//\tMaxInt(2, 3) => 3\nfunc MaxInt(x, y int) int {\n\tif x > y {\n\t\treturn x\n\t}\n\treturn y\n}\n\n// Clamp restricts values to a minimum and maximum value\n//\n// Examples:\n//\n//\tclamp(6, 3, 8) => 6\n//\tclamp(1, 3, 8) => 3\n//\tclamp(9, 3, 8) => 8\nfunc Clamp(x, a, b int) int {\n\tif a > x {\n\t\treturn a\n\t}\n\tif b < x {\n\t\treturn b\n\t}\n\treturn x\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc displayGridConfigError() {\n\tfmt.Printf(\"\\n%s 'grid' config values are invalid. 'columns' and 'rows' cannot be empty.\\n\", aurora.Red(\"ERROR\"))\n\tfmt.Println()\n\tfmt.Println(\"This is invalid:\")\n\tfmt.Println()\n\tfmt.Println(\"  grid:\")\n\tfmt.Println(\"    columns: []\")\n\tfmt.Println(\"    rows: []\")\n\tfmt.Println()\n\tfmt.Printf(\"%s If you want the columns and rows to be dynamically-determined, remove the 'grid' key and child keys from your config file.\\n\", aurora.Yellow(\"*\"))\n\tfmt.Printf(\"%s If you want explicit widths and heights, add integer values to the 'columns' and 'rows' arrays.\\n\", aurora.Yellow(\"*\"))\n\tfmt.Println()\n}\n"
  },
  {
    "path": "utils/utils_test.go",
    "content": "package utils\n\nimport (\n\t\"os/exec\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_DoesNotInclude(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tstrs     []string\n\t\tval      string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"when included\",\n\t\t\tstrs:     []string{\"a\", \"b\", \"c\"},\n\t\t\tval:      \"b\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"when not included\",\n\t\t\tstrs:     []string{\"a\", \"b\", \"c\"},\n\t\t\tval:      \"f\",\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := DoesNotInclude(tt.strs, tt.val)\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %t\\n     got: %t\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_ExecuteCommand(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcmd      *exec.Cmd\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"with nil command\",\n\t\t\tcmd:      nil,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with defined command\",\n\t\t\tcmd:      exec.Command(\"echo\", \"cats\"),\n\t\t\texpected: \"cats\\n\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := ExecuteCommand(tt.cmd)\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %s\\n     got: %s\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_FindMatch(t *testing.T) {\n\texpected := [][]string{{\"SSID: 7E5B5C\", \"7E5B5C\"}}\n\tresult := FindMatch(`s*SSID: (.+)s*`, \"SSID: 7E5B5C\")\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc Test_Includes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tstrs     []string\n\t\tval      string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"when included\",\n\t\t\tstrs:     []string{\"a\", \"b\", \"c\"},\n\t\t\tval:      \"b\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"when not included\",\n\t\t\tstrs:     []string{\"a\", \"b\", \"c\"},\n\t\t\tval:      \"f\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := Includes(tt.strs, tt.val)\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %t\\n     got: %t\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_ReadFileBytes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfile     string\n\t\texpected []byte\n\t}{\n\t\t{\n\t\t\tname:     \"with non-existent file\",\n\t\t\tfile:     \"/tmp/junk-daa6bf613f4c.md\",\n\t\t\texpected: []byte{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual, _ := ReadFileBytes(tt.file)\n\n\t\t\tif reflect.DeepEqual(tt.expected, actual) == false {\n\t\t\t\tt.Errorf(\"\\nexpected: %q\\n     got: %q\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_MaxInt(t *testing.T) {\n\texpected := 3\n\tresult := MaxInt(3, 2)\n\n\tassert.Equal(t, expected, result)\n\n\texpected = 3\n\tresult = MaxInt(2, 3)\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc Test_Clamp(t *testing.T) {\n\texpected := 6\n\tresult := Clamp(6, 3, 8)\n\n\tassert.Equal(t, expected, result)\n\n\texpected = 3\n\tresult = Clamp(1, 3, 8)\n\n\tassert.Equal(t, expected, result)\n\n\texpected = 8\n\tresult = Clamp(9, 3, 8)\n\n\tassert.Equal(t, expected, result)\n}\n"
  },
  {
    "path": "view/bargraph.go",
    "content": "package view\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\n// BarGraph defines the data required to make a bar graph\ntype BarGraph struct {\n\tmaxStars int\n\tstarChar string\n\n\t*Base\n\t*KeyboardWidget\n\n\tView *tview.TextView\n}\n\n// Bar defines a single row in the bar graph\ntype Bar struct {\n\tLabel      string\n\tPercent    int\n\tValueLabel string\n\tLabelColor string\n}\n\n// NewBarGraph creates and returns an instance of BarGraph\nfunc NewBarGraph(tviewApp *tview.Application, redrawChan chan bool, _ string, commonSettings *cfg.Common) BarGraph {\n\twidget := BarGraph{\n\t\tBase:           NewBase(tviewApp, redrawChan, nil, commonSettings),\n\t\tKeyboardWidget: NewKeyboardWidget(commonSettings),\n\n\t\tmaxStars: commonSettings.Config.UInt(\"graphStars\", 20),\n\t\tstarChar: commonSettings.Config.UString(\"graphIcon\", \"|\"),\n\t}\n\n\twidget.View = widget.createView(widget.bordered)\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// BuildBars will build a string of * to represent your data of [time][value]\n// time should be passed as a int64\nfunc (widget *BarGraph) BuildBars(data []Bar) {\n\twidget.View.SetText(BuildStars(data, widget.maxStars, widget.starChar))\n\twidget.RedrawChan <- true\n}\n\n// BuildStars build the string to display\nfunc BuildStars(data []Bar, maxStars int, starChar string) string {\n\tvar buffer bytes.Buffer\n\n\t// the number of characters in the longest label\n\tvar longestLabel int\n\n\t//just getting min and max values\n\tfor _, bar := range data {\n\t\tif len(bar.Label) > longestLabel {\n\t\t\tlongestLabel = len(bar.Label)\n\t\t}\n\t}\n\n\t// each number = how many stars?\n\tvar starRatio = float64(maxStars) / 100\n\n\t//build the stars\n\tfor _, bar := range data {\n\t\t//how many stars for this one?\n\t\tvar starCount = int(float64(bar.Percent) * starRatio)\n\n\t\tlabel := bar.ValueLabel\n\t\tif label == \"\" {\n\t\t\tlabel = fmt.Sprint(bar.Percent)\n\t\t}\n\n\t\tlabelColor := bar.LabelColor\n\t\tif labelColor == \"\" {\n\t\t\tlabelColor = \"default\"\n\t\t}\n\n\t\t//write the line\n\t\t_, err := fmt.Fprintf(\n\t\t\t&buffer,\n\t\t\t\"%s%s[[%s]%s[default]%s] %s\\n\",\n\t\t\tbar.Label,\n\t\t\tstrings.Repeat(\" \", longestLabel-len(bar.Label)),\n\t\t\tlabelColor,\n\t\t\tstrings.Repeat(starChar, starCount),\n\t\t\tstrings.Repeat(\" \", maxStars-starCount),\n\t\t\tlabel,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t}\n\n\treturn buffer.String()\n}\n\nfunc (widget *BarGraph) TextView() *tview.TextView {\n\treturn widget.View\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *BarGraph) createView(bordered bool) *tview.TextView {\n\tview := tview.NewTextView()\n\n\tview.SetBackgroundColor(wtf.ColorFor(widget.commonSettings.Colors.Background))\n\tview.SetBorder(bordered)\n\tview.SetBorderColor(wtf.ColorFor(widget.BorderColor()))\n\tview.SetDynamicColors(true)\n\tview.SetTitle(widget.ContextualTitle(widget.CommonSettings().Title))\n\tview.SetTitleColor(wtf.ColorFor(widget.commonSettings.Colors.Title))\n\tview.SetWrap(false)\n\n\treturn view\n}\n"
  },
  {
    "path": "view/bargraph_test.go",
    "content": "package view\n\nimport (\n\t\"testing\"\n\n\t\"github.com/olebedev/config\"\n\t\"github.com/rivo/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\n// MakeData - Create sample data\nfunc makeData() []Bar {\n\t//this could come from config\n\tconst lineCount = 3\n\tvar stats [lineCount]Bar\n\n\tstats[0] = Bar{\n\t\tLabel:   \"Jun 27, 2018\",\n\t\tPercent: 20,\n\t}\n\n\tstats[1] = Bar{\n\t\tLabel:      \"Jul 09, 2018\",\n\t\tPercent:    80,\n\t\tLabelColor: \"red\",\n\t}\n\n\tstats[2] = Bar{\n\t\tLabel:      \"Jul 09, 2018\",\n\t\tPercent:    80,\n\t\tLabelColor: \"green\",\n\t}\n\n\treturn stats[:]\n}\n\nfunc newTestGraph(graphStars int, graphIcon string) *BarGraph {\n\twidget := NewBarGraph(\n\t\ttview.NewApplication(),\n\t\tmake(chan bool),\n\t\t\"testapp\",\n\t\t&cfg.Common{\n\t\t\tConfig: &config.Config{\n\t\t\t\tRoot: map[string]interface{}{\n\t\t\t\t\t\"graphStars\": graphStars,\n\t\t\t\t\t\"graphIcon\":  graphIcon,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n\treturn &widget\n}\n\nfunc Test_NewBarGraph(t *testing.T) {\n\twidget := newTestGraph(15, \"|\")\n\n\tassert.NotNil(t, widget.View)\n\tassert.Equal(t, 15, widget.maxStars)\n\tassert.Equal(t, \"|\", widget.starChar)\n}\n\n// func Test_BuildBars(t *testing.T) {\n// \twidget := newTestGraph(15, \"|\")\n\n// \tbefore := widget.View.GetText(false)\n// \twidget.BuildBars(makeData())\n// \tafter := widget.View.GetText(false)\n\n// \tassert.NotEqual(t, before, after)\n// }\n\nfunc Test_TextView(t *testing.T) {\n\twidget := newTestGraph(15, \"|\")\n\n\tassert.NotNil(t, widget.TextView())\n}\n\nfunc Test_BuildStars(t *testing.T) {\n\tresult := BuildStars(makeData(), 20, \"*\")\n\tassert.Equal(t,\n\t\t\"Jun 27, 2018[[default]****[default]                ] 20\\nJul 09, 2018[[red]****************[default]    ] 80\\nJul 09, 2018[[green]****************[default]    ] 80\\n\",\n\t\tresult,\n\t)\n}\n"
  },
  {
    "path": "view/base.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\ntype Base struct {\n\tbordered        bool\n\tcommonSettings  *cfg.Common\n\tenabled         bool\n\tenabledMutex    *sync.Mutex\n\tfocusChar       string\n\tfocusable       bool\n\thelpTextFunc    func() string\n\tname            string\n\tpages           *tview.Pages\n\tquitChan        chan bool\n\trefreshInterval time.Duration\n\trefreshing      bool\n\ttviewApp        *tview.Application\n\tview            *tview.TextView\n\n\tRedrawChan chan bool\n}\n\n// NewBase creates and returns an instance of the Base module, the lowest-level\n// primitive module from which all others are derived\nfunc NewBase(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, commonSettings *cfg.Common) *Base {\n\tbase := &Base{\n\t\tcommonSettings: commonSettings,\n\n\t\tbordered:        commonSettings.Bordered,\n\t\tenabled:         commonSettings.Enabled,\n\t\tenabledMutex:    &sync.Mutex{},\n\t\tfocusChar:       commonSettings.FocusChar(),\n\t\tfocusable:       commonSettings.Focusable,\n\t\tname:            commonSettings.Name,\n\t\tpages:           pages,\n\t\tquitChan:        make(chan bool),\n\t\trefreshInterval: commonSettings.RefreshInterval,\n\t\trefreshing:      false,\n\t\ttviewApp:        tviewApp,\n\n\t\tRedrawChan: redrawChan,\n\t}\n\n\treturn base\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// Bordered returns whether or not this widget should be drawn with a border\nfunc (base *Base) Bordered() bool {\n\treturn base.bordered\n}\n\n// BorderColor returns the color that the border of this widget should be drawn in\nfunc (base *Base) BorderColor() string {\n\tif base.Focusable() {\n\t\treturn base.commonSettings.Colors.Focusable\n\t}\n\n\treturn base.commonSettings.Colors.Unfocusable\n}\n\nfunc (base *Base) CommonSettings() *cfg.Common {\n\treturn base.commonSettings\n}\n\nfunc (base *Base) ConfigText() string {\n\treturn utils.HelpFromInterface(cfg.Common{})\n}\n\nfunc (base *Base) ContextualTitle(defaultStr string) string {\n\tswitch {\n\tcase defaultStr == \"\" && base.FocusChar() == \"\":\n\t\treturn \"\"\n\tcase defaultStr != \"\" && base.FocusChar() == \"\":\n\t\treturn fmt.Sprintf(\" %s \", defaultStr)\n\tcase defaultStr == \"\" && base.FocusChar() != \"\":\n\t\treturn fmt.Sprintf(\" [darkgray::u]%s[::-][white] \", base.FocusChar())\n\t}\n\n\treturn fmt.Sprintf(\" %s [darkgray::u]%s[::-][white] \", defaultStr, base.FocusChar())\n}\n\nfunc (base *Base) Disable() {\n\tbase.enabledMutex.Lock()\n\tbase.enabled = false\n\tbase.enabledMutex.Unlock()\n}\n\nfunc (base *Base) Disabled() bool {\n\tbase.enabledMutex.Lock()\n\tresult := !base.enabled\n\tbase.enabledMutex.Unlock()\n\treturn result\n}\n\nfunc (base *Base) Enabled() bool {\n\tbase.enabledMutex.Lock()\n\tresult := base.enabled\n\tbase.enabledMutex.Unlock()\n\treturn result\n}\n\nfunc (base *Base) Focusable() bool {\n\tbase.enabledMutex.Lock()\n\tresult := base.enabled && base.focusable\n\tbase.enabledMutex.Unlock()\n\treturn result\n}\n\nfunc (base *Base) FocusChar() string {\n\treturn base.focusChar\n}\n\nfunc (base *Base) Name() string {\n\treturn base.name\n}\n\nfunc (base *Base) QuitChan() chan bool {\n\treturn base.quitChan\n}\n\n// Refreshing returns TRUE if the base is currently refreshing its data, FALSE if it is not\nfunc (base *Base) Refreshing() bool {\n\treturn base.refreshing\n}\n\n// RefreshInterval returns how often the base will return its data\nfunc (base *Base) RefreshInterval() time.Duration {\n\treturn base.refreshInterval\n}\n\nfunc (base *Base) SetFocusChar(char string) {\n\tbase.focusChar = char\n}\n\n// SetView assigns the passed-in tview.TextView view to this widget\nfunc (base *Base) SetView(view *tview.TextView) {\n\tbase.view = view\n}\n\n// ShowHelp displays the modal help dialog for a module\nfunc (base *Base) ShowHelp() {\n\tif base.pages == nil {\n\t\treturn\n\t}\n\n\tcloseFunc := func() {\n\t\tbase.pages.RemovePage(\"help\")\n\t\tbase.tviewApp.SetFocus(base.view)\n\t}\n\n\tmodal := NewBillboardModal(base.helpTextFunc(), closeFunc)\n\n\tbase.pages.AddPage(\"help\", modal, false, true)\n\tbase.tviewApp.SetFocus(modal)\n\n\t// Tell the app to force redraw the screen\n\tbase.RedrawChan <- true\n}\n\nfunc (base *Base) Stop() {\n\tbase.enabledMutex.Lock()\n\tbase.enabled = false\n\tbase.enabledMutex.Unlock()\n\tbase.quitChan <- true\n}\n\nfunc (base *Base) String() string {\n\treturn base.name\n}\n"
  },
  {
    "path": "view/base_test.go",
    "content": "package view\n\nimport (\n\t\"testing\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nfunc Benchmark_ContextualTitle(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbase := NewBase(\n\t\ttview.NewApplication(),\n\t\tmake(chan bool),\n\t\ttview.NewPages(),\n\t\t&cfg.Common{},\n\t)\n\tbase.SetFocusChar(\"a\")\n\n\tdefaultStr := \"This is test\"\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = base.ContextualTitle(defaultStr)\n\t}\n}\n\nfunc Test_ContextualTitle(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tdefaultStr string\n\t\tfocusChar  string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tname:       \"with empty defaultStr and empty focusChar\",\n\t\t\tdefaultStr: \"\",\n\t\t\tfocusChar:  \"\",\n\t\t\texpected:   \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"with valid defaultStr and empty focusChar\",\n\t\t\tdefaultStr: \"cats\",\n\t\t\tfocusChar:  \"\",\n\t\t\texpected:   \" cats \",\n\t\t},\n\t\t{\n\t\t\tname:       \"with empty defaultStr and valid focusChar\",\n\t\t\tdefaultStr: \"\",\n\t\t\tfocusChar:  \"a\",\n\t\t\texpected:   \" [darkgray::u]a[::-][white] \",\n\t\t},\n\t\t{\n\t\t\tname:       \"with valid defaultStr and valid focusChar\",\n\t\t\tdefaultStr: \"cats\",\n\t\t\tfocusChar:  \"a\",\n\t\t\texpected:   \" cats [darkgray::u]a[::-][white] \",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbase := NewBase(\n\t\t\t\ttview.NewApplication(),\n\t\t\t\tmake(chan bool),\n\t\t\t\ttview.NewPages(),\n\t\t\t\t&cfg.Common{},\n\t\t\t)\n\t\t\tbase.SetFocusChar(tt.focusChar)\n\n\t\t\tactual := base.ContextualTitle(tt.defaultStr)\n\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "view/billboard_modal.go",
    "content": "package view\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n)\n\nconst offscreen = -1000\nconst modalWidth = 80\nconst modalHeight = 22\n\n// NewBillboardModal creates and returns a modal dialog suitable for displaying\n// a wall of text\n// An example of this is the keyboard help modal that shows up for all widgets\n// that support keyboard control when '/' is pressed\nfunc NewBillboardModal(text string, closeFunc func()) *tview.Frame {\n\tkeyboardIntercept := func(event *tcell.EventKey) *tcell.EventKey {\n\t\tif string(event.Rune()) == \"/\" {\n\t\t\tcloseFunc()\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyEsc:\n\t\t\tcloseFunc()\n\t\t\treturn nil\n\t\tcase tcell.KeyTab:\n\t\t\treturn nil\n\t\tdefault:\n\t\t\treturn event\n\t\t}\n\t}\n\n\ttextView := tview.NewTextView()\n\ttextView.SetDynamicColors(true)\n\ttextView.SetInputCapture(keyboardIntercept)\n\ttextView.SetText(text)\n\ttextView.SetWrap(true)\n\n\tframe := tview.NewFrame(textView)\n\tframe.SetRect(offscreen, offscreen, modalWidth, modalHeight)\n\n\tdrawFunc := func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {\n\t\tw, h := screen.Size()\n\t\tframe.SetRect((w/2)-(width/2), (h/2)-(height/2), width, height)\n\t\treturn x, y, width, height\n\t}\n\n\tframe.SetBorder(true)\n\tframe.SetBorders(1, 1, 0, 0, 1, 1)\n\tframe.SetDrawFunc(drawFunc)\n\n\treturn frame\n}\n"
  },
  {
    "path": "view/info_table.go",
    "content": "package view\n\nimport (\n\t\"bytes\"\n\t\"sort\"\n\n\t\"github.com/olekukonko/tablewriter\"\n)\n\n/*\n\tAn InfoTable is a two-column table of properties/values:\n\n\t-------------------------- -------------------------------------------------\n\t\t\t PROPERTY                                VALUE\n\t-------------------------- -------------------------------------------------\n\t CPUs                       1\n\t Created                    2019-12-12T18:39:09Z\n\t Disk                       25\n\t Features                   ipv6\n\t Image                      18.04.3 (LTS) x64 (Ubuntu)\n\t Memory                     1024\n\t Region                     Toronto 1 (tor1)\n\t-------------------------- -------------------------------------------------\n*/\n\n// InfoTable contains the internal guts of an InfoTable\ntype InfoTable struct {\n\tbuf       *bytes.Buffer\n\ttblWriter *tablewriter.Table\n}\n\n// NewInfoTable creates and returns the stringified contents of a two-column table\nfunc NewInfoTable(headers []string, dataMap map[string]string, colWidth0, colWidth1, tableHeight int) *InfoTable {\n\ttbl := &InfoTable{\n\t\tbuf: new(bytes.Buffer),\n\t}\n\n\ttbl.tblWriter = tablewriter.NewWriter(tbl.buf)\n\n\ttbl.tblWriter.SetHeader(headers)\n\ttbl.tblWriter.SetBorder(true)\n\ttbl.tblWriter.SetCenterSeparator(\" \")\n\ttbl.tblWriter.SetColumnSeparator(\" \")\n\ttbl.tblWriter.SetRowSeparator(\"-\")\n\ttbl.tblWriter.SetAlignment(tablewriter.ALIGN_LEFT)\n\ttbl.tblWriter.SetColMinWidth(0, colWidth0)\n\ttbl.tblWriter.SetColMinWidth(1, colWidth1)\n\n\tkeys := []string{}\n\tfor key := range dataMap {\n\t\tkeys = append(keys, key)\n\t}\n\n\tsort.Strings(keys)\n\n\t// Enumerate over the alphabetically-sorted keys to render the property values\n\tfor _, key := range keys {\n\t\ttbl.tblWriter.Append([]string{key, dataMap[key]})\n\t}\n\n\t// Pad the table with extra rows to push it to the bottom\n\tpaddingAmt := tableHeight - len(dataMap) - 1\n\tif paddingAmt > 0 {\n\t\tfor i := 0; i < paddingAmt; i++ {\n\t\t\ttbl.tblWriter.Append([]string{\"\", \"\"})\n\t\t}\n\t}\n\n\treturn tbl\n}\n\n// Render returns the stringified version of the table\nfunc (tbl *InfoTable) Render() string {\n\ttbl.tblWriter.Render()\n\treturn tbl.buf.String()\n}\n"
  },
  {
    "path": "view/info_table_test.go",
    "content": "package view\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc makeMap() map[string]string {\n\tm := make(map[string]string)\n\tm[\"foo\"] = \"val1\"\n\tm[\"bar\"] = \"val2\"\n\tm[\"baz\"] = \"val3\"\n\n\treturn m\n}\n\nfunc newTestTable(height, colWidth0, colWidth1 int) *InfoTable {\n\tvar headers [2]string\n\n\theaders[0] = \"hdr0\"\n\theaders[1] = \"hdr1\"\n\n\ttable := NewInfoTable(\n\t\theaders[:],\n\t\tmakeMap(),\n\t\tcolWidth0,\n\t\tcolWidth1,\n\t\theight,\n\t)\n\treturn table\n}\n\nfunc Test_RenderSimpleInfoTable(t *testing.T) {\n\ttable := newTestTable(4, 1, 1).Render()\n\n\tassert.Equal(t, \" ----- ------ \\n  HDR0   HDR1  \\n ----- ------ \\n  bar   val2  \\n  baz   val3  \\n  foo   val1  \\n ----- ------ \\n\", table)\n}\n\nfunc Test_RenderPaddedInfoTable(t *testing.T) {\n\ttable := newTestTable(6, 1, 1).Render()\n\n\tassert.Equal(t, \" ----- ------ \\n  HDR0   HDR1  \\n ----- ------ \\n  bar   val2  \\n  baz   val3  \\n  foo   val1  \\n              \\n              \\n ----- ------ \\n\", table)\n}\n\nfunc Test_RenderWithSpecifiedWidthLeftColumn(t *testing.T) {\n\ttable := newTestTable(4, 10, 1).Render()\n\n\tassert.Equal(t, \" ------------ ------ \\n     HDR0      HDR1  \\n ------------ ------ \\n  bar          val2  \\n  baz          val3  \\n  foo          val1  \\n ------------ ------ \\n\", table)\n}\n\nfunc Test_RenderWithSpecifiedWidthRightColumn(t *testing.T) {\n\ttable := newTestTable(4, 1, 10).Render()\n\n\tassert.Equal(t, \" ----- ------------ \\n  HDR0      HDR1     \\n ----- ------------ \\n  bar   val2        \\n  baz   val3        \\n  foo   val1        \\n ----- ------------ \\n\", table)\n}\n\nfunc Test_RenderWithSpecifiedWidthBothColumns(t *testing.T) {\n\ttable := newTestTable(4, 15, 10).Render()\n\n\tassert.Equal(t, \" ----------------- ------------ \\n       HDR0            HDR1     \\n ----------------- ------------ \\n  bar               val2        \\n  baz               val3        \\n  foo               val1        \\n ----------------- ------------ \\n\", table)\n}\n"
  },
  {
    "path": "view/keyboard_widget.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\nconst helpKeyChar = \"/\"\nconst refreshKeyChar = \"r\"\n\ntype helpItem struct {\n\tKey  string\n\tText string\n}\n\n// KeyboardWidget manages keyboard control for a widget\ntype KeyboardWidget struct {\n\tsettings *cfg.Common\n\n\tcharMap  map[string]func()\n\tkeyMap   map[tcell.Key]func()\n\tcharHelp []helpItem\n\tkeyHelp  []helpItem\n\tmaxKey   int\n}\n\n// NewKeyboardWidget creates and returns a new instance of KeyboardWidget\n// func NewKeyboardWidget(tviewApp *tview.Application, pages *tview.Pages, settings *cfg.Common) *KeyboardWidget {\nfunc NewKeyboardWidget(settings *cfg.Common) *KeyboardWidget {\n\tkeyWidget := &KeyboardWidget{\n\t\tsettings: settings,\n\t\tcharMap:  make(map[string]func()),\n\t\tkeyMap:   make(map[tcell.Key]func()),\n\t\tcharHelp: []helpItem{},\n\t\tkeyHelp:  []helpItem{},\n\t}\n\n\tkeyWidget.initializeCommonKeyboardControls()\n\n\treturn keyWidget\n}\n\n/* -------------------- Exported Functions --------------------- */\n\n// AssignedChars returns a list of all the text characters assigned to an operation\nfunc (widget *KeyboardWidget) AssignedChars() []string {\n\tchars := []string{}\n\n\tfor char := range widget.charMap {\n\t\tchars = append(chars, char)\n\t}\n\n\treturn chars\n}\n\n// HelpText returns the help text and keyboard command info for this widget\nfunc (widget *KeyboardWidget) HelpText() string {\n\tc := cases.Title(language.English)\n\tstr := \" [green::b]Keyboard commands for \" + c.String(widget.settings.Type) + \"[white]\\n\\n\"\n\n\tfor _, item := range widget.charHelp {\n\t\tstr += fmt.Sprintf(\"  %s\\t%s\\n\", item.Key, item.Text)\n\t}\n\n\tstr += \"\\n\\n\"\n\n\tfor _, item := range widget.keyHelp {\n\t\tstr += fmt.Sprintf(\"  %-*s\\t%s\\n\", widget.maxKey, item.Key, item.Text)\n\t}\n\n\treturn str\n}\n\n// InitializeHelpTextKeyboardControl assigns the function that displays help text to the\n// common help text key value\nfunc (widget *KeyboardWidget) InitializeHelpTextKeyboardControl(helpFunc func()) {\n\tif helpFunc != nil {\n\t\twidget.SetKeyboardChar(helpKeyChar, helpFunc, \"Show/hide this help prompt\")\n\t}\n}\n\n// InitializeRefreshKeyboardControl assigns the module's explicit refresh function to\n// the commom refresh key value\nfunc (widget *KeyboardWidget) InitializeRefreshKeyboardControl(refreshFunc func()) {\n\tif refreshFunc != nil {\n\t\twidget.SetKeyboardChar(refreshKeyChar, refreshFunc, \"Refresh widget\")\n\t}\n}\n\n// InputCapture is the function passed to tview's SetInputCapture() function\n// This is done during the main widget's creation process using the following code:\n//\n//\twidget.View.SetInputCapture(widget.InputCapture)\nfunc (widget *KeyboardWidget) InputCapture(event *tcell.EventKey) *tcell.EventKey {\n\tif event == nil {\n\t\treturn nil\n\t}\n\n\tfn := widget.charMap[string(event.Rune())]\n\tif fn != nil {\n\t\tfn()\n\t\treturn nil\n\t}\n\n\tfn = widget.keyMap[event.Key()]\n\tif fn != nil {\n\t\tfn()\n\t\treturn nil\n\t}\n\n\treturn event\n}\n\n// LaunchDocumentation opens the module docs in a browser\nfunc (widget *KeyboardWidget) LaunchDocumentation() {\n\tpath := widget.settings.DocPath\n\tif path == \"\" {\n\t\tpath = widget.settings.Type\n\t}\n\n\turl := \"https://wtfutil.com/modules/\" + path\n\tutils.OpenFile(url)\n}\n\n// SetKeyboardChar sets a character/function combination that responds to key presses\n// Example:\n//\n//\twidget.SetKeyboardChar(\"d\", widget.deleteSelectedItem)\nfunc (widget *KeyboardWidget) SetKeyboardChar(char string, fn func(), helpText string) {\n\tif char == \"\" {\n\t\treturn\n\t}\n\n\t// Check to ensure that the key trying to be used isn't already being used for something\n\tif _, ok := widget.charMap[char]; ok {\n\t\tpanic(fmt.Sprintf(\"Key is already mapped to a keyboard command: %s\\n\", char))\n\t}\n\n\twidget.charMap[char] = fn\n\twidget.charHelp = append(widget.charHelp, helpItem{char, helpText})\n}\n\n// SetKeyboardKey sets a tcell.Key/function combination that responds to key presses\n// Example:\n//\n//\twidget.SetKeyboardKey(tcell.KeyCtrlD, widget.deleteSelectedItem)\nfunc (widget *KeyboardWidget) SetKeyboardKey(key tcell.Key, fn func(), helpText string) {\n\twidget.keyMap[key] = fn\n\twidget.keyHelp = append(widget.keyHelp, helpItem{tcell.KeyNames[key], helpText})\n\n\tif len(tcell.KeyNames[key]) > widget.maxKey {\n\t\twidget.maxKey = len(tcell.KeyNames[key])\n\t}\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\n// initializeCommonKeyboardControls sets up the keyboard controls that are common to\n// all widgets that accept keyboard input\nfunc (widget *KeyboardWidget) initializeCommonKeyboardControls() {\n\twidget.SetKeyboardChar(\"\\\\\", widget.LaunchDocumentation, \"Open the documentation for this module in a browser\")\n}\n"
  },
  {
    "path": "view/keyboard_widget_test.go",
    "content": "package view\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nfunc test() {}\n\nfunc testKeyboardWidget() *KeyboardWidget {\n\tkeyWid := NewKeyboardWidget(\n\t\t&cfg.Common{\n\t\t\tModule: cfg.Module{\n\t\t\t\tName: \"testWidget\",\n\t\t\t\tType: \"testType\",\n\t\t\t},\n\t\t},\n\t)\n\treturn keyWid\n}\n\nfunc Test_SetKeyboardChar(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tchar     string\n\t\tfn       func()\n\t\thelpText string\n\t\tmapChar  string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"with blank char\",\n\t\t\tchar:     \"\",\n\t\t\tfn:       test,\n\t\t\thelpText: \"help\",\n\t\t\tmapChar:  \"\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"with undefined char\",\n\t\t\tchar:     \"d\",\n\t\t\tfn:       test,\n\t\t\thelpText: \"help\",\n\t\t\tmapChar:  \"m\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"with defined char\",\n\t\t\tchar:     \"d\",\n\t\t\tfn:       test,\n\t\t\thelpText: \"help\",\n\t\t\tmapChar:  \"d\",\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tkeyWid := testKeyboardWidget()\n\t\t\tkeyWid.SetKeyboardChar(tt.char, tt.fn, tt.helpText)\n\n\t\t\tactual := keyWid.charMap[tt.mapChar]\n\n\t\t\tif tt.expected != (actual != nil) {\n\t\t\t\tt.Errorf(\"\\nexpected: %s\\n     got: %T\", \"actual != nil\", actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SetKeyboardKey(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tkey      tcell.Key\n\t\tfn       func()\n\t\thelpText string\n\t\tmapKey   tcell.Key\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"with undefined key\",\n\t\t\tkey:      tcell.KeyCtrlA,\n\t\t\tfn:       test,\n\t\t\thelpText: \"help\",\n\t\t\tmapKey:   tcell.KeyCtrlZ,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"with defined key\",\n\t\t\tkey:      tcell.KeyCtrlA,\n\t\t\tfn:       test,\n\t\t\thelpText: \"help\",\n\t\t\tmapKey:   tcell.KeyCtrlA,\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tkeyWid := testKeyboardWidget()\n\t\t\tkeyWid.SetKeyboardKey(tt.key, tt.fn, tt.helpText)\n\n\t\t\tactual := keyWid.keyMap[tt.mapKey]\n\n\t\t\tif tt.expected != (actual != nil) {\n\t\t\t\tt.Errorf(\"\\nexpected: %s\\n     got: %T\", \"actual != nil\", actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_InputCapture(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tbefore   func(keyWid *KeyboardWidget) *KeyboardWidget\n\t\tevent    *tcell.EventKey\n\t\texpected *tcell.EventKey\n\t}{\n\t\t{\n\t\t\tname:     \"with nil event\",\n\t\t\tbefore:   func(keyWid *KeyboardWidget) *KeyboardWidget { return keyWid },\n\t\t\tevent:    nil,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"with undefined event\",\n\t\t\tbefore:   func(keyWid *KeyboardWidget) *KeyboardWidget { return keyWid },\n\t\t\tevent:    tcell.NewEventKey(tcell.KeyRune, 'a', tcell.ModNone),\n\t\t\texpected: tcell.NewEventKey(tcell.KeyRune, 'a', tcell.ModNone),\n\t\t},\n\t\t{\n\t\t\tname: \"with defined event and char handler\",\n\t\t\tbefore: func(keyWid *KeyboardWidget) *KeyboardWidget {\n\t\t\t\tkeyWid.SetKeyboardChar(\"a\", test, \"help\")\n\t\t\t\treturn keyWid\n\t\t\t},\n\t\t\tevent:    tcell.NewEventKey(tcell.KeyRune, 'a', tcell.ModNone),\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"with defined event and key handler\",\n\t\t\tbefore: func(keyWid *KeyboardWidget) *KeyboardWidget {\n\t\t\t\tkeyWid.SetKeyboardKey(tcell.KeyRune, test, \"help\")\n\t\t\t\treturn keyWid\n\t\t\t},\n\t\t\tevent:    tcell.NewEventKey(tcell.KeyRune, 'a', tcell.ModNone),\n\t\t\texpected: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tkeyWid := testKeyboardWidget()\n\t\t\tkeyWid = tt.before(keyWid)\n\t\t\tactual := keyWid.InputCapture(tt.event)\n\n\t\t\tif tt.expected == nil {\n\t\t\t\tif actual != nil {\n\t\t\t\t\tt.Errorf(\"\\nexpected: %v\\n     got: %v\", tt.expected, actual.Rune())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.expected.Rune() != actual.Rune() {\n\t\t\t\tt.Errorf(\"\\nexpected: %v\\n     got: %v\", tt.expected.Rune(), actual.Rune())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_initializeCommonKeyboardControls(t *testing.T) {\n\tt.Run(\"nil refreshFunc\", func(t *testing.T) {\n\t\tkeyWid := testKeyboardWidget()\n\n\t\tassert.NotNil(t, keyWid.charMap[\"\\\\\"])\n\t})\n}\n\nfunc Test_InitializeRefreshKeyboardControl(t *testing.T) {\n\tt.Run(\"nil refreshFunc\", func(t *testing.T) {\n\t\tkeyWid := testKeyboardWidget()\n\t\tkeyWid.InitializeRefreshKeyboardControl(nil)\n\n\t\tassert.Nil(t, keyWid.charMap[\"r\"])\n\t})\n\n\tt.Run(\"non-nil refreshFunc\", func(t *testing.T) {\n\t\tkeyWid := testKeyboardWidget()\n\t\tkeyWid.InitializeRefreshKeyboardControl(func() {})\n\n\t\tassert.NotNil(t, keyWid.charMap[\"r\"])\n\t})\n}\n\nfunc Test_HelpText(t *testing.T) {\n\tkeyWid := testKeyboardWidget()\n\tkeyWid.SetKeyboardChar(\"a\", test, \"a help\")\n\tkeyWid.SetKeyboardKey(tcell.KeyCtrlO, test, \"keyCtrlO help\")\n\n\tassert.NotNil(t, keyWid.HelpText())\n}\n"
  },
  {
    "path": "view/multisource_widget.go",
    "content": "package view\n\nimport (\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/utils\"\n)\n\n// MultiSourceWidget is a widget that supports displaying data from multiple sources\ntype MultiSourceWidget struct {\n\tmoduleConfig *cfg.Common\n\tsingular     string\n\tplural       string\n\n\tDisplayFunction func()\n\tIdx             int\n\tSources         []string\n}\n\n// NewMultiSourceWidget creates and returns an instance of MultiSourceWidget\nfunc NewMultiSourceWidget(moduleConfig *cfg.Common, singular, plural string) MultiSourceWidget {\n\twidget := MultiSourceWidget{\n\t\tmoduleConfig: moduleConfig,\n\t\tsingular:     singular,\n\t\tplural:       plural,\n\t}\n\n\twidget.loadSources()\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// CurrentSource returns the string representations of the currently-displayed source\nfunc (widget *MultiSourceWidget) CurrentSource() string {\n\tif widget.Idx >= len(widget.Sources) {\n\t\treturn \"\"\n\t}\n\n\treturn widget.Sources[widget.Idx]\n}\n\n// NextSource displays the next source in the source list. If the current source is the last\n// source it wraps around to the first source\nfunc (widget *MultiSourceWidget) NextSource() {\n\twidget.Idx++\n\tif widget.Idx == len(widget.Sources) {\n\t\twidget.Idx = 0\n\t}\n\n\tif widget.DisplayFunction != nil {\n\t\twidget.DisplayFunction()\n\t}\n}\n\n// PrevSource displays the previous source in the source list. If the current source is the first\n// source, it wraps around to the last source\nfunc (widget *MultiSourceWidget) PrevSource() {\n\twidget.Idx--\n\tif widget.Idx < 0 {\n\t\twidget.Idx = len(widget.Sources) - 1\n\t}\n\n\tif widget.DisplayFunction != nil {\n\t\twidget.DisplayFunction()\n\t}\n}\n\n// SetDisplayFunction stores the function that should be called when the source is\n// changed. This is typically called from within the initializer for the struct that\n// embeds MultiSourceWidget\n//\n// Example:\n//\n//\twidget := Widget{\n//\t  MultiSourceWidget: wtf.NewMultiSourceWidget(settings.common, \"person\", \"people\")\n//\t}\n//\n//\twidget.SetDisplayFunction(widget.display)\nfunc (widget *MultiSourceWidget) SetDisplayFunction(displayFunc func()) {\n\twidget.DisplayFunction = displayFunc\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *MultiSourceWidget) loadSources() {\n\tvar empty []interface{}\n\n\tsingle := widget.moduleConfig.Config.UString(widget.singular, \"\")\n\tmultiple := widget.moduleConfig.Config.UList(widget.plural, empty)\n\n\tasStrs := utils.ToStrs(multiple)\n\n\tif single != \"\" {\n\t\tasStrs = append(asStrs, single)\n\t}\n\n\twidget.Sources = asStrs\n}\n"
  },
  {
    "path": "view/scrollable_widget.go",
    "content": "package view\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\ntype ScrollableWidget struct {\n\tTextWidget\n\n\tSelected       int\n\tmaxItems       int\n\tRenderFunction func()\n}\n\nfunc NewScrollableWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, commonSettings *cfg.Common) ScrollableWidget {\n\twidget := ScrollableWidget{\n\t\tTextWidget: NewTextWidget(tviewApp, redrawChan, pages, commonSettings),\n\t}\n\n\twidget.Unselect()\n\twidget.View.SetScrollable(true)\n\twidget.View.SetRegions(true)\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\nfunc (widget *ScrollableWidget) SetRenderFunction(displayFunc func()) {\n\twidget.RenderFunction = displayFunc\n}\n\nfunc (widget *ScrollableWidget) SetItemCount(items int) {\n\twidget.maxItems = items\n\tif items == 0 {\n\t\twidget.Selected = -1\n\t}\n}\n\nfunc (widget *ScrollableWidget) GetSelected() int {\n\treturn widget.Selected\n}\n\nfunc (widget *ScrollableWidget) RowColor(idx int) string {\n\tif widget.View.HasFocus() && (idx == widget.Selected) {\n\t\treturn widget.CommonSettings().DefaultFocusedRowColor()\n\t}\n\n\treturn widget.CommonSettings().RowColor(idx)\n}\n\nfunc (widget *ScrollableWidget) Next() {\n\twidget.Selected++\n\tif widget.Selected >= widget.maxItems {\n\t\twidget.Selected = 0\n\t}\n\tif widget.maxItems == 0 {\n\t\twidget.Selected = -1\n\t}\n\twidget.RenderFunction()\n}\n\nfunc (widget *ScrollableWidget) Prev() {\n\twidget.Selected--\n\tif widget.Selected < 0 {\n\t\twidget.Selected = widget.maxItems - 1\n\t}\n\tif widget.maxItems == 0 {\n\t\twidget.Selected = -1\n\t}\n\twidget.RenderFunction()\n}\n\nfunc (widget *ScrollableWidget) Unselect() {\n\twidget.Selected = -1\n\tif widget.RenderFunction != nil {\n\t\twidget.RenderFunction()\n\t}\n}\n\nfunc (widget *ScrollableWidget) Redraw(data func() (string, string, bool)) {\n\twidget.TextWidget.Redraw(data)\n\n\twidget.View.Highlight(strconv.Itoa(widget.Selected))\n\twidget.View.ScrollToHighlight()\n}\n"
  },
  {
    "path": "view/text_widget.go",
    "content": "package view\n\nimport (\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/cfg\"\n\t\"github.com/wtfutil/wtf/wtf\"\n)\n\n// TextWidget defines the data necessary to make a text widget\ntype TextWidget struct {\n\t*Base\n\t*KeyboardWidget\n\n\tView *tview.TextView\n}\n\n// NewTextWidget creates and returns an instance of TextWidget\nfunc NewTextWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, commonSettings *cfg.Common) TextWidget {\n\twidget := TextWidget{\n\t\tBase:           NewBase(tviewApp, redrawChan, pages, commonSettings),\n\t\tKeyboardWidget: NewKeyboardWidget(commonSettings),\n\t}\n\n\twidget.View = widget.createView(widget.bordered)\n\twidget.View.SetInputCapture(widget.InputCapture)\n\n\twidget.SetView(widget.View)\n\twidget.helpTextFunc = widget.HelpText\n\n\treturn widget\n}\n\n/* -------------------- Exported Functions -------------------- */\n\n// TextView returns the tview.TextView instance\nfunc (widget *TextWidget) TextView() *tview.TextView {\n\treturn widget.View\n}\n\n// Redraw forces a refresh of the onscreen text content of this widget\nfunc (widget *TextWidget) Redraw(data func() (string, string, bool)) {\n\ttitle, content, wrap := data()\n\n\twidget.View.Clear()\n\twidget.View.SetWrap(wrap)\n\twidget.View.SetTitle(widget.ContextualTitle(title))\n\twidget.View.SetText(strings.TrimRight(content, \"\\n\"))\n\n\twidget.RedrawChan <- true\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc (widget *TextWidget) createView(bordered bool) *tview.TextView {\n\tview := tview.NewTextView()\n\n\tview.SetBackgroundColor(wtf.ColorFor(widget.commonSettings.Colors.Background))\n\tview.SetBorder(bordered)\n\tview.SetBorderColor(wtf.ColorFor(widget.BorderColor()))\n\tview.SetDynamicColors(true)\n\tview.SetTextColor(wtf.ColorFor(widget.commonSettings.Colors.Text))\n\tview.SetTitleColor(wtf.ColorFor(widget.commonSettings.Colors.Title))\n\tview.SetWrap(false)\n\n\treturn view\n}\n"
  },
  {
    "path": "view/text_widget_test.go",
    "content": "package view\n\nimport (\n\t\"testing\"\n\n\t\"github.com/rivo/tview\"\n\t\"github.com/wtfutil/wtf/cfg\"\n)\n\nfunc testTextWidget() TextWidget {\n\ttxtWid := NewTextWidget(\n\t\ttview.NewApplication(),\n\t\tmake(chan bool),\n\t\ttview.NewPages(),\n\t\t&cfg.Common{\n\t\t\tModule: cfg.Module{\n\t\t\t\tName: \"test widget\",\n\t\t\t},\n\t\t},\n\t)\n\treturn txtWid\n}\n\nfunc Test_Bordered(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tbefore   func(txtWid TextWidget) TextWidget\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"without border\",\n\t\t\tbefore: func(txtWid TextWidget) TextWidget {\n\t\t\t\ttxtWid.bordered = false\n\t\t\t\treturn txtWid\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"with border\",\n\t\t\tbefore: func(txtWid TextWidget) TextWidget {\n\t\t\t\ttxtWid.bordered = true\n\t\t\t\treturn txtWid\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttxtWid := testTextWidget()\n\t\t\ttxtWid = tt.before(txtWid)\n\t\t\tactual := txtWid.Bordered()\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %t\\n     got: %t\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Disabled(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tbefore   func(txtWid TextWidget) TextWidget\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"when not enabled\",\n\t\t\tbefore: func(txtWid TextWidget) TextWidget {\n\t\t\t\ttxtWid.enabled = false\n\t\t\t\treturn txtWid\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"when enabled\",\n\t\t\tbefore: func(txtWid TextWidget) TextWidget {\n\t\t\t\ttxtWid.enabled = true\n\t\t\t\treturn txtWid\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttxtWid := testTextWidget()\n\t\t\ttxtWid = tt.before(txtWid)\n\t\t\tactual := txtWid.Disabled()\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %t\\n     got: %t\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Enabled(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tbefore   func(txtWid TextWidget) TextWidget\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"when not enabled\",\n\t\t\tbefore: func(txtWid TextWidget) TextWidget {\n\t\t\t\ttxtWid.enabled = false\n\t\t\t\treturn txtWid\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"when enabled\",\n\t\t\tbefore: func(txtWid TextWidget) TextWidget {\n\t\t\t\ttxtWid.enabled = true\n\t\t\t\treturn txtWid\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttxtWid := testTextWidget()\n\t\t\ttxtWid = tt.before(txtWid)\n\t\t\tactual := txtWid.Enabled()\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %t\\n     got: %t\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Focusable(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tbefore   func(txtWid TextWidget) TextWidget\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"when not focusable\",\n\t\t\tbefore: func(txtWid TextWidget) TextWidget {\n\t\t\t\ttxtWid.enabled = false\n\t\t\t\ttxtWid.focusable = false\n\t\t\t\treturn txtWid\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"when not focusable\",\n\t\t\tbefore: func(txtWid TextWidget) TextWidget {\n\t\t\t\ttxtWid.enabled = false\n\t\t\t\ttxtWid.focusable = true\n\t\t\t\treturn txtWid\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"when not focusable\",\n\t\t\tbefore: func(txtWid TextWidget) TextWidget {\n\t\t\t\ttxtWid.enabled = true\n\t\t\t\ttxtWid.focusable = false\n\t\t\t\treturn txtWid\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"when focusable\",\n\t\t\tbefore: func(txtWid TextWidget) TextWidget {\n\t\t\t\ttxtWid.enabled = true\n\t\t\t\ttxtWid.focusable = true\n\t\t\t\treturn txtWid\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttxtWid := testTextWidget()\n\t\t\ttxtWid = tt.before(txtWid)\n\t\t\tactual := txtWid.Focusable()\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %t\\n     got: %t\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Name(t *testing.T) {\n\ttxtWid := testTextWidget()\n\tactual := txtWid.Name()\n\texpected := \"test widget\"\n\n\tif expected != actual {\n\t\tt.Errorf(\"\\nexpected: %s\\n     got: %s\", expected, actual)\n\t}\n}\n\nfunc Test_String(t *testing.T) {\n\ttxtWid := testTextWidget()\n\tactual := txtWid.String()\n\texpected := \"test widget\"\n\n\tif expected != actual {\n\t\tt.Errorf(\"\\nexpected: %s\\n     got: %s\", expected, actual)\n\t}\n}\n"
  },
  {
    "path": "wtf/colors.go",
    "content": "package wtf\n\nimport (\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nvar colorMap = map[int]string{\n\t0:   \"#000000\",\n\t1:   \"#800000\",\n\t2:   \"#008000\",\n\t3:   \"#808000\",\n\t4:   \"#000080\",\n\t5:   \"#800080\",\n\t6:   \"#008080\",\n\t7:   \"#c0c0c0\",\n\t8:   \"#808080\",\n\t9:   \"#ff0000\",\n\t10:  \"#00ff00\",\n\t11:  \"#ffff00\",\n\t12:  \"#0000ff\",\n\t13:  \"#ff00ff\",\n\t14:  \"#00ffff\",\n\t15:  \"#ffffff\",\n\t16:  \"#000000\",\n\t17:  \"#00005f\",\n\t18:  \"#000087\",\n\t19:  \"#0000af\",\n\t20:  \"#0000d7\",\n\t21:  \"#0000ff\",\n\t22:  \"#005f00\",\n\t23:  \"#005f5f\",\n\t24:  \"#005f87\",\n\t25:  \"#005faf\",\n\t26:  \"#005fd7\",\n\t27:  \"#005fff\",\n\t28:  \"#008700\",\n\t29:  \"#00875f\",\n\t30:  \"#008787\",\n\t31:  \"#0087af\",\n\t32:  \"#0087d7\",\n\t33:  \"#0087ff\",\n\t34:  \"#00af00\",\n\t35:  \"#00af5f\",\n\t36:  \"#00af87\",\n\t37:  \"#00afaf\",\n\t38:  \"#00afd7\",\n\t39:  \"#00afff\",\n\t40:  \"#00d700\",\n\t41:  \"#00d75f\",\n\t42:  \"#00d787\",\n\t43:  \"#00d7af\",\n\t44:  \"#00d7d7\",\n\t45:  \"#00d7ff\",\n\t46:  \"#00ff00\",\n\t47:  \"#00ff5f\",\n\t48:  \"#00ff87\",\n\t49:  \"#00ffaf\",\n\t50:  \"#00ffd7\",\n\t51:  \"#00ffff\",\n\t52:  \"#5f0000\",\n\t53:  \"#5f005f\",\n\t54:  \"#5f0087\",\n\t55:  \"#5f00af\",\n\t56:  \"#5f00d7\",\n\t57:  \"#5f00ff\",\n\t58:  \"#5f5f00\",\n\t59:  \"#5f5f5f\",\n\t60:  \"#5f5f87\",\n\t61:  \"#5f5faf\",\n\t62:  \"#5f5fd7\",\n\t63:  \"#5f5fff\",\n\t64:  \"#5f8700\",\n\t65:  \"#5f875f\",\n\t66:  \"#5f8787\",\n\t67:  \"#5f87af\",\n\t68:  \"#5f87d7\",\n\t69:  \"#5f87ff\",\n\t70:  \"#5faf00\",\n\t71:  \"#5faf5f\",\n\t72:  \"#5faf87\",\n\t73:  \"#5fafaf\",\n\t74:  \"#5fafd7\",\n\t75:  \"#5fafff\",\n\t76:  \"#5fd700\",\n\t77:  \"#5fd75f\",\n\t78:  \"#5fd787\",\n\t79:  \"#5fd7af\",\n\t80:  \"#5fd7d7\",\n\t81:  \"#5fd7ff\",\n\t82:  \"#5fff00\",\n\t83:  \"#5fff5f\",\n\t84:  \"#5fff87\",\n\t85:  \"#5fffaf\",\n\t86:  \"#5fffd7\",\n\t87:  \"#5fffff\",\n\t88:  \"#870000\",\n\t89:  \"#87005f\",\n\t90:  \"#870087\",\n\t91:  \"#8700af\",\n\t92:  \"#8700d7\",\n\t93:  \"#8700ff\",\n\t94:  \"#875f00\",\n\t95:  \"#875f5f\",\n\t96:  \"#875f87\",\n\t97:  \"#875faf\",\n\t98:  \"#875fd7\",\n\t99:  \"#875fff\",\n\t100: \"#878700\",\n\t101: \"#87875f\",\n\t102: \"#878787\",\n\t103: \"#8787af\",\n\t104: \"#8787d7\",\n\t105: \"#8787ff\",\n\t106: \"#87af00\",\n\t107: \"#87af5f\",\n\t108: \"#87af87\",\n\t109: \"#87afaf\",\n\t110: \"#87afd7\",\n\t111: \"#87afff\",\n\t112: \"#87d700\",\n\t113: \"#87d75f\",\n\t114: \"#87d787\",\n\t115: \"#87d7af\",\n\t116: \"#87d7d7\",\n\t117: \"#87d7ff\",\n\t118: \"#87ff00\",\n\t119: \"#87ff5f\",\n\t120: \"#87ff87\",\n\t121: \"#87ffaf\",\n\t122: \"#87ffd7\",\n\t123: \"#87ffff\",\n\t124: \"#af0000\",\n\t125: \"#af005f\",\n\t126: \"#af0087\",\n\t127: \"#af00af\",\n\t128: \"#af00d7\",\n\t129: \"#af00ff\",\n\t130: \"#af5f00\",\n\t131: \"#af5f5f\",\n\t132: \"#af5f87\",\n\t133: \"#af5faf\",\n\t134: \"#af5fd7\",\n\t135: \"#af5fff\",\n\t136: \"#af8700\",\n\t137: \"#af875f\",\n\t138: \"#af8787\",\n\t139: \"#af87af\",\n\t140: \"#af87d7\",\n\t141: \"#af87ff\",\n\t142: \"#afaf00\",\n\t143: \"#afaf5f\",\n\t144: \"#afaf87\",\n\t145: \"#afafaf\",\n\t146: \"#afafd7\",\n\t147: \"#afafff\",\n\t148: \"#afd700\",\n\t149: \"#afd75f\",\n\t150: \"#afd787\",\n\t151: \"#afd7af\",\n\t152: \"#afd7d7\",\n\t153: \"#afd7ff\",\n\t154: \"#afff00\",\n\t155: \"#afff5f\",\n\t156: \"#afff87\",\n\t157: \"#afffaf\",\n\t158: \"#afffd7\",\n\t159: \"#afffff\",\n\t160: \"#d70000\",\n\t161: \"#d7005f\",\n\t162: \"#d70087\",\n\t163: \"#d700af\",\n\t164: \"#d700d7\",\n\t165: \"#d700ff\",\n\t166: \"#d75f00\",\n\t167: \"#d75f5f\",\n\t168: \"#d75f87\",\n\t169: \"#d75faf\",\n\t170: \"#d75fd7\",\n\t171: \"#d75fff\",\n\t172: \"#d78700\",\n\t173: \"#d7875f\",\n\t174: \"#d78787\",\n\t175: \"#d787af\",\n\t176: \"#d787d7\",\n\t177: \"#d787ff\",\n\t178: \"#d7af00\",\n\t179: \"#d7af5f\",\n\t180: \"#d7af87\",\n\t181: \"#d7afaf\",\n\t182: \"#d7afd7\",\n\t183: \"#d7afff\",\n\t184: \"#d7d700\",\n\t185: \"#d7d75f\",\n\t186: \"#d7d787\",\n\t187: \"#d7d7af\",\n\t188: \"#d7d7d7\",\n\t189: \"#d7d7ff\",\n\t190: \"#d7ff00\",\n\t191: \"#d7ff5f\",\n\t192: \"#d7ff87\",\n\t193: \"#d7ffaf\",\n\t194: \"#d7ffd7\",\n\t195: \"#d7ffff\",\n\t196: \"#ff0000\",\n\t197: \"#ff005f\",\n\t198: \"#ff0087\",\n\t199: \"#ff00af\",\n\t200: \"#ff00d7\",\n\t201: \"#ff00ff\",\n\t202: \"#ff5f00\",\n\t203: \"#ff5f5f\",\n\t204: \"#ff5f87\",\n\t205: \"#ff5faf\",\n\t206: \"#ff5fd7\",\n\t207: \"#ff5fff\",\n\t208: \"#ff8700\",\n\t209: \"#ff875f\",\n\t210: \"#ff8787\",\n\t211: \"#ff87af\",\n\t212: \"#ff87d7\",\n\t213: \"#ff87ff\",\n\t214: \"#ffaf00\",\n\t215: \"#ffaf5f\",\n\t216: \"#ffaf87\",\n\t217: \"#ffafaf\",\n\t218: \"#ffafd7\",\n\t219: \"#ffafff\",\n\t220: \"#ffd700\",\n\t221: \"#ffd75f\",\n\t222: \"#ffd787\",\n\t223: \"#ffd7af\",\n\t224: \"#ffd7d7\",\n\t225: \"#ffd7ff\",\n\t226: \"#ffff00\",\n\t227: \"#ffff5f\",\n\t228: \"#ffff87\",\n\t229: \"#ffffaf\",\n\t230: \"#ffffd7\",\n\t231: \"#ffffff\",\n\t232: \"#080808\",\n\t233: \"#121212\",\n\t234: \"#1c1c1c\",\n\t235: \"#262626\",\n\t236: \"#303030\",\n\t237: \"#3a3a3a\",\n\t238: \"#444444\",\n\t239: \"#4e4e4e\",\n\t240: \"#585858\",\n\t241: \"#626262\",\n\t242: \"#6c6c6c\",\n\t243: \"#767676\",\n\t244: \"#808080\",\n\t245: \"#8a8a8a\",\n\t246: \"#949494\",\n\t247: \"#9e9e9e\",\n\t248: \"#a8a8a8\",\n\t249: \"#b2b2b2\",\n\t250: \"#bcbcbc\",\n\t251: \"#c6c6c6\",\n\t252: \"#d0d0d0\",\n\t253: \"#dadada\",\n\t254: \"#e4e4e4\",\n\t255: \"#eeeeee\",\n}\n\nfunc ASCIItoTviewColors(text string) string {\n\tboldRegExp := regexp.MustCompile(`\\033\\[1m`)\n\tfgColorRegExp := regexp.MustCompile(`\\033\\[38;5;(?P<color>\\d+);*\\d*m`)\n\tresColorRegExp := regexp.MustCompile(`\\033\\[0m`)\n\n\treturn resColorRegExp.ReplaceAllString(\n\t\tboldRegExp.ReplaceAllString(\n\t\t\tfgColorRegExp.ReplaceAllStringFunc(\n\t\t\t\ttext, replaceWithHexColorString), `[::b]`), `[-]`)\n}\n\nfunc ColorFor(label string) tcell.Color {\n\treturn tcell.GetColor(label)\n}\n\n/* -------------------- Unexported Functions -------------------- */\n\nfunc replaceWithHexColorString(substring string) string {\n\tcolorID, err := strconv.Atoi(strings.Trim(\n\t\tstrings.Split(substring, \";\")[2], \"m\"))\n\tif err != nil {\n\t\treturn substring\n\t}\n\n\thexColor := \"[\" + colorMap[colorID] + \"]\"\n\n\treturn hexColor\n}\n"
  },
  {
    "path": "wtf/colors_test.go",
    "content": "package wtf\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gdamore/tcell/v2\"\n)\n\nfunc Test_ASCIItoTviewColors(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\ttext     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"with blank text\",\n\t\t\ttext:     \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with no color\",\n\t\t\ttext:     \"cat\",\n\t\t\texpected: \"cat\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with defined color\",\n\t\t\ttext:     \"[38;5;226mcat/\\x1b[0m\",\n\t\t\texpected: \"[38;5;226mcat/[-]\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := ASCIItoTviewColors(tt.text)\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %q\\n     got: %q\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_ColorFor(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tlabel    string\n\t\texpected tcell.Color\n\t}{\n\t\t{\n\t\t\tname:     \"with no label\",\n\t\t\tlabel:    \"\",\n\t\t\texpected: tcell.ColorDefault,\n\t\t},\n\t\t{\n\t\t\tname:     \"with missing label\",\n\t\t\tlabel:    \"cat\",\n\t\t\texpected: tcell.ColorDefault,\n\t\t},\n\t\t{\n\t\t\tname:     \"with defined label\",\n\t\t\tlabel:    \"tomato\",\n\t\t\texpected: tcell.ColorTomato,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := ColorFor(tt.label)\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %q\\n     got: %q\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "wtf/datetime.go",
    "content": "package wtf\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\nconst (\n\t// DateFormat defines the format we expect to receive dates from BambooHR in\n\tDateFormat = \"2006-01-02\"\n\n\t// TimeFormat defines the format we expect to receive times from BambooHR in\n\tTimeFormat = \"15:04\"\n)\n\n// IsToday returns TRUE if the date is today, FALSE if the date is not today\nfunc IsToday(date time.Time) bool {\n\tnow := time.Now().Local()\n\n\treturn (date.Year() == now.Year()) &&\n\t\t(date.Month() == now.Month()) &&\n\t\t(date.Day() == now.Day())\n}\n\n// PrettyDate takes a programmer-style date string and converts it\n// in a friendlier-to-read format\nfunc PrettyDate(dateStr string) string {\n\tnewTime, err := time.Parse(DateFormat, dateStr)\n\tif err != nil {\n\t\treturn dateStr\n\t}\n\n\treturn fmt.Sprint(newTime.Format(\"Jan 2, 2006\"))\n}\n\n// UnixTime takes a Unix epoch time (in seconds) and returns a\n// time.Time instance\nfunc UnixTime(unix int64) time.Time {\n\treturn time.Unix(unix, 0)\n}\n"
  },
  {
    "path": "wtf/datetime_test.go",
    "content": "package wtf\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc Test_IsToday(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tdate     time.Time\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"when yesterday\",\n\t\t\tdate:     time.Now().Local().AddDate(0, 0, -1),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"when today\",\n\t\t\tdate:     time.Now().Local(),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"when tomorrow\",\n\t\t\tdate:     time.Now().Local().AddDate(0, 0, +1),\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := IsToday(tt.date)\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %t\\n     got: %t\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_PrettyDate(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tdate     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"with empty date\",\n\t\t\tdate:     \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with invalid date\",\n\t\t\tdate:     \"10-21-1999\",\n\t\t\texpected: \"10-21-1999\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with valid date\",\n\t\t\tdate:     \"1999-10-21\",\n\t\t\texpected: \"Oct 21, 1999\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := PrettyDate(tt.date)\n\n\t\t\tif tt.expected != actual {\n\t\t\t\tt.Errorf(\"\\nexpected: %s\\n     got: %s\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\nfunc Test_UnixTime(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tunixVal  int64\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"with 0 time\",\n\t\t\tunixVal:  0,\n\t\t\texpected: \"1970-01-01 00:00:00 +0000 UTC\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with explicit time\",\n\t\t\tunixVal:  1564883266,\n\t\t\texpected: \"2019-08-04 01:47:46 +0000 UTC\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := UnixTime(tt.unixVal).UTC()\n\n\t\t\tif tt.expected != actual.String() {\n\t\t\t\tt.Errorf(\"\\nexpected: %s\\n     got: %s\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "wtf/enablable.go",
    "content": "package wtf\n\n// Enablable is the interface that enforces enable/disable capabilities on a module\ntype Enablable interface {\n\tDisable()\n\tDisabled() bool\n\tEnabled() bool\n}\n"
  },
  {
    "path": "wtf/numbers.go",
    "content": "package wtf\n\nimport \"math\"\n\n// Round rounds a float to an integer\nfunc Round(num float64) int {\n\treturn int(num + math.Copysign(0.5, num))\n}\n\n// TruncateFloat64 truncates the decimal places of a float64 to the specified precision\nfunc TruncateFloat64(num float64, precision int) float64 {\n\toutput := math.Pow(10, float64(precision))\n\treturn float64(Round(num*output)) / output\n}\n"
  },
  {
    "path": "wtf/numbers_test.go",
    "content": "package wtf\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/assert\"\n)\n\nfunc Test_Round(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    float64\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"negative\",\n\t\t\tinput:    -3,\n\t\t\texpected: -3,\n\t\t},\n\t\t{\n\t\t\tname:     \"integer\",\n\t\t\tinput:    3,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tname:     \"float down\",\n\t\t\tinput:    3.123456,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tname:     \"float up\",\n\t\t\tinput:    3.998786,\n\t\t\texpected: 4,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := Round(tt.input)\n\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n\nfunc Test_TruncateFloat(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinput     float64\n\t\tprecision int\n\t\texpected  float64\n\t}{\n\t\t{\n\t\t\tname:      \"negative precision\",\n\t\t\tinput:     23.234567,\n\t\t\tprecision: -2,\n\t\t\texpected:  0,\n\t\t},\n\t\t{\n\t\t\tname:      \"zero precision\",\n\t\t\tinput:     23.234567,\n\t\t\tprecision: 0,\n\t\t\texpected:  23,\n\t\t},\n\t\t{\n\t\t\tname:      \"positive precision\",\n\t\t\tinput:     23.234567,\n\t\t\tprecision: 2,\n\t\t\texpected:  23.23,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := TruncateFloat64(tt.input, tt.precision)\n\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "wtf/schedulable.go",
    "content": "package wtf\n\nimport \"time\"\n\n// Schedulable is the interface that enforces scheduling capabilities on a module\ntype Schedulable interface {\n\tRefresh()\n\tRefreshing() bool\n\tRefreshInterval() time.Duration\n}\n"
  },
  {
    "path": "wtf/stoppable.go",
    "content": "package wtf\n\n// Stoppable is the interface that enforces a stoppable state\ntype Stoppable interface {\n\tStop()\n}\n"
  },
  {
    "path": "wtf/terminal.go",
    "content": "package wtf\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/logrusorgru/aurora/v4\"\n\t\"github.com/olebedev/config\"\n)\n\n// SetTerminal sets the TERM environment variable, defaulting to whatever the OS\n// has as the current value if none is specified.\n// See https://www.gnu.org/software/gettext/manual/html_node/The-TERM-variable.html for\n// more details.\nfunc SetTerminal(config *config.Config) {\n\tterm := config.UString(\"wtf.term\", os.Getenv(\"TERM\"))\n\terr := os.Setenv(\"TERM\", term)\n\tif err != nil {\n\t\tfmt.Printf(\"\\n%s Failed to set $TERM to %s.\\n\", aurora.Red(\"ERROR\"), aurora.Yellow(term))\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "wtf/wtfable.go",
    "content": "package wtf\n\nimport (\n\t\"github.com/wtfutil/wtf/cfg\"\n\n\t\"github.com/rivo/tview\"\n)\n\n// Wtfable is the interface that enforces WTF system capabilities on a module\ntype Wtfable interface {\n\tEnablable\n\tSchedulable\n\tStoppable\n\n\tBorderColor() string\n\tConfigText() string\n\tFocusChar() string\n\tFocusable() bool\n\tHelpText() string\n\tName() string\n\tQuitChan() chan bool\n\tSetFocusChar(string)\n\tTextView() *tview.TextView\n\n\tCommonSettings() *cfg.Common\n}\n"
  }
]