├── .github └── workflows │ └── schedule.yml ├── .gitignore ├── LICENSE ├── README.md ├── README_zh.md ├── cmd └── box │ └── main.go ├── go.mod ├── go.sum └── pkg └── wakabox ├── box.go ├── box_test.go └── test.md /.github/workflows/schedule.yml: -------------------------------------------------------------------------------- 1 | name: Update gist with WakaTime stats 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | - cron: "0 0 * * *" 12 | 13 | jobs: 14 | build: 15 | name: Update-gist 16 | runs-on: ubuntu-latest 17 | env: 18 | WAKATIME_API_KEY: ${{ secrets.WAKATIME_API_KEY }} 19 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 20 | GIST_ID: 9bc7025496e478f439b9cd43eba989a4 21 | GIST_BARSTYLE: SOLIDLT 22 | GIST_BARLENGTH: -1 23 | GIST_TIMESTYLE: SHORT 24 | 25 | steps: 26 | - name: Set up Go 1.x 27 | uses: actions/setup-go@v2 28 | with: 29 | go-version: ^1.14 30 | id: go 31 | 32 | - name: Check out code into the Go module directory 33 | uses: actions/checkout@v2 34 | 35 | - name: Update-gist 36 | run: go run ./cmd/box/main.go 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [2020] [chuiyouwu@gmail.com] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 |
4 |
5 |
📊 Update pinned gist / profile README to contain your weekly WakaTime stats.
7 |A Golang implementation, see the original version waka-box 8 |
11 | 12 | 13 | --- 14 | 15 | English | [简体中文](./README_zh.md) 16 | 17 | > 📌✨ For more pinned-gist projects like this one, check out: https://github.com/matchai/awesome-pinned-gists 18 | 19 | ## 💻 Setup 20 | 21 | ### 🎒 Prep work 22 | 23 | > if only want's to update a markdown,like profile README,skip step 1 and step 2. 24 | 25 | 1. Create a new public GitHub Gist with name `📊 Weekly development breakdown` (https://gist.github.com/) 26 | 1. Create a token with the `gist` scope and copy it. (https://github.com/settings/tokens/new) 27 | 1. Create a WakaTime account (https://wakatime.com/signup) 28 | 1. In your WakaTime profile settings (https://wakatime.com/settings/profile) ensure `Display coding activity publicly` and `Display languages, editors, operating systems publicly` are checked. 29 | 1. In your account settings, copy the existing WakaTime API Key (https://wakatime.com/settings/api-key) 30 | 1. For updating a markdown file,add comments to the place where you want to update in the markdown file. 31 | ```markdown 32 | 33 | 34 | ``` 35 | 36 | ### 🚀 Project setup 37 | 38 | 1. Fork this repo 39 | 40 | 41 | 2. Edit the [environment variable](https://github.com/YouEclipse/waka-box-go/actions/runs/126970182/workflow#L17-L19) in `.github/workflows/schedule.yml`: 42 | 43 | > For updating github profile README,you can follow [waka-box.yml](https://github.com/YouEclipse/YouEclipse/blob/master/.github/workflows/waka-box.yml) in [YouEclipse](https://github.com/YouEclipse/YouEclipse) to create a Action in your README repo.Remember it's unsafe to use token with **`repo`** scope for updating the repo, waka-box update the profile repo using git command in Github Action instead of using github API. 44 | 45 | > DO NOT CHANGE THE WAKATIME_API_KEY or GH_TOKEN VALUES IN THIS FILE, USE THE REPO SECRETS SET BELOW. FAILURE TO DO THIS WILL MAKE YOUR WAKATIME API KEY PUBLIC AND CAN POTENTIALLY EXPOSE SENSITIVE INFORMATION. 46 | 47 | - **UPDATE_OPTION:** Default is `GIST`.For only update a markdown file ,set to`MARKDOWN`,and ignore environment variables with prefix **GIST\_** below.Set to `GIST_AND_MARKDOWN` updates both the gist and the markdown file. 48 | - **MARKDOWN_FILE:** The filename for the markdown file. 49 | 50 | - **GIST_ID:** The ID portion from your gist url: `https://gist.github.com/YouEclipse/`**`9bc7025496e478f439b9cd43eba989a4`**. 51 | 52 | **the following are optional, thanks [@AarynSmith](https://github.com/AarynSmith) for PR([#11](https://github.com/YouEclipse/waka-box-go/pull/11))** 53 | 54 | - **GIST_BARSTYLE:** Background of the progress bar. Default is "SOLIDLT" other options include "SOLIDMD", "SOLIDDK" for medium and dark backgrounds, "EMPTY" for blank background, and "UNDERSCORE" for a line along the bottom. 55 | - **GIST_BARLENGTH:** Length of the progress bar. Default is 21. Set to -1 to auto size the bar. 56 | - **GIST_TIMESTYLE** Abbreviate the time text. Default is "LONG" ( "# hrs # mins" ). "SHORT" updates the text to "#h#m". 57 | 58 | 3. Go to the repo **Settings > Secrets** 59 | 4. Add the following environment variables: 60 | - **GH_TOKEN:** The GitHub token generated above. 61 | - **WAKATIME_API_KEY:** The API key for your WakaTime account. 62 | 63 | ## 🕵️ How it works 64 | 65 | - Get stats from WakaTime API 66 | - Update Gist with Github API 67 | - Use Github Actions for updating Gist 68 | 69 | ## 📄 License 70 | 71 | This project is licensed under [Apache-2.0](./LICENSE) 72 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 |
4 |
5 |
📊 将你的 WakaTime 每周统计更新在 pined gist / profile README
7 |Golang 实现,查看原始版本 waka-box 8 |
11 | 12 | 13 | --- 14 | 15 | [English](./README.md) | 简体中文 16 | 17 | > 📌✨ 查看更多像这样的 Pinned Gist 项目,传送门: https://github.com/matchai/awesome-pinned-gists 18 | 19 | ## 💻 安装 20 | 21 | ### 🎒 前置工作 22 | 23 | > 如果只想更新某个 markdown 文件,比如 profile README,可以跳过 1,2 两步 24 | 25 | 1. 创建一个公开的 GitHub Gist,文件名为`📊 Weekly development breakdown` (https://gist.github.com/) 26 | 1. 创建一个拥有 `gist` 权限的 token 并复制. (https://github.com/settings/tokens/new) 27 | 1. 创建一个 WakaTime 账号(如果已经有了可以跳过),配置好编辑器插件使用一段时间,建议 WakaTime 后台有数据了再进入下一步。 (https://wakatime.com/signup) 28 | 1. 在 WakaTime 的 profile settings (https://wakatime.com/settings/profile) 确保 `Display coding activity publicly` 和 `Display languages, editors, operating systems publicly` 被勾选了 29 | 1. 在你的 WakaTime 的 account settings, 复制 WakaTime API Key (https://wakatime.com/settings/api-key) 30 | 1. 如果需要更新到某个 markdown 文件,请在对应文件需要更新的地方添加以下注释 31 | 32 | ```markdown 33 | 34 | 35 | ``` 36 | ### 🚀 开始安装 37 | 38 | 1. Fork 这个仓库 39 | 40 | 41 | 2. 编辑 `.github/workflows/schedule.yml` 中的[环境变量](https://github.com/YouEclipse/waka-box-go/actions/runs/126970182/workflow#L17-L19) : 42 | 43 | > 如果是需要更新 github profile README,可以在 profile README 的仓库中创建 Action,具体配置参考 我的 [YouEclipse](https://github.com/YouEclipse/YouEclipse) 中的 [waka-box.yml](https://github.com/YouEclipse/YouEclipse/blob/master/.github/workflows/waka-box.yml).因为使用 **`repo`** 权限的token 来通过 API 更新仓库,可能会不安全,所以我的示例中使用 git 命令来更新,这样更加安全。 44 | 45 | > 不要修改此文件中的 WAKATIME_API_KEY 和 GH_TOKEN VALUES, 使用下方设置的的 Secret.否则你的 WAKATIME API KEY 会变成公开的,导致泄露一些敏感信息。 46 | 47 | 48 | - **UPDATE_OPTION:** 默认是 `GIST`,如果只想更新到某个 markdown 文件,设置为`MARKDOWN`,并可以忽略以下以 **GIST\_** 开头的环境变量,如果想同时更新 gist 和 markdown,设置为`GIST_AND_MARKDOWN` 49 | - **MARKDOWN_FILE:** 如果是更新到某个 markdown 文件,填写 markdown 文件名(包含相对路径或者绝对路径) 50 | - **GIST_ID:** ID 是 gist url 的后缀 : `https://gist.github.com/YouEclipse/`**`9bc7025496e478f439b9cd43eba989a4`**. 51 | 52 | **以下为可选参数,感谢[@AarynSmith](https://github.com/AarynSmith)的 PR([#11](https://github.com/YouEclipse/waka-box-go/pull/11))** 53 | 54 | - **GIST_BARSTYLE:** 进度条的背景样式. 默认是 "SOLIDLT",其他样式包括 "SOLIDMD", "SOLIDDK" (黑色), "EMPTY" (空白) 和 "UNDERSCORE"(下划线). 55 | - **GIST_BARLENGTH:** 条形图的长度. 默认 21. 设置为 -1 可以自动适配. 56 | - **GIST_TIMESTYLE:** 时间文本的样式. 默认是 "LONG" ( "# hrs # mins" ). "SHORT" 则是 "#h#m". 57 | 58 | 3. 前往 fork 后的仓库的 **Settings > Secrets** 59 | 4. 添加以下环境变量: 60 | - **GH_TOKEN:** 前置工作中生成的 github token. 61 | - **WAKATIME_API_KEY:** WakaTime 的 API key. 62 | 63 | ## 🕵️ 工作原理 64 | 65 | - 基于 WakaTime API 获取统计数据 66 | - 基于 Github API 获取/更新 Gist 67 | - 使用 Github Actions 定时更新 Gist 68 | 69 | ## 📄 开源协议 70 | 71 | 本项目使用 [Apache-2.0](./LICENSE) 协议 72 | -------------------------------------------------------------------------------- /cmd/box/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/YouEclipse/waka-box-go/pkg/wakabox" 11 | "github.com/google/go-github/github" 12 | "github.com/joho/godotenv" 13 | ) 14 | 15 | func init() { 16 | godotenv.Load(".env") 17 | } 18 | 19 | func main() { 20 | wakaAPIKey := os.Getenv("WAKATIME_API_KEY") 21 | ghToken := os.Getenv("GH_TOKEN") 22 | ghUsername := os.Getenv("GH_USER") 23 | gistID := os.Getenv("GIST_ID") 24 | updateOption := os.Getenv("UPDATE_OPTION") // options for update: GIST,MARKDOWN,GIST_AND_MARKDOWN 25 | markdownFile := os.Getenv("MARKDOWN_FILE") // the markdown filename 26 | 27 | var updateGist, updateMarkdown bool 28 | if updateOption == "MARKDOWN" { 29 | updateMarkdown = true 30 | } else if updateOption == "GIST_AND_MARKDOWN" { 31 | updateGist = true 32 | updateMarkdown = true 33 | } else { 34 | updateGist = true 35 | } 36 | 37 | style := wakabox.BoxStyle{ 38 | BarStyle: os.Getenv("GIST_BARSTYLE"), 39 | BarLength: os.Getenv("GIST_BARLENGTH"), 40 | TimeStyle: os.Getenv("GIST_TIMESTYLE"), 41 | } 42 | 43 | box := wakabox.NewBox(wakaAPIKey, ghUsername, ghToken, style) 44 | 45 | ctx := context.Background() 46 | lines, err := box.GetStats(ctx) 47 | if err != nil { 48 | panic(err) 49 | } 50 | filename := "📊 Weekly development breakdown" 51 | if updateGist { 52 | 53 | gist, err := box.GetGist(ctx, gistID) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | f := gist.Files[github.GistFilename(filename)] 59 | 60 | f.Content = github.String(strings.Join(lines, "\n")) 61 | gist.Files[github.GistFilename(filename)] = f 62 | err = box.UpdateGist(ctx, gistID, gist) 63 | if err != nil { 64 | panic(err) 65 | } 66 | fmt.Println("updating gist successfully") 67 | } 68 | 69 | if updateMarkdown && markdownFile != "" { 70 | title := filename 71 | if updateGist { 72 | title = fmt.Sprintf(`#### %s`, gistID, title) 73 | } 74 | 75 | content := bytes.NewBuffer(nil) 76 | content.WriteString(strings.Join(lines, "\n")) 77 | 78 | err = box.UpdateMarkdown(ctx, title, markdownFile, content.Bytes()) 79 | if err != nil { 80 | fmt.Println(err) 81 | } 82 | fmt.Println("updating markdown successfully on", markdownFile) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/YouEclipse/waka-box-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/YouEclipse/wakatime-go v0.1.2-0.20221018190731-e3134bb99788 7 | github.com/google/go-github v17.0.0+incompatible 8 | github.com/google/go-querystring v1.0.0 // indirect 9 | github.com/joho/godotenv v1.3.0 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/YouEclipse/wakatime-go v0.1.2-0.20221018190731-e3134bb99788 h1:r7ZHyoO02GZBW+l1RZCAsFy+gwdgaI8qf4fqsQcdV/M= 2 | github.com/YouEclipse/wakatime-go v0.1.2-0.20221018190731-e3134bb99788/go.mod h1:h4Y4peXcOv5faIKMvWAao9FCSWxxD8jU7hKtIib2fNA= 3 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 4 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 5 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 6 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 7 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 8 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 9 | -------------------------------------------------------------------------------- /pkg/wakabox/box.go: -------------------------------------------------------------------------------- 1 | package wakabox 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io/ioutil" 8 | "math" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "unicode/utf8" 13 | 14 | "github.com/YouEclipse/wakatime-go/pkg/wakatime" 15 | "github.com/google/go-github/github" 16 | ) 17 | 18 | // maxLineLength is the visible number of characters in a pinned gist box 19 | // (accounting for the clock emoji) 20 | const maxLineLength = 53 21 | 22 | // BarStyle defines valid styles for the progress bar 23 | var BarStyle = map[string][]rune{ 24 | "SOLIDLT": []rune(`░▏▎▍▌▋▊▉█`), 25 | "SOLIDMD": []rune(`▒▏▎▍▌▋▊▉█`), 26 | "SOLIDDK": []rune(`▓▏▎▍▌▋▊▉█`), 27 | "EMPTY": []rune(` ▏▎▍▌▋▊▉█`), 28 | "UNDERSCORE": []rune(`▁▏▎▍▌▋▊▉█`), 29 | } 30 | 31 | // BoxStyle contains information for initalizing a gist box style 32 | type BoxStyle struct { 33 | BarStyle string // Style of the progress bar as defined by BarStyle 34 | BarLength string // Length of the bar as a string (gets converted to an Int) 35 | TimeStyle string // Style of the time text. "SHORT" will be abbreviated. 36 | barLengthInt int // Set automatically from the Length defined above 37 | maxLangLen int // Set automatically from the list of languages from wakatime 38 | maxTimeLen int // Set automatically from the list of times from wakatime 39 | 40 | } 41 | 42 | // Box contains a github and wakatime client and styling information for the gist box 43 | type Box struct { 44 | github *github.Client 45 | wakatime *wakatime.Client 46 | style BoxStyle 47 | } 48 | 49 | // NewBox creates a box struct with appropriate wakatime and github information and gist styling information 50 | func NewBox(wakaAPIKey, ghUsername, ghToken string, style BoxStyle) *Box { 51 | box := &Box{} 52 | 53 | box.wakatime = wakatime.NewClient(wakaAPIKey, nil) 54 | 55 | tp := github.BasicAuthTransport{ 56 | Username: strings.TrimSpace(ghUsername), 57 | Password: strings.TrimSpace(ghToken), 58 | } 59 | box.github = github.NewClient(tp.Client()) 60 | 61 | length, err := strconv.Atoi(style.BarLength) 62 | if err != nil { 63 | length = 21 //Default to 21 64 | } 65 | style.barLengthInt = length 66 | if style.BarStyle == "" { 67 | style.BarStyle = "SOLIDLT" // Default to SOLIDLT 68 | } 69 | box.style = style 70 | 71 | return box 72 | } 73 | 74 | // GetStats gets the language stats form wakatime.com. 75 | func (b *Box) GetStats(ctx context.Context) ([]string, error) { 76 | stats, err := b.wakatime.Stats.Current(ctx, wakatime.RangeLast7Days, &wakatime.StatsQuery{}) 77 | if err != nil { 78 | return nil, fmt.Errorf("wakabox.GetStats: Error getting Current Stats: %w", err) 79 | } 80 | 81 | if languages := stats.Data.Languages; len(languages) > 0 { 82 | lines, err := b.GenerateGistLines(ctx, languages) 83 | if err != nil { 84 | return nil, fmt.Errorf("wakabox.GetStats: Error generating gist lines: %w", err) 85 | } 86 | 87 | return lines, nil 88 | } 89 | return []string{"Still Gathering Statistics..."}, nil 90 | } 91 | 92 | // GetGist gets the gist from github.com. 93 | func (b *Box) GetGist(ctx context.Context, id string) (*github.Gist, error) { 94 | gist, _, err := b.github.Gists.Get(ctx, id) 95 | if err != nil { 96 | return nil, fmt.Errorf("wakabox.GetGist: Error getting gist from github: %w", err) 97 | } 98 | return gist, nil 99 | } 100 | 101 | // UpdateGist updates the gist. 102 | func (b *Box) UpdateGist(ctx context.Context, id string, gist *github.Gist) error { 103 | _, _, err := b.github.Gists.Edit(ctx, id, gist) 104 | if err != nil { 105 | return fmt.Errorf("wakabox.UpdateGist: Error updating gist: %w", err) 106 | } 107 | return nil 108 | } 109 | 110 | func (b *Box) UpdateMarkdown(ctx context.Context, title, filename string, content []byte) error { 111 | md, err := ioutil.ReadFile(filename) 112 | if err != nil { 113 | return fmt.Errorf("wakabox.UpdateMarkdown: Error reade a file: %w", err) 114 | } 115 | 116 | start := []byte("") 117 | before := md[:bytes.Index(md, start)+len(start)] 118 | end := []byte("") 119 | after := md[bytes.Index(md, end):] 120 | 121 | newMd := bytes.NewBuffer(nil) 122 | newMd.Write(before) 123 | newMd.WriteString("\n" + title + "\n") 124 | newMd.WriteString("```text\n") 125 | newMd.Write(content) 126 | newMd.WriteString("\n") 127 | newMd.WriteString("```\n") 128 | newMd.WriteString("\n") 129 | newMd.Write(after) 130 | 131 | err = ioutil.WriteFile(filename, newMd.Bytes(), os.ModeAppend) 132 | if err != nil { 133 | return fmt.Errorf("wakabox.UpdateMarkdown: Error write a file: %w", err) 134 | } 135 | 136 | return nil 137 | } 138 | 139 | // GenerateGistLines takes an slice of wakatime.StatItems, and generates a line for the gist. 140 | func (b *Box) GenerateGistLines(ctx context.Context, languages []wakatime.StatItem) ([]string, error) { 141 | max := 0 142 | lines := make([]string, 0) 143 | for _, stat := range languages { 144 | if b.style.TimeStyle == "SHORT" { 145 | *stat.Text = convertDuration(*stat.Text) 146 | } 147 | if b.style.maxTimeLen < len(*stat.Text) { 148 | b.style.maxTimeLen = len(*stat.Text) 149 | } 150 | if b.style.maxLangLen < len(*stat.Name) { 151 | b.style.maxLangLen = len(*stat.Name) 152 | } 153 | } 154 | if b.style.barLengthInt < 0 { 155 | b.style.barLengthInt = maxLineLength - (b.style.maxLangLen + b.style.maxTimeLen + 10) 156 | } 157 | for _, stat := range languages { 158 | if max >= 5 { 159 | break 160 | } 161 | lines = append(lines, b.ConstructLine(ctx, stat)) 162 | max++ 163 | } 164 | return lines, nil 165 | } 166 | 167 | // ConstructLine formats a gist line from stat infomation 168 | func (b *Box) ConstructLine(ctx context.Context, stat wakatime.StatItem) string { 169 | return fmt.Sprintf("%-*s🕓 %-*s%s%5.1f%%", 170 | b.style.maxLangLen+1, *stat.Name, 171 | b.style.maxTimeLen+1, *stat.Text, 172 | GenerateBarChart(ctx, *stat.Percent, b.style.barLengthInt, b.style.BarStyle), 173 | *stat.Percent) 174 | } 175 | 176 | // GenerateBarChart generates a bar chart with the given percent and size. 177 | // Percent is a float64 from 0-100 representing the progress bar percentage 178 | // Size is an int representing the length of the progress bar in characters 179 | // BarType is a BarType representing the type of barchart: It can be one of the following: 180 | // SOLIDLT SOLIDMD SOLIDDK: Block characters with a dotted background 181 | // UNDERSCORE: Block characters with an line accross the boottom 182 | // EMPTY: Block characters with an empty background 183 | func GenerateBarChart(ctx context.Context, percent float64, size int, barType string) string { 184 | // using rune as for utf-8 encoding 185 | syms := BarStyle[barType] 186 | if len(syms) > 9 { 187 | panic("No Syms") 188 | } 189 | 190 | frac := int(math.Floor((float64(size) * 8 * percent) / 100)) 191 | barsFull := int(math.Floor(float64(frac) / 8)) 192 | 193 | if barsFull >= size { 194 | return strings.Repeat(string(syms[8:9]), size) 195 | } 196 | 197 | var semi = frac % 8 198 | 199 | bar := strings.Repeat(string(syms[8:9]), barsFull) + string(syms[semi:semi+1]) 200 | 201 | return pad(bar, string(syms[0:1]), size) 202 | } 203 | 204 | func pad(s, pad string, targetLength int) string { 205 | padding := targetLength - utf8.RuneCountInString(s) 206 | if padding <= 0 { 207 | return s 208 | } 209 | 210 | return s + strings.Repeat(pad, padding) 211 | } 212 | 213 | func convertDuration(t string) string { 214 | r := strings.NewReplacer( 215 | "hr", "h", 216 | "min", "m", 217 | "sec", "s", 218 | " ", "", 219 | "s", "", 220 | ) 221 | return r.Replace(t) 222 | } 223 | -------------------------------------------------------------------------------- /pkg/wakabox/box_test.go: -------------------------------------------------------------------------------- 1 | package wakabox 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/google/go-github/github" 13 | "github.com/joho/godotenv" 14 | ) 15 | 16 | func init() { 17 | godotenv.Load("../.env") 18 | } 19 | 20 | func TestGenerateBarChart(t *testing.T) { 21 | type args struct { 22 | ctx context.Context 23 | percent float64 24 | size int 25 | style string 26 | } 27 | 28 | ctx := context.Background() 29 | tests := []struct { 30 | name string 31 | args args 32 | want string 33 | }{ 34 | { 35 | name: "barchart-0%-Empty", 36 | args: args{ 37 | ctx: ctx, 38 | percent: 0, 39 | size: 21, 40 | style: "EMPTY", 41 | }, 42 | want: " ", 43 | }, 44 | { 45 | name: "barchart-23.5%-Underscore", 46 | args: args{ 47 | ctx: ctx, 48 | percent: 23.5, 49 | size: 21, 50 | style: "UNDERSCORE", 51 | }, 52 | want: "████▉▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁", 53 | }, 54 | { 55 | name: "barchart-0%", 56 | args: args{ 57 | ctx: ctx, 58 | percent: 0, 59 | size: 21, 60 | style: "SOLIDLT", 61 | }, 62 | want: "░░░░░░░░░░░░░░░░░░░░░", 63 | }, 64 | { 65 | name: "barchart-23.5%", 66 | args: args{ 67 | ctx: ctx, 68 | percent: 23.5, 69 | size: 21, 70 | style: "SOLIDLT", 71 | }, 72 | want: "████▉░░░░░░░░░░░░░░░░", 73 | }, 74 | { 75 | name: "barchart-72.5%", 76 | args: args{ 77 | ctx: ctx, 78 | percent: 72.5, 79 | size: 21, 80 | style: "SOLIDLT", 81 | }, 82 | want: "███████████████▏░░░░░", 83 | }, 84 | { 85 | name: "barchart-100%", 86 | args: args{ 87 | ctx: ctx, 88 | percent: 100, 89 | size: 21, 90 | style: "SOLIDLT", 91 | }, 92 | want: "█████████████████████", 93 | }, 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | if got := GenerateBarChart(tt.args.ctx, tt.args.percent, tt.args.size, tt.args.style); got != tt.want { 98 | t.Errorf("GenerateBarChart() = %v, want %v", got, tt.want) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestBox_GetStats(t *testing.T) { 105 | 106 | wakaAPIKey := os.Getenv("WAKATIME_API_KEY") 107 | 108 | ghToken := os.Getenv("GH_TOKEN") 109 | ghUsername := os.Getenv("GH_USER") 110 | style := BoxStyle{ 111 | BarStyle: os.Getenv("GIST_BARSTYLE"), 112 | BarLength: os.Getenv("GIST_BARLENGTH"), 113 | TimeStyle: os.Getenv("GIST_TIMESTYLE"), 114 | } 115 | fmt.Printf("%+v - %+v", style, BarStyle[style.BarStyle]) 116 | box := NewBox(wakaAPIKey, ghUsername, ghToken, style) 117 | 118 | lines, err := box.GetStats(context.Background()) 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | t.Log(strings.Join(lines, "\n")) 123 | 124 | } 125 | 126 | func TestBox_Gist(t *testing.T) { 127 | wakaAPIKey := os.Getenv("WAKATIME_API_KEY") 128 | 129 | ghToken := os.Getenv("GH_TOKEN") 130 | ghUsername := os.Getenv("GH_USER") 131 | gistID := os.Getenv("GIST_ID") 132 | 133 | style := BoxStyle{ 134 | BarStyle: os.Getenv("GIST_BARSTYLE"), 135 | BarLength: os.Getenv("GIST_BARLENGTH"), 136 | TimeStyle: os.Getenv("GIST_TIMESTYLE"), 137 | } 138 | 139 | box := NewBox(wakaAPIKey, ghUsername, ghToken, style) 140 | 141 | ctx := context.Background() 142 | filename := "📊 Weekly development breakdown" 143 | gist, err := box.GetGist(ctx, gistID) 144 | if err != nil { 145 | t.Error(err) 146 | } 147 | 148 | f := gist.Files[github.GistFilename(filename)] 149 | 150 | f.Content = github.String(time.Now().UTC().Format(time.RFC3339)) 151 | gist.Files[github.GistFilename(filename)] = f 152 | err = box.UpdateGist(ctx, gistID, gist) 153 | if err != nil { 154 | t.Error(err) 155 | } 156 | } 157 | func Test_convertTime(t *testing.T) { 158 | type args struct { 159 | t string 160 | } 161 | tests := []struct { 162 | name string 163 | want string 164 | }{ 165 | { 166 | name: "10 hrs", 167 | want: "10h", 168 | }, 169 | { 170 | name: "18 hrs 40 mins", 171 | want: "18h40m", 172 | }, 173 | { 174 | name: "1 hr 13 mins", 175 | want: "1h13m", 176 | }, 177 | { 178 | name: "2 mins", 179 | want: "2m", 180 | }, 181 | { 182 | name: "0 secs", 183 | want: "0s", 184 | }, 185 | { 186 | name: "99 hrs 99 mins 99 secs", 187 | want: "99h99m99s", 188 | }, 189 | { 190 | name: "1 sec", 191 | want: "1s", 192 | }, 193 | { 194 | name: "1 min", 195 | want: "1m", 196 | }, 197 | } 198 | for _, tt := range tests { 199 | t.Run(tt.name, func(t *testing.T) { 200 | if got := convertDuration(tt.name); got != tt.want { 201 | t.Errorf("convertTime() = %v, want %v", got, tt.want) 202 | } 203 | }) 204 | } 205 | } 206 | 207 | func TestBox_Readme(t *testing.T) { 208 | wakaAPIKey := os.Getenv("WAKATIME_API_KEY") 209 | 210 | ghToken := os.Getenv("GH_TOKEN") 211 | ghUsername := os.Getenv("GH_USER") 212 | 213 | style := BoxStyle{ 214 | BarStyle: os.Getenv("GIST_BARSTYLE"), 215 | BarLength: os.Getenv("GIST_BARLENGTH"), 216 | TimeStyle: os.Getenv("GIST_TIMESTYLE"), 217 | } 218 | 219 | box := NewBox(wakaAPIKey, ghUsername, ghToken, style) 220 | 221 | ctx := context.Background() 222 | 223 | filename := "test.md" 224 | title := `#### 📊 Weekly development breakdown` 225 | content := []byte(`Go 🕓 18h3m ██████████████████████▉░░░░░ 82.1% 226 | YAML 🕓 1h47m ██▎░░░░░░░░░░░░░░░░░░░░░░░░░ 8.1% 227 | JavaScript 🕓 40m ▊░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3.1% 228 | Markdown 🕓 34m ▋░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2.6% 229 | Other 🕓 32m ▋░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2.5%`) 230 | 231 | err := box.UpdateMarkdown(ctx, title, filename, content) 232 | if err != nil { 233 | t.Error(err) 234 | } 235 | c, _ := ioutil.ReadFile(filename) 236 | if err != nil { 237 | t.Error(err) 238 | } 239 | t.Logf("%s", c) 240 | } 241 | -------------------------------------------------------------------------------- /pkg/wakabox/test.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | --------------------------------------------------------------------------------