├── .github └── workflows │ ├── go.yaml │ └── release.yaml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── internal ├── config │ ├── config.go │ ├── error_widget.go │ ├── parser.go │ ├── plugins.go │ ├── widget_config.go │ └── widget_event_config.go ├── logger │ └── looger.go └── registry │ ├── registry.go │ └── store │ └── store.go ├── main.go ├── pkg ├── executor │ └── executor.go └── signals │ ├── signals.go │ └── signals_c.go ├── plugins ├── .gitignore ├── README.md ├── example │ ├── Makefile │ ├── README.md │ ├── main.go │ ├── plugin │ │ └── spec.go │ ├── widget │ │ └── widget.go │ └── yagostatus.yml ├── example_builtin.go ├── keep.go ├── pprof │ ├── Makefile │ ├── README.md │ ├── main.go │ ├── plugin │ │ └── spec.go │ └── yagostatus.yml └── pprof_builtin.go ├── version.go ├── widgets ├── clock.go ├── exec.go ├── http.go ├── static.go └── wrapper.go ├── yagostatus.go ├── yagostatus.yml └── ygs ├── blankWidget.go ├── looger.go ├── parser.go ├── protocol.go ├── registry.go ├── vary.go └── widget.go /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow 2 | # This workflow will build a golang project 3 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 4 | 5 | name: Go 6 | 7 | on: 8 | workflow_call: 9 | push: 10 | branches: [ "master" ] 11 | pull_request: 12 | branches: [ "master" ] 13 | 14 | jobs: 15 | go: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v4 22 | with: 23 | go-version-file: "go.mod" 24 | check-latest: true 25 | cache: true 26 | 27 | - name: Build 28 | run: go build . 29 | - name: Test 30 | run: go test -race -cover -v ./... 31 | 32 | - name: Golangci-lint 33 | uses: golangci/golangci-lint-action@v6 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow 2 | name: Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | go: 11 | name: Go 12 | uses: ./.github/workflows/go.yaml 13 | secrets: inherit 14 | 15 | changelog: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | name: Changelog 20 | needs: [ "go", "build" ] 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: | 29 | gh release create --generate-notes --verify-tag "${GITHUB_REF#refs/*/}" 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yagostatus 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - asciicheck 5 | - bodyclose 6 | - deadcode 7 | - dogsled 8 | - dupl 9 | - errcheck 10 | - errorlint 11 | - exportloopref 12 | - goconst 13 | - gocritic 14 | - godot 15 | - godox 16 | - gofmt 17 | - gofumpt 18 | - goimports 19 | - golint 20 | - goprintffuncname 21 | - gosec 22 | - gosimple 23 | - govet 24 | - ineffassign 25 | - interfacer 26 | - lll 27 | - misspell 28 | - nakedret 29 | - nlreturn 30 | - noctx 31 | - prealloc 32 | - rowserrcheck 33 | - scopelint 34 | - sqlclosecheck 35 | - staticcheck 36 | - structcheck 37 | - stylecheck 38 | - typecheck 39 | - unconvert 40 | - unparam 41 | - unused 42 | - varcheck 43 | - whitespace 44 | - wsl 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YaGoStatus 2 | Yet Another i3status replacement written in Go. 3 | 4 | [![GitHub release](https://img.shields.io/github/release/burik666/yagostatus.svg)](https://github.com/burik666/yagostatus) 5 | [![Build Status](https://cloud.drone.io/api/badges/burik666/yagostatus/status.svg)](https://cloud.drone.io/burik666/yagostatus) 6 | [![GitHub license](https://img.shields.io/github/license/burik666/yagostatus.svg)](https://github.com/burik666/yagostatus/blob/master/LICENSE) 7 | 8 | 9 | ![yagostatus.gif](https://raw.githubusercontent.com/wiki/burik666/yagostatus/yagostatus.gif) 10 | 11 | ## Features 12 | - Instant and independent updating of widgets. 13 | - Handling click events. 14 | - Shell scripting widgets and events handlers. 15 | - Wrapping other status programs (i3status, py3status, conky, etc.). 16 | - Different widgets on different workspaces. 17 | - Templates for widgets outputs. 18 | - Update widget via http/websocket requests. 19 | - Update widget by POSIX Real-Time Signals (SIGRTMIN-SIGRTMAX). 20 | - [Snippets](https://github.com/burik666/ygs-snippets). 21 | - [Plugins](plugins). 22 | 23 | ## Installation 24 | 25 | go install github.com/burik666/yagostatus@latest 26 | mkdir -p ~/.config/yagostatus 27 | yagostatus -dump > ~/.config/yagostatus/yagostatus.yml 28 | 29 | Replace `status_command` to `~/go/bin/yagostatus --config ~/.config/yagostatus/yagostatus.yml` in your i3 config file. 30 | 31 | If you using Sway add the `--sway` parameter. 32 | 33 | ### Troubleshooting 34 | Yagostatus outputs error messages in stderr, you can log them by redirecting stderr to a file. 35 | 36 | `status_command exec ~/go/bin/yagostatus --config /path/to/yagostatus.yml 2> /tmp/yagostatus.log` 37 | 38 | ## Configuration 39 | 40 | If `--config` is not specified, yagostatus is looking for `yagostatus.yml` in `$HOME/.config/yagostatus` (or `$XDG_HOME_CONFIG/yagostatus` if set) or in the current working directory. 41 | 42 | Yagostatus uses a configuration file in the yaml format. 43 | 44 | Example: 45 | ```yml 46 | widgets: 47 | - widget: static 48 | blocks: > 49 | [ 50 | { 51 | "full_text": "YaGoStatus", 52 | "color": "#2e9ef4" 53 | } 54 | ] 55 | events: 56 | - button: 1 57 | command: xdg-open https://github.com/burik666/yagostatus/ 58 | 59 | - widget: wrapper 60 | command: /usr/bin/i3status 61 | 62 | - widget: clock 63 | format: Jan _2 Mon 15:04:05 # https://golang.org/pkg/time/#Time.Format 64 | templates: > 65 | [{ 66 | "color": "#ffffff", 67 | "separator": true, 68 | "separator_block_width": 21 69 | }] 70 | ``` 71 | ## Widgets 72 | 73 | ### Common parameters 74 | 75 | - `widget` - Widget name. 76 | - `workspaces` - List of workspaces to display the widget. 77 | 78 | Example: 79 | ```yml 80 | - widget: static 81 | workspaces: 82 | - "1:www" 83 | - "2:IM" 84 | 85 | blocks: > 86 | [ 87 | { 88 | "full_text": "Visible only on 1:www and 2:IM workspaces" 89 | } 90 | ] 91 | 92 | - widget: static 93 | workspaces: 94 | - "!1:www" 95 | 96 | blocks: > 97 | [ 98 | { 99 | "full_text": "Visible on all workspaces except 1:www" 100 | } 101 | ] 102 | ``` 103 | 104 | - `templates` - The templates that apply to widget blocks. 105 | - `events` - List of commands to be executed on user actions. 106 | * `button` - X11 button ID (0 for any, 1 to 3 for left/middle/right mouse button. 4/5 for mouse wheel up/down. Default: `0`). 107 | * `modifiers` - List of X11 modifiers condition. 108 | * `command` - Command to execute (via `sh -c`). 109 | Сlick_event json will be written to stdin. 110 | Also env variables are available: `$I3_NAME`, `$I3_INSTANCE`, `$I3_BUTTON`, `$I3_MODIFIERS`, `$I3_{X,Y}`, `$I3_OUTPUT_{X,Y}`, `$I3_RELATIVE_{X,Y}`, `$I3_{WIDTH,HEIGHT}`, `$I3_MODIFIERS`. 111 | The clicked widget fields are available as ENV variables with the prefix `I3_` (example:` $ I3_full_text`). 112 | * `workdir` - Set a working directory. 113 | * `env` - Set environment variables. 114 | * `output_format` - The command output format (`none`, `text`, `json`, `auto`) (default: `none`). 115 | * `name` - Filter by `name` for widgets with multiple blocks (default: empty). 116 | * `instance` - Filter by `instance` for widgets with multiple blocks (default: empty). 117 | * `override` - If `true`, previously defined events with the same `button`, `modifier`, `name` and `instance` will be ignored (default: `false`) 118 | 119 | Example: 120 | ```yml 121 | - widget: static 122 | blocks: > 123 | [ 124 | { 125 | "full_text": "Firefox", 126 | "name": "ff" 127 | }, 128 | { 129 | "full_text": "Chrome", 130 | "name": "ch" 131 | } 132 | ] 133 | templates: > 134 | [ 135 | { 136 | "color": "#ff8000" 137 | }, 138 | { 139 | "color": "#ff3030" 140 | } 141 | ] 142 | events: 143 | - button: 1 144 | command: /usr/bin/firefox 145 | name: ff 146 | 147 | - button: 1 148 | modifiers: 149 | - "!Control" # "!" must be quoted 150 | command: /usr/bin/chrome 151 | name: ch 152 | 153 | - button: 1 154 | - Control 155 | command: /usr/bin/chrome --incognito 156 | name: ch 157 | ``` 158 | 159 | ### Snippets 160 | 161 | Yagostatus supports the inclusion of snippets from files. 162 | ```yml 163 | - widget: $ygs-snippets/snip.yaml 164 | msg: hello world 165 | color: #00ff00 166 | ``` 167 | 168 | `ygs-snippets/snip.yaml`: 169 | ```yml 170 | variables: 171 | msg: "default messsage" 172 | color: #ffffff 173 | widgets: 174 | - widget: static 175 | blocks: > 176 | [ 177 | { 178 | "full_text": "message: ${msg}", 179 | "color": "${color}" 180 | } 181 | ] 182 | ``` 183 | 184 | `ygs-snippets/snip.yaml` - relative path from the current file. 185 | 186 | 187 | ### Widget `clock` 188 | 189 | The clock widget returns the current time in the specified format. 190 | 191 | - `format` - Time format (https://golang.org/pkg/time/#Time.Format). 192 | - `interval` - Clock update interval in seconds (default: `1`). 193 | 194 | 195 | ### Widget `exec` 196 | 197 | This widget runs the command at the specified interval. 198 | 199 | - `command` - Command to execute (via `sh -c`). 200 | - `workdir` - Set a working directory. 201 | - `env` - Set environment variables. 202 | - `interval` - Update interval in seconds (`0` to run once at start; `-1` for loop without delay; default: `0`). 203 | - `retry` - Retry interval in seconds if command failed (default: none). 204 | - `silent` - Don't show error widget if command failed (default: `false`). 205 | - `events_update` - Update widget if an event occurred (default: `false`). 206 | - `output_format` - The command output format (`none`, `text`, `json`, `auto`) (default: `auto`). 207 | - `signal` - SIGRTMIN offset to update widget. Should be between 0 and `SIGRTMIN`-`SIGRTMAX`. 208 | 209 | The current widget fields are available as ENV variables with the prefix `I3_` (example: `$I3_full_text`). 210 | For widgets with multiple blocks, an suffix with an index will be added. (example: `$I3_full_text`, `$I3_full_text_1`, `$I3_full_text_2`, etc.) 211 | 212 | Use pkill to send signals: 213 | 214 | pkill -SIGRTMIN+1 yagostatus 215 | 216 | 217 | ### Widget `wrapper` 218 | 219 | The wrapper widget starts the command and proxy received blocks (and click_events). 220 | See: https://i3wm.org/docs/i3bar-protocol.html 221 | 222 | - `command` - Command to execute. 223 | - `workdir` - Set a working directory. 224 | - 'env' - Set environment variables. 225 | 226 | 227 | ### Widget `static` 228 | 229 | The static widget renders the blocks. Useful for labels and buttons. 230 | 231 | - `blocks` - JSON List of i3bar blocks. 232 | 233 | 234 | ### Widget `http` 235 | 236 | The http widget starts http server and accept HTTP or Websocket requests. 237 | 238 | - `network` - `tcp` or `unix` (default `tcp`). 239 | - `listen` - Hostname and port or path to the socket file to bind (example: `localhost:9900`, `/tmp/yagostatus.sock`). 240 | - `path` - Path for receiving requests (example: `/mystatus/`). 241 | Must be unique for multiple widgets with same `listen`. 242 | 243 | For example, you can update the widget with the following command: 244 | 245 | curl http://localhost:9900/mystatus/ -d '[{"full_text": "hello"}, {"full_text": "world"}]' 246 | 247 | Send an empty array to clear: 248 | 249 | curl http://localhost:9900/mystatus/ -d '[]' 250 | 251 | Unix socket: 252 | 253 | curl --unix-socket /tmp/yagostatus.sock localhost/mystatus/ -d '[{"full_text": "hello"}]' 254 | 255 | 256 | ## Examples 257 | 258 | ### Counter 259 | 260 | This example shows how you can use custom fields. 261 | 262 | - Left mouse button - increment 263 | - Right mouse button - decrement 264 | - Middle mouse button - reset 265 | 266 | ```yml 267 | - widget: static 268 | blocks: > 269 | [ 270 | { 271 | "full_text":"COUNTER" 272 | } 273 | ] 274 | events: 275 | - command: | 276 | printf '[{"full_text":"Counter: %d", "_count":%d}]' $((I3__COUNT + 1)) $((I3__COUNT + 1)) 277 | output_format: json 278 | button: 1 279 | - command: | 280 | printf '[{"full_text":"Counter: %d", "_count":%d}]' $((I3__COUNT - 1)) $((I3__COUNT - 1)) 281 | output_format: json 282 | button: 3 283 | - command: | 284 | printf '[{"full_text":"Counter: 0", "_count":0}]' 285 | output_format: json 286 | button: 2 287 | ``` 288 | 289 | ### Volume control 290 | i3 config: 291 | ``` 292 | bindsym XF86AudioLowerVolume exec pactl set-sink-volume @DEFAULT_SINK@ -1%; exec pkill -SIGRTMIN+1 yagostatus 293 | bindsym XF86AudioRaiseVolume exec pactl set-sink-volume @DEFAULT_SINK@ +1%; exec pkill -SIGRTMIN+1 yagostatus 294 | bindsym XF86AudioMute exec pactl set-sink-mute @DEFAULT_SINK@ toggle; exec pkill -SIGRTMIN+1 yagostatus 295 | ``` 296 | 297 | ```yml 298 | - widget: exec 299 | command: | 300 | color="#ffffff" 301 | if [ $(pacmd list-sinks |sed '1,/* index/d'|grep -E '^\smuted:'|head -n1|awk '{print $2}') = "yes" ]; then 302 | color="#ff0000" 303 | fi 304 | volume=$(pacmd list-sinks |sed '1,/* index/d'|grep -E '^\svolume:'|head -n1|awk '{print $5}') 305 | echo -e '[{"full_text":"♪ '${volume}'","color":"'$color'"}]' 306 | 307 | interval: 0 308 | signal: 1 309 | events_update: true 310 | events: 311 | - button: 1 312 | command: pactl set-sink-mute @DEFAULT_SINK@ toggle 313 | 314 | - button: 4 315 | command: pactl set-sink-volume @DEFAULT_SINK@ +1% 316 | 317 | - button: 5 318 | command: pactl set-sink-volume @DEFAULT_SINK@ -1% 319 | 320 | templates: > 321 | [{ 322 | "markup": "pango", 323 | "separator": true, 324 | "separator_block_width": 21 325 | }] 326 | ``` 327 | 328 | ### Weather 329 | 330 | To get access to weather API you need an APIID. 331 | See https://openweathermap.org/appid for details. 332 | 333 | Requires [jq](https://stedolan.github.com/jq/) for json parsing. 334 | 335 | ```yml 336 | - widget: static 337 | blocks: > 338 | [ 339 | { 340 | "full_text": "Weather:", 341 | "color": "#2e9ef4", 342 | "separator": false 343 | } 344 | ] 345 | - widget: exec 346 | command: curl -s 'http://api.openweathermap.org/data/2.5/weather?q=London,uk&units=metric&appid='|jq .main.temp 347 | interval: 300 348 | templates: > 349 | [{ 350 | "separator": true, 351 | "separator_block_width": 21 352 | }] 353 | ``` 354 | 355 | You can use the [weather snippet](https://github.com/burik666/ygs-snippets/tree/master/weather) instead. 356 | 357 | ### Conky 358 | 359 | ```yml 360 | - widget: wrapper 361 | command: /usr/bin/conky -c /home/burik/.config/i3/conky.conf 362 | ``` 363 | Specify the full path to `conky.conf`. 364 | 365 | **conky.conf** (conky 1.10 or higher): 366 | ```lua 367 | conky.config = { 368 | out_to_x = false, 369 | own_window = false, 370 | out_to_console = true, 371 | background = false, 372 | max_text_width = 0, 373 | 374 | -- Update interval in seconds 375 | update_interval = 1.0, 376 | 377 | -- This is the number of times Conky will update before quitting. 378 | -- Set to zero to run forever. 379 | total_run_times = 0, 380 | 381 | -- Shortens units to a single character (kiB->k, GiB->G, etc.). Default is off. 382 | -- short_units yes 383 | 384 | -- Add spaces to keep things from moving about? This only affects certain objects. 385 | -- use_spacer should have an argument of left, right, or none 386 | use_spacer = 'left', 387 | 388 | -- Force UTF8? note that UTF8 support required XFT 389 | override_utf8_locale = false, 390 | 391 | -- number of cpu samples to average 392 | -- set to 1 to disable averaging 393 | cpu_avg_samples = 3, 394 | net_avg_samples = 3, 395 | diskio_avg_samples = 3, 396 | 397 | format_human_readable = true, 398 | lua_load = '~/.config/i3/conky.lua', 399 | }; 400 | 401 | conky.text = [[ 402 | 403 | [ 404 | 405 | { "full_text": "CPU:", "color": "\#2e9ef4", "separator": false }, 406 | { ${lua_parse cpu cpu1} , "min_width": "100%", "align": "right", "separator": false }, 407 | { ${lua_parse cpu cpu2} , "min_width": "100%", "align": "right", "separator": false }, 408 | { ${lua_parse cpu cpu3} , "min_width": "100%", "align": "right", "separator": false }, 409 | { ${lua_parse cpu cpu4} , "min_width": "100%", "align": "right", "separator": true, "separator_block_width":21 }, 410 | 411 | { "full_text": "RAM:", "color": "\#2e9ef4", "separator": false }, 412 | { "full_text": "${mem} / ${memeasyfree}", "color": ${if_match ${memperc}<80}"\#ffffff"${else}"\#ff0000"${endif}, "separator": true, "separator_block_width":21 }, 413 | 414 | { "full_text": "sda:", "color": "\#2e9ef4", "separator": false }, 415 | { "full_text": "▼ ${diskio_read sda} ▲ ${diskio_write sda}", "color": "\#ffffff", "separator": true, "separator_block_width":21 }, 416 | 417 | { "full_text": "eth0:", "color": "\#2e9ef4", "separator": false }, 418 | { "full_text": "▼ ${downspeed eth0} ▲ ${upspeed eth0}", "color": "\#ffffff", "separator": true, "separator_block_width":21 } 419 | 420 | ] 421 | 422 | ]]; 423 | ``` 424 | 425 | 426 | **conky.lua**: 427 | ```lua 428 | function gradient_red(min, max, val) 429 | local min = tonumber(min) 430 | local max = tonumber(max) 431 | local val = tonumber(val) 432 | if (val > max) then val = max end 433 | if (val < min) then val = min end 434 | 435 | local v = val - min 436 | local d = (max - min) * 0.5 437 | local red, green 438 | 439 | if (v <= d) then 440 | red = math.floor((255 * v) / d + 0.5) 441 | green = 255 442 | else 443 | red = 255 444 | green = math.floor(255 - (255 * (v-d)) / (max - min - d) + 0.5) 445 | end 446 | 447 | return string.format("%02x%02x00", red, green) 448 | end 449 | 450 | function conky_cpu (cpun) 451 | local val = tonumber(conky_parse("${cpu " .. cpun .. "}")) 452 | if val == nil then val = 0 end 453 | return "\"full_text\": \"" .. val .. "%\", \"color\": \"\\#" .. gradient_red(0, 100, val) .. "\"" 454 | end 455 | ``` 456 | 457 | ## License 458 | 459 | YaGoStatus is licensed under the GNU GPLv3 License. 460 | 461 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/burik666/yagostatus 2 | 3 | go 1.23 4 | 5 | require ( 6 | go.i3wm.org/i3/v4 v4.21.0 7 | golang.org/x/net v0.35.0 8 | gopkg.in/yaml.v2 v2.4.0 9 | ) 10 | 11 | require ( 12 | github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect 13 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk= 2 | github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 3 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= 4 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= 5 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 6 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 7 | go.i3wm.org/i3/v4 v4.21.0 h1:99EsxfEEV/raV5NqLFHiVMqprlPiSFFMCmTHE+DaLM0= 8 | go.i3wm.org/i3/v4 v4.21.0/go.mod h1:HIV6Kj7EyTYxYOF6l/mfu8KZczMEjjFkpXJ2+nnBf4o= 9 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 10 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 11 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 12 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 16 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 17 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "syscall" 8 | 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | // Config represents the main configuration. 13 | type Config struct { 14 | Signals struct { 15 | StopSignal syscall.Signal `yaml:"stop"` 16 | ContSignal syscall.Signal `yaml:"cont"` 17 | } `yaml:"signals"` 18 | Plugins struct { 19 | Path string `yaml:"path"` 20 | Load []PluginConfig `yaml:"load"` 21 | } `yaml:"plugins"` 22 | Variables map[string]interface{} `yaml:"variables"` 23 | Widgets []WidgetConfig `yaml:"widgets"` 24 | File string `yaml:"-"` 25 | } 26 | 27 | // SnippetConfig represents the snippet configuration. 28 | type SnippetConfig struct { 29 | Variables map[string]interface{} `yaml:"variables"` 30 | Widgets []WidgetConfig `yaml:"widgets"` 31 | } 32 | 33 | // LoadFile loads and parses config from file. 34 | func LoadFile(filename string) (*Config, error) { 35 | data, err := ioutil.ReadFile(filename) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | dir := filepath.Dir(filename) 41 | 42 | dir, err = filepath.Abs(dir) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | cfg, err := parse(data, dir, filepath.Base(filename)) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | cfg.File = filename 53 | 54 | return cfg, nil 55 | } 56 | 57 | // Parse parses config. 58 | func Parse(data []byte, source string) (*Config, error) { 59 | wd, err := os.Getwd() 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | cfg, err := parse(data, wd, source) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | cfg.File = source 70 | 71 | return cfg, nil 72 | } 73 | 74 | // Dump dumps config. 75 | func Dump(cfg *Config) ([]byte, error) { 76 | return yaml.Marshal(cfg) 77 | } 78 | -------------------------------------------------------------------------------- /internal/config/error_widget.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/burik666/yagostatus/ygs" 7 | ) 8 | 9 | // ErrorWidget creates new widget with error message. 10 | func ErrorWidget(text string) WidgetConfig { 11 | blocks, _ := json.Marshal([]ygs.I3BarBlock{ 12 | { 13 | FullText: text, 14 | Color: "#ff0000", 15 | }, 16 | }) 17 | 18 | return WidgetConfig{ 19 | Name: "static", 20 | Params: map[string]interface{}{ 21 | "blocks": string(blocks), 22 | }, 23 | File: "builtin", 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/config/parser.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | 16 | "github.com/burik666/yagostatus/internal/logger" 17 | "github.com/burik666/yagostatus/ygs" 18 | 19 | "gopkg.in/yaml.v2" 20 | ) 21 | 22 | func parse(data []byte, workdir string, source string) (*Config, error) { 23 | config := Config{} 24 | config.Signals.StopSignal = syscall.SIGUSR1 25 | config.Signals.ContSignal = syscall.SIGCONT 26 | 27 | if config.Plugins.Path == "" { 28 | wd, err := os.Getwd() 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | config.Plugins.Path = wd 34 | } 35 | 36 | if err := yaml.UnmarshalStrict(data, &config); err != nil { 37 | return nil, trimYamlErr(err, false) 38 | } 39 | 40 | for wi := range config.Widgets { 41 | config.Widgets[wi].File = source 42 | config.Widgets[wi].Index = wi 43 | } 44 | 45 | dict := make(map[string]string, len(config.Variables)) 46 | 47 | for k, v := range config.Variables { 48 | vb, err := json.Marshal(v) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | var vraw ygs.Vary 54 | 55 | err = json.Unmarshal(vb, &vraw) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | dict[fmt.Sprintf("${%s}", k)] = strings.TrimRight(vraw.String(), "\n") 61 | } 62 | 63 | v := reflect.ValueOf(config.Widgets) 64 | replaceRecursive(&v, dict) 65 | 66 | WIDGET: 67 | for wi := 0; wi < len(config.Widgets); wi++ { 68 | widget := &config.Widgets[wi] 69 | 70 | l := logger.WithPrefix(fmt.Sprintf("[%s#%d]", widget.File, widget.Index+1)) 71 | 72 | params := config.Widgets[wi].Params 73 | if params == nil { 74 | params = make(map[string]interface{}) 75 | } 76 | 77 | if widget.WorkDir == "" { 78 | widget.WorkDir = workdir 79 | } 80 | 81 | for i := range widget.Events { 82 | if widget.Events[i].WorkDir == "" { 83 | widget.Events[i].WorkDir = widget.WorkDir 84 | } 85 | } 86 | 87 | // for backward compatibility 88 | if itpl, ok := params["template"]; ok { 89 | tpl, ok := itpl.(string) 90 | if !ok { 91 | setError(widget, fmt.Errorf("invalid template"), false) 92 | 93 | continue WIDGET 94 | } 95 | 96 | widget.Templates = append(widget.Templates, ygs.I3BarBlock{}) 97 | if err := json.Unmarshal([]byte(tpl), &widget.Templates[0]); err != nil { 98 | setError(widget, err, false) 99 | 100 | continue WIDGET 101 | } 102 | } 103 | 104 | if itpls, ok := params["templates"]; ok { 105 | tpls, ok := itpls.(string) 106 | if !ok { 107 | setError(widget, fmt.Errorf("invalid template"), false) 108 | 109 | continue WIDGET 110 | } 111 | 112 | if err := json.Unmarshal([]byte(tpls), &widget.Templates); err != nil { 113 | setError(widget, err, false) 114 | 115 | continue WIDGET 116 | } 117 | } 118 | 119 | ok, err := parseSnippet(&config, wi, params) 120 | if err != nil { 121 | l.Errorf("parse snippets: %s", err) 122 | 123 | setError(widget, err, false) 124 | 125 | continue WIDGET 126 | } 127 | 128 | if ok { 129 | wi-- 130 | 131 | continue WIDGET 132 | } 133 | 134 | if err := widget.Validate(); err != nil { 135 | setError(widget, err, true) 136 | 137 | continue WIDGET 138 | } 139 | } 140 | 141 | return &config, nil 142 | } 143 | 144 | func parseSnippet(config *Config, wi int, params map[string]interface{}) (bool, error) { 145 | widget := config.Widgets[wi] 146 | 147 | if len(widget.Name) > 0 && widget.Name[0] == '$' { 148 | for i := range widget.IncludeStack { 149 | if widget.Name == widget.IncludeStack[i] { 150 | stack := append(widget.IncludeStack, widget.Name) 151 | 152 | return false, fmt.Errorf("recursive include: '%s'", strings.Join(stack, " -> ")) 153 | } 154 | } 155 | 156 | wd := widget.WorkDir 157 | 158 | filename := widget.Name[1:] 159 | if !filepath.IsAbs(filename) { 160 | filename = filepath.Join(wd, filename) 161 | } 162 | 163 | data, err := ioutil.ReadFile(filename) 164 | if err != nil { 165 | return false, err 166 | } 167 | 168 | var snippetConfig SnippetConfig 169 | if err := yaml.UnmarshalStrict(data, &snippetConfig); err != nil { 170 | return false, trimYamlErr(err, false) 171 | } 172 | 173 | for k, v := range snippetConfig.Variables { 174 | if _, ok := params[k]; !ok { 175 | params[k] = v 176 | } 177 | } 178 | 179 | dict := make(map[string]string, len(params)) 180 | 181 | for k, v := range params { 182 | if k == "template" || k == "templates" { 183 | continue 184 | } 185 | 186 | if _, ok := snippetConfig.Variables[k]; !ok { 187 | return false, fmt.Errorf("unknown variable '%s'", k) 188 | } 189 | 190 | vb, err := json.Marshal(v) 191 | if err != nil { 192 | return false, err 193 | } 194 | 195 | var vraw ygs.Vary 196 | 197 | err = json.Unmarshal(vb, &vraw) 198 | if err != nil { 199 | return false, err 200 | } 201 | 202 | dict[fmt.Sprintf("${%s}", k)] = strings.TrimRight(vraw.String(), "\n") 203 | } 204 | 205 | v := reflect.ValueOf(snippetConfig.Widgets) 206 | replaceRecursive(&v, dict) 207 | 208 | var tpls []byte 209 | if len(widget.Templates) > 0 { 210 | tpls, _ = json.Marshal(widget.Templates) 211 | } 212 | 213 | wd = filepath.Dir(filename) 214 | 215 | for i := range snippetConfig.Widgets { 216 | if snippetConfig.Widgets[i].WorkDir == "" { 217 | snippetConfig.Widgets[i].WorkDir = wd 218 | } 219 | 220 | snippetConfig.Widgets[i].File = filename 221 | snippetConfig.Widgets[i].Index = i 222 | //nolint:gocritic 223 | snippetConfig.Widgets[i].IncludeStack = append(widget.IncludeStack, widget.Name) 224 | if tpls != nil { 225 | snippetConfig.Widgets[i].Params["templates"] = string(tpls) 226 | _ = json.Unmarshal(tpls, &snippetConfig.Widgets[i].Templates) 227 | } 228 | 229 | snipEvents := snippetConfig.Widgets[i].Events 230 | for ei := range snipEvents { 231 | if snipEvents[ei].WorkDir == "" { 232 | snipEvents[ei].WorkDir = snippetConfig.Widgets[i].WorkDir 233 | } 234 | } 235 | 236 | for _, e := range widget.Events { 237 | if e.Override { 238 | sort.Strings(e.Modifiers) 239 | 240 | ne := make([]WidgetEventConfig, 0, len(snipEvents)) 241 | 242 | for _, se := range snipEvents { 243 | sort.Strings(se.Modifiers) 244 | 245 | if e.Button == se.Button && 246 | e.Name == se.Name && 247 | e.Instance == se.Instance && 248 | reflect.DeepEqual(e.Modifiers, se.Modifiers) { 249 | continue 250 | } 251 | 252 | ne = append(ne, se) 253 | } 254 | //nolint:gocritic 255 | snipEvents = append(ne, e) 256 | } else { 257 | snipEvents = append(snipEvents, e) 258 | } 259 | } 260 | 261 | snippetConfig.Widgets[i].Events = snipEvents 262 | } 263 | 264 | config.Widgets = append(config.Widgets[:wi], config.Widgets[wi+1:]...) 265 | config.Widgets = append(config.Widgets[:wi], append(snippetConfig.Widgets, config.Widgets[wi:]...)...) 266 | 267 | return true, nil 268 | } 269 | 270 | return false, nil 271 | } 272 | 273 | func setError(widget *WidgetConfig, err error, trimLineN bool) { 274 | *widget = ErrorWidget(trimYamlErr(err, trimLineN).Error()) 275 | } 276 | 277 | func trimYamlErr(err error, trimLineN bool) error { 278 | msg := strings.TrimPrefix(err.Error(), "yaml: ") 279 | msg = strings.TrimPrefix(msg, "unmarshal errors:\n ") 280 | 281 | if trimLineN { 282 | msg = strings.TrimPrefix(msg, "line ") 283 | msg = strings.TrimLeft(msg, "1234567890: ") 284 | } 285 | 286 | return errors.New(msg) 287 | } 288 | 289 | func replaceRecursive(v *reflect.Value, dict map[string]string) { 290 | vv := *v 291 | for vv.Kind() == reflect.Ptr || vv.Kind() == reflect.Interface { 292 | vv = vv.Elem() 293 | } 294 | 295 | switch vv.Kind() { 296 | case reflect.Slice, reflect.Array: 297 | for i := 0; i < vv.Len(); i++ { 298 | vi := vv.Index(i) 299 | replaceRecursive(&vi, dict) 300 | vv.Index(i).Set(vi) 301 | } 302 | case reflect.Map: 303 | for _, i := range vv.MapKeys() { 304 | vm := vv.MapIndex(i) 305 | replaceRecursive(&vm, dict) 306 | vv.SetMapIndex(i, vm) 307 | } 308 | case reflect.Struct: 309 | t := vv.Type() 310 | for i := 0; i < t.NumField(); i++ { 311 | vf := v.Field(i) 312 | replaceRecursive(&vf, dict) 313 | } 314 | case reflect.String: 315 | st := vv.String() 316 | for s, r := range dict { 317 | st = strings.ReplaceAll(st, s, r) 318 | } 319 | 320 | if n, err := strconv.ParseInt(st, 10, 64); err == nil { 321 | vi := reflect.New(reflect.ValueOf(n).Type()).Elem() 322 | vi.SetInt(n) 323 | *v = vi 324 | 325 | return 326 | } 327 | 328 | if vv.CanSet() { 329 | vv.SetString(st) 330 | } else { 331 | vn := reflect.New(vv.Type()).Elem() 332 | vn.SetString(st) 333 | *v = vn 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /internal/config/plugins.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "plugin" 7 | "reflect" 8 | 9 | "github.com/burik666/yagostatus/ygs" 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | type PluginConfig struct { 14 | Plugin string `yaml:"plugin"` 15 | Params map[string]interface{} `yaml:",inline"` 16 | } 17 | 18 | func LoadPlugins(cfg Config, logger ygs.Logger) error { 19 | for _, l := range cfg.Plugins.Load { 20 | fname := l.Plugin 21 | 22 | if !filepath.IsAbs(fname) { 23 | fname = filepath.Join(cfg.Plugins.Path, fname) 24 | } 25 | 26 | logger.Infof("Load plugin: %s", fname) 27 | 28 | p, err := plugin.Open(fname) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | plugin, err := p.Lookup("Plugin") 34 | if err != nil { 35 | return fmt.Errorf("variable Plugin: %w", err) 36 | } 37 | 38 | pv := reflect.ValueOf(plugin) 39 | 40 | specp, ok := pv.Interface().(*ygs.PluginSpec) 41 | if !ok { 42 | return fmt.Errorf("variable Plugin is not a ygs.PluginSpec") 43 | } 44 | 45 | spec := (*specp) 46 | 47 | spec.Name = fmt.Sprintf("%s#%s", l.Plugin, spec.Name) 48 | 49 | if spec.DefaultParams != nil { 50 | pb, err := yaml.Marshal(l.Params) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | params := reflect.New(reflect.TypeOf(spec.DefaultParams)) 56 | params.Elem().Set(reflect.ValueOf(spec.DefaultParams)) 57 | 58 | if err := yaml.UnmarshalStrict(pb, params.Interface()); err != nil { 59 | return trimYamlErr(err, true) 60 | } 61 | 62 | spec.DefaultParams = params.Elem().Interface() 63 | } 64 | 65 | if err := ygs.RegisterPlugin(spec); err != nil { 66 | return fmt.Errorf("failed to register plugin: %w", err) 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func InitPlugins(logger ygs.Logger) error { 74 | for _, yp := range ygs.RegisteredPlugins() { 75 | if yp.InitFunc != nil { 76 | logger.Infof("Init plugin: %s", yp.Name) 77 | 78 | l := logger.WithPrefix(fmt.Sprintf("[%s]", yp.Name)) 79 | if err := yp.InitFunc(yp.DefaultParams, l); err != nil { 80 | logger.Errorf("init [%s]: %s", yp.Name, err) 81 | } 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func ShutdownPlugins(logger ygs.Logger) { 89 | for _, yp := range ygs.RegisteredPlugins() { 90 | if yp.ShutdownFunc != nil { 91 | logger.Infof("Shutdown plugin: %s", yp.Name) 92 | 93 | if err := yp.ShutdownFunc(); err != nil { 94 | logger.Errorf("shutdown [%s]: %s", yp.Name, err) 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /internal/config/widget_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/burik666/yagostatus/ygs" 8 | ) 9 | 10 | // WidgetConfig represents a widget configuration. 11 | type WidgetConfig struct { 12 | Name string `yaml:"widget"` 13 | Workspaces []string `yaml:"workspaces"` 14 | Templates []ygs.I3BarBlock `yaml:"-"` 15 | Events []WidgetEventConfig `yaml:"events"` 16 | WorkDir string `yaml:"workdir"` 17 | Index int `yaml:"-"` 18 | File string `yaml:"-"` 19 | 20 | Params map[string]interface{} `yaml:",inline"` 21 | 22 | IncludeStack []string `yaml:"-"` 23 | } 24 | 25 | // Validate checks widget configuration. 26 | func (c WidgetConfig) Validate() error { 27 | if c.Name == "" { 28 | return errors.New("missing widget name") 29 | } 30 | 31 | for ei := range c.Events { 32 | if err := c.Events[ei].Validate(); err != nil { 33 | return fmt.Errorf("events#%d: %w", ei+1, err) 34 | } 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/config/widget_event_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // WidgetEventConfig represents a widget events. 9 | type WidgetEventConfig struct { 10 | Command string `yaml:"command"` 11 | Button uint8 `yaml:"button"` 12 | Modifiers []string `yaml:"modifiers,omitempty"` 13 | Name string `yaml:"name,omitempty"` 14 | Instance string `yaml:"instance,omitempty"` 15 | OutputFormat string `yaml:"output_format,omitempty"` 16 | Override bool `yaml:"override"` 17 | WorkDir string `yaml:"workdir"` 18 | Env []string `yaml:"env"` 19 | 20 | Params map[string]interface{} `yaml:",inline"` 21 | } 22 | 23 | // Validate checks event parameters. 24 | func (e *WidgetEventConfig) Validate() error { 25 | availableWidgetEventModifiers := [...]string{"Shift", "Control", "Mod1", "Mod2", "Mod3", "Mod4", "Mod5"} 26 | 27 | if len(e.Params) > 0 { 28 | for k := range e.Params { 29 | return fmt.Errorf("unknown '%s' parameter", k) 30 | } 31 | } 32 | 33 | for _, mod := range e.Modifiers { 34 | found := false 35 | mod = strings.TrimLeft(mod, "!") 36 | 37 | for _, m := range availableWidgetEventModifiers { 38 | if mod == m { 39 | found = true 40 | 41 | break 42 | } 43 | } 44 | 45 | if !found { 46 | return fmt.Errorf("unknown '%s' modifier", mod) 47 | } 48 | } 49 | 50 | if e.OutputFormat == "" { 51 | e.OutputFormat = "none" 52 | } 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/logger/looger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/burik666/yagostatus/ygs" 9 | ) 10 | 11 | func New() ygs.Logger { 12 | return &logger{ 13 | std: log.New(os.Stderr, "", log.Ldate+log.Ltime+log.Lshortfile), 14 | calldepth: 2, 15 | } 16 | } 17 | 18 | type logger struct { 19 | std *log.Logger 20 | prefix string 21 | calldepth int 22 | } 23 | 24 | func (l logger) outputf(calldepth int, subprefix string, format string, v ...interface{}) { 25 | st := l.prefix + subprefix + fmt.Sprintf(format, v...) 26 | _ = l.std.Output(calldepth+1, st) 27 | } 28 | 29 | func (l logger) Infof(format string, v ...interface{}) { 30 | l.outputf(l.calldepth, "INFO ", format, v...) 31 | } 32 | 33 | func (l logger) Errorf(format string, v ...interface{}) { 34 | l.outputf(l.calldepth, "ERROR ", format, v...) 35 | } 36 | 37 | func (l logger) Debugf(format string, v ...interface{}) { 38 | l.outputf(l.calldepth, "DEBUG ", format, v...) 39 | } 40 | 41 | func (l logger) WithPrefix(prefix string) ygs.Logger { 42 | l.prefix = prefix + " " 43 | 44 | return &l 45 | } 46 | 47 | var l = &logger{ 48 | std: log.New(os.Stderr, "", log.Ldate+log.Ltime+log.Lshortfile), 49 | calldepth: 3, 50 | } 51 | 52 | func Infof(format string, v ...interface{}) { 53 | l.Infof(format, v...) 54 | } 55 | 56 | func Errorf(format string, v ...interface{}) { 57 | l.Errorf(format, v...) 58 | } 59 | 60 | func Debugf(format string, v ...interface{}) { 61 | l.Debugf(format, v...) 62 | } 63 | 64 | func WithPrefix(prefix string) ygs.Logger { 65 | nl := *l 66 | nl.calldepth-- 67 | 68 | return nl.WithPrefix(prefix) 69 | } 70 | -------------------------------------------------------------------------------- /internal/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/burik666/yagostatus/internal/config" 10 | rs "github.com/burik666/yagostatus/internal/registry/store" 11 | "github.com/burik666/yagostatus/ygs" 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | // NewWidget creates new widget by name. 16 | func NewWidget(widgetConfig config.WidgetConfig, wlogger ygs.Logger) (ygs.Widget, error) { 17 | name := widgetConfig.Name 18 | wi, ok := rs.Load("widget_" + name) 19 | 20 | if !ok { 21 | return nil, fmt.Errorf("widget '%s' not found", name) 22 | } 23 | 24 | widget := wi.(ygs.WidgetSpec) 25 | if widget.DefaultParams == nil { 26 | return widget.NewFunc(nil, wlogger) 27 | } 28 | 29 | def := reflect.ValueOf(widget.DefaultParams) 30 | 31 | params := reflect.New(def.Type()) 32 | pe := params.Elem() 33 | pe.Set(def) 34 | 35 | delete(widgetConfig.Params, "template") 36 | delete(widgetConfig.Params, "templates") 37 | 38 | b, err := yaml.Marshal(widgetConfig.Params) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | if err := yaml.UnmarshalStrict(b, params.Interface()); err != nil { 44 | return nil, trimYamlErr(err, true) 45 | } 46 | 47 | if _, ok := widgetConfig.Params["workdir"]; !ok { 48 | for i := 0; i < pe.NumField(); i++ { 49 | fn := pe.Type().Field(i).Name 50 | if strings.ToLower(fn) == "workdir" { 51 | pe.Field(i).SetString(widgetConfig.WorkDir) 52 | } 53 | } 54 | } 55 | 56 | return widget.NewFunc(pe.Interface(), wlogger) 57 | } 58 | 59 | func trimYamlErr(err error, trimLineN bool) error { 60 | msg := strings.TrimPrefix(err.Error(), "yaml: unmarshal errors:\n ") 61 | if trimLineN { 62 | msg = strings.TrimPrefix(msg, "line ") 63 | msg = strings.TrimLeft(msg, "1234567890: ") 64 | } 65 | 66 | return errors.New(msg) 67 | } 68 | -------------------------------------------------------------------------------- /internal/registry/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var m sync.Map 8 | 9 | func Store(key, value interface{}) { 10 | m.Store(key, value) 11 | } 12 | 13 | func Load(key interface{}) (value interface{}, ok bool) { 14 | return m.Load(key) 15 | } 16 | 17 | func LoadAndDelete(key interface{}) (value interface{}, loaded bool) { 18 | return m.LoadAndDelete(key) 19 | } 20 | 21 | func LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) { 22 | return m.LoadOrStore(key, value) 23 | } 24 | 25 | func Delete(key interface{}) { 26 | m.Delete(key) 27 | } 28 | 29 | func Range(f func(key, value interface{}) bool) { 30 | m.Range(f) 31 | } 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Yet Another i3status replacement written in Go. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/burik666/yagostatus/internal/config" 12 | "github.com/burik666/yagostatus/internal/logger" 13 | ) 14 | 15 | var builtinConfig = []byte(` 16 | widgets: 17 | - widget: static 18 | blocks: > 19 | [ 20 | { 21 | "full_text": "YaGoStatus", 22 | "color": "#2e9ef4" 23 | } 24 | ] 25 | events: 26 | - button: 1 27 | command: xdg-open https://github.com/burik666/yagostatus/ 28 | - widget: wrapper 29 | command: /usr/bin/i3status 30 | - widget: clock 31 | format: Jan _2 Mon 15:04:05 # https://golang.org/pkg/time/#Time.Format 32 | templates: > 33 | [{ 34 | "color": "#ffffff", 35 | "separator": true, 36 | "separator_block_width": 21 37 | }] 38 | `) 39 | 40 | func main() { 41 | logger := logger.New() 42 | 43 | var configFile string 44 | 45 | flag.StringVar(&configFile, "config", "", `config file (default "yagostatus.yml")`) 46 | 47 | versionFlag := flag.Bool("version", false, "print version information and exit") 48 | swayFlag := flag.Bool("sway", false, "set it when using sway") 49 | dumpConfigFlag := flag.Bool("dump", false, "dump parsed config file to stdout") 50 | 51 | flag.Parse() 52 | 53 | if *versionFlag { 54 | logger.Infof("YaGoStatus %s", Version) 55 | 56 | return 57 | } 58 | 59 | var initErrors []error 60 | 61 | cfg, cfgError := loadConfig(configFile) 62 | if cfgError != nil { 63 | logger.Errorf("Failed to load config: %s", cfgError) 64 | initErrors = append(initErrors, cfgError) 65 | } 66 | 67 | if cfg != nil { 68 | logger.Infof("using config: %s", cfg.File) 69 | } else { 70 | cfg = &config.Config{} 71 | } 72 | 73 | if err := config.LoadPlugins(*cfg, logger); err != nil { 74 | logger.Errorf("Failed to load plugins: %s", err) 75 | initErrors = append(initErrors, err) 76 | } 77 | 78 | if *dumpConfigFlag { 79 | b, err := config.Dump(cfg) 80 | if err != nil { 81 | logger.Errorf("Failed to dump config: %s", err) 82 | os.Exit(1) 83 | } 84 | 85 | _, _ = os.Stdout.Write(b) 86 | os.Exit(0) 87 | } 88 | 89 | if err := config.InitPlugins(logger); err != nil { 90 | logger.Errorf("Failed to init plugins: %s", err) 91 | initErrors = append(initErrors, err) 92 | } 93 | 94 | yaGoStatus := NewYaGoStatus(*cfg, *swayFlag, logger) 95 | 96 | for _, err := range initErrors { 97 | yaGoStatus.errorWidget(err.Error()) 98 | } 99 | 100 | stopContSignals := make(chan os.Signal, 1) 101 | signal.Notify(stopContSignals, cfg.Signals.StopSignal, cfg.Signals.ContSignal) 102 | 103 | go func() { 104 | for { 105 | sig := <-stopContSignals 106 | switch sig { 107 | case cfg.Signals.StopSignal: 108 | yaGoStatus.Stop() 109 | case cfg.Signals.ContSignal: 110 | yaGoStatus.Continue() 111 | } 112 | } 113 | }() 114 | 115 | shutdownsignals := make(chan os.Signal, 1) 116 | signal.Notify(shutdownsignals, 117 | syscall.SIGINT, 118 | syscall.SIGTERM, 119 | syscall.SIGQUIT, 120 | syscall.SIGPIPE, 121 | ) 122 | 123 | go func() { 124 | if err := yaGoStatus.Run(); err != nil { 125 | logger.Errorf("Failed to run yagostatus: %s", err) 126 | } 127 | shutdownsignals <- syscall.SIGTERM 128 | }() 129 | 130 | <-shutdownsignals 131 | 132 | logger.Infof("shutdown") 133 | 134 | config.ShutdownPlugins(logger) 135 | 136 | yaGoStatus.Shutdown() 137 | 138 | logger.Infof("exit") 139 | } 140 | 141 | func loadConfig(configFile string) (*config.Config, error) { 142 | if configFile == "" { 143 | configDir, err := os.UserConfigDir() 144 | if err != nil { 145 | return nil, fmt.Errorf("failed to get config dir: %w", err) 146 | } 147 | 148 | cfg, err := config.LoadFile(configDir + "/yagostatus/yagostatus.yml") 149 | if os.IsNotExist(err) { 150 | cfg, err := config.LoadFile("yagostatus.yml") 151 | if os.IsNotExist(err) { 152 | return config.Parse(builtinConfig, "builtin") 153 | } 154 | 155 | return cfg, err 156 | } 157 | 158 | return cfg, err 159 | } 160 | 161 | return config.LoadFile(configFile) 162 | } 163 | -------------------------------------------------------------------------------- /pkg/executor/executor.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "os/exec" 12 | "regexp" 13 | "strings" 14 | "syscall" 15 | 16 | "github.com/burik666/yagostatus/ygs" 17 | ) 18 | 19 | type OutputFormat string 20 | 21 | const ( 22 | OutputFormatAuto OutputFormat = "auto" 23 | OutputFormatNone OutputFormat = "none" 24 | OutputFormatText OutputFormat = "text" 25 | OutputFormatJSON OutputFormat = "json" 26 | ) 27 | 28 | type Executor struct { 29 | cmd *exec.Cmd 30 | header *ygs.I3BarHeader 31 | 32 | finished bool 33 | waiterr error 34 | } 35 | 36 | func Exec(command string, args ...string) (*Executor, error) { 37 | r := regexp.MustCompile("'.+'|\".+\"|\\S+") 38 | m := r.FindAllString(command, -1) 39 | name := m[0] 40 | args = append(m[1:], args...) 41 | 42 | e := &Executor{} 43 | 44 | e.cmd = exec.Command(name, args...) 45 | e.cmd.Env = os.Environ() 46 | e.cmd.SysProcAttr = &syscall.SysProcAttr{ 47 | Setpgid: true, 48 | Pgid: 0, 49 | } 50 | 51 | return e, nil 52 | } 53 | 54 | func (e *Executor) SetWD(wd string) { 55 | if e.cmd != nil { 56 | e.cmd.Dir = wd 57 | } 58 | } 59 | 60 | func (e *Executor) Run(logger ygs.Logger, c chan<- []ygs.I3BarBlock, format OutputFormat) error { 61 | stderr, err := e.cmd.StderrPipe() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | defer stderr.Close() 67 | 68 | go (func() { 69 | scanner := bufio.NewScanner(stderr) 70 | for scanner.Scan() { 71 | logger.Errorf("(stderr) %s", scanner.Text()) 72 | } 73 | })() 74 | 75 | stdout, err := e.cmd.StdoutPipe() 76 | if err != nil { 77 | return err 78 | } 79 | 80 | defer stdout.Close() 81 | 82 | if err := e.cmd.Start(); err != nil { 83 | return err 84 | } 85 | 86 | defer func() { 87 | _ = e.wait() 88 | }() 89 | 90 | if format == OutputFormatNone { 91 | return nil 92 | } 93 | 94 | buf := &bufferCloser{} 95 | outreader := io.TeeReader(stdout, buf) 96 | 97 | decoder := json.NewDecoder(outreader) 98 | 99 | var firstMessage interface{} 100 | 101 | err = decoder.Decode(&firstMessage) 102 | if (err != nil) && format == OutputFormatJSON { 103 | buf.Close() 104 | 105 | return err 106 | } 107 | 108 | isJSON := false 109 | switch firstMessage.(type) { 110 | case map[string]interface{}: 111 | isJSON = true 112 | case []interface{}: 113 | isJSON = true 114 | } 115 | 116 | if err != nil || !isJSON || format == OutputFormatText { 117 | _, err := io.Copy(ioutil.Discard, outreader) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | if buf.Len() > 0 { 123 | lines := strings.Split(strings.Trim(buf.String(), "\n"), "\n") 124 | out := make([]ygs.I3BarBlock, len(lines)) 125 | 126 | for i := range lines { 127 | out[i] = ygs.I3BarBlock{ 128 | FullText: strings.Trim(lines[i], "\n "), 129 | } 130 | } 131 | c <- out 132 | } 133 | 134 | buf.Close() 135 | 136 | return nil 137 | } 138 | 139 | buf.Close() 140 | 141 | firstMessageData, _ := json.Marshal(firstMessage) 142 | 143 | headerDecoder := json.NewDecoder(bytes.NewBuffer(firstMessageData)) 144 | headerDecoder.DisallowUnknownFields() 145 | 146 | var header ygs.I3BarHeader 147 | if err := headerDecoder.Decode(&header); err == nil { 148 | e.header = &header 149 | 150 | _, err := decoder.Token() 151 | if err != nil { 152 | return err 153 | } 154 | } else { 155 | var blocks []ygs.I3BarBlock 156 | if err := json.Unmarshal(firstMessageData, &blocks); err != nil { 157 | return err 158 | } 159 | c <- blocks 160 | } 161 | 162 | defer func() { 163 | _ = e.Shutdown() 164 | }() 165 | 166 | for { 167 | var blocks []ygs.I3BarBlock 168 | if err := decoder.Decode(&blocks); err != nil { 169 | if errors.Is(err, io.EOF) { 170 | return nil 171 | } 172 | 173 | if e.finished { 174 | return nil 175 | } 176 | 177 | return err 178 | } 179 | c <- blocks 180 | } 181 | } 182 | 183 | func (e *Executor) Stdin() (io.WriteCloser, error) { 184 | return e.cmd.StdinPipe() 185 | } 186 | 187 | func (e *Executor) AddEnv(env ...string) { 188 | e.cmd.Env = append(e.cmd.Env, env...) 189 | } 190 | 191 | func (e *Executor) wait() error { 192 | if e.finished { 193 | return e.waiterr 194 | } 195 | 196 | e.waiterr = e.cmd.Wait() 197 | e.finished = true 198 | 199 | return e.waiterr 200 | } 201 | 202 | func (e *Executor) Shutdown() error { 203 | if e.finished { 204 | return nil 205 | } 206 | 207 | if e.cmd != nil && e.cmd.Process != nil && e.cmd.Process.Pid > 1 { 208 | return syscall.Kill(-e.cmd.Process.Pid, syscall.SIGTERM) 209 | } 210 | 211 | return e.wait() 212 | } 213 | 214 | func (e *Executor) Signal(sig syscall.Signal) error { 215 | if e.cmd != nil && e.cmd.Process != nil && e.cmd.Process.Pid > 1 { 216 | return syscall.Kill(-e.cmd.Process.Pid, sig) 217 | } 218 | 219 | return nil 220 | } 221 | 222 | func (e *Executor) ProcessState() *os.ProcessState { 223 | return e.cmd.ProcessState 224 | } 225 | 226 | func (e *Executor) I3BarHeader() *ygs.I3BarHeader { 227 | return e.header 228 | } 229 | 230 | type bufferCloser struct { 231 | bytes.Buffer 232 | stoped bool 233 | } 234 | 235 | func (b *bufferCloser) Write(p []byte) (n int, err error) { 236 | if b.stoped { 237 | return len(p), nil 238 | } 239 | 240 | return b.Buffer.Write(p) 241 | } 242 | 243 | func (b *bufferCloser) Close() error { 244 | b.stoped = true 245 | b.Reset() 246 | 247 | return nil 248 | } 249 | -------------------------------------------------------------------------------- /pkg/signals/signals.go: -------------------------------------------------------------------------------- 1 | // +build !cgo 2 | 3 | package signals 4 | 5 | // SIGRTMIN signal. 6 | var SIGRTMIN int = 34 7 | 8 | // SIGRTMAX signal. 9 | var SIGRTMAX int = 64 10 | -------------------------------------------------------------------------------- /pkg/signals/signals_c.go: -------------------------------------------------------------------------------- 1 | // +build cgo 2 | 3 | package signals 4 | 5 | // #include 6 | import "C" 7 | 8 | // SIGRTMIN signal. 9 | var SIGRTMIN int = int(C.SIGRTMIN) 10 | 11 | // SIGRTMAX signal. 12 | var SIGRTMAX int = int(C.SIGRTMAX) 13 | -------------------------------------------------------------------------------- /plugins/.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Yagostatus plugins 2 | 3 | ## Overview 4 | 5 | Yagostatus supports [go plugins](https://golang.org/pkg/plugin/). 6 | Plugins can be used to add or replace existing widgets. 7 | 8 | To load a plugin, you need to specify it in the config file. 9 | `yagostatus.yml`: 10 | ```yaml 11 | plugins: 12 | path: /path/to/plugins 13 | load: 14 | - plugin: example.so 15 | param: value 16 | ``` 17 | - `path` - Directory where the `.so` file are located (default: current working directory). 18 | - `plugin` - Plugin file (you can specify an absolute path). 19 | - Plugins can have parameters. 20 | 21 | ## Example 22 | 23 | See [example](example) 24 | 25 | ## Builtin 26 | 27 | Plugins can be embedded in the yagostatus binary file. 28 | 29 | go get -tags plugin_example github.com/burik666/yagostatus 30 | 31 | See [example_builtin.go](example_builtin.go), [example/builtin.go](example/builtin.go) 32 | 33 | -------------------------------------------------------------------------------- /plugins/example/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | go build -ldflags "-s -w" -buildmode=plugin 4 | -------------------------------------------------------------------------------- /plugins/example/README.md: -------------------------------------------------------------------------------- 1 | # Example plugin 2 | This is an example plugin that adds a widget. 3 | 4 | ## Parameters 5 | - `default_message` - Default message for the example widget. 6 | 7 | ## Widget `example` 8 | - `message` - Message to display (default: `default_message`). 9 | 10 | ```yaml 11 | - widget: example 12 | message: "Hello world" 13 | ``` 14 | -------------------------------------------------------------------------------- /plugins/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/burik666/yagostatus/plugins/example/plugin" 5 | ) 6 | 7 | // Plugin exported plugin spec. 8 | //nolint:deadcode,unused 9 | var Plugin = plugin.Spec 10 | -------------------------------------------------------------------------------- /plugins/example/plugin/spec.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/burik666/yagostatus/plugins/example/widget" 5 | "github.com/burik666/yagostatus/ygs" 6 | ) 7 | 8 | // Params contains example plugin parameters. 9 | type Params struct { 10 | DefaultMessage string `yaml:"default_message"` 11 | } 12 | 13 | var Spec = ygs.PluginSpec{ 14 | Name: "example", 15 | DefaultParams: Params{ 16 | "not set", 17 | }, 18 | InitFunc: func(p interface{}, l ygs.Logger) error { 19 | params := p.(Params) 20 | l.Infof("params: %+v", params) 21 | 22 | if err := ygs.RegisterWidget(ygs.WidgetSpec{ 23 | Name: "example", 24 | NewFunc: widget.NewWidget, 25 | DefaultParams: widget.Params{ 26 | Message: params.DefaultMessage, 27 | }, 28 | }); err != nil { 29 | panic(err) 30 | } 31 | 32 | return nil 33 | }, 34 | 35 | ShutdownFunc: func() error { 36 | return nil 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /plugins/example/widget/widget.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "github.com/burik666/yagostatus/ygs" 5 | ) 6 | 7 | // Params are widget parameters. 8 | type Params struct { 9 | Message string 10 | } 11 | 12 | // Widget implements a widget. 13 | type Widget struct { 14 | ygs.BlankWidget 15 | 16 | params Params 17 | } 18 | 19 | // NewWidget returns a new Widget. 20 | func NewWidget(params interface{}, wlogger ygs.Logger) (ygs.Widget, error) { 21 | w := &Widget{ 22 | params: params.(Params), 23 | } 24 | 25 | return w, nil 26 | } 27 | 28 | // Run starts the main loop. 29 | func (w *Widget) Run(c chan<- []ygs.I3BarBlock) error { 30 | c <- []ygs.I3BarBlock{ 31 | { 32 | FullText: w.params.Message, 33 | }, 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /plugins/example/yagostatus.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | load: 3 | - plugin: example.so 4 | default_message: "hello world" 5 | widgets: 6 | - widget: example 7 | 8 | - widget: example 9 | message: "hi" 10 | -------------------------------------------------------------------------------- /plugins/example_builtin.go: -------------------------------------------------------------------------------- 1 | // +build plugin_example 2 | 3 | package plugins 4 | 5 | import ( 6 | "github.com/burik666/yagostatus/plugins/example/plugin" 7 | "github.com/burik666/yagostatus/ygs" 8 | ) 9 | 10 | func init() { 11 | if err := ygs.RegisterPlugin(plugin.Spec); err != nil { 12 | panic(err) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /plugins/keep.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | -------------------------------------------------------------------------------- /plugins/pprof/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | go build -ldflags "-s -w" -buildmode=plugin 4 | -------------------------------------------------------------------------------- /plugins/pprof/README.md: -------------------------------------------------------------------------------- 1 | # pprof plugin 2 | 3 | This plugin runs an http server with [pprof](https://golang.org/pkg/net/http/pprof/). 4 | 5 | ## Build 6 | 7 | go get -tags plugin_pprof github.com/burik666/yagostatus 8 | 9 | ## Parameters 10 | - `listen` - Address and port for listen (default: `localhost:6060`). 11 | -------------------------------------------------------------------------------- /plugins/pprof/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/burik666/yagostatus/plugins/pprof/plugin" 5 | ) 6 | 7 | // Plugin exported plugin spec. 8 | //nolint:deadcode,unused 9 | var Plugin = plugin.Spec 10 | -------------------------------------------------------------------------------- /plugins/pprof/plugin/spec.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/pprof" 7 | 8 | "github.com/burik666/yagostatus/ygs" 9 | ) 10 | 11 | type Params struct { 12 | Listen string 13 | } 14 | 15 | var srv *http.Server 16 | 17 | var Spec = ygs.PluginSpec{ 18 | Name: "pprof", 19 | DefaultParams: Params{ 20 | Listen: "localhost:6060", 21 | }, 22 | InitFunc: func(p interface{}, l ygs.Logger) error { 23 | params := p.(Params) 24 | l.Infof("http://%s/debug/pprof", params.Listen) 25 | 26 | srv = &http.Server{ 27 | Addr: params.Listen, 28 | } 29 | 30 | mux := http.ServeMux{} 31 | 32 | mux.HandleFunc("/debug/pprof/", pprof.Index) 33 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 34 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 35 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 36 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 37 | 38 | go func() { 39 | l.Infof("%s", srv.ListenAndServe()) 40 | }() 41 | 42 | return nil 43 | }, 44 | ShutdownFunc: func() error { 45 | if srv == nil { 46 | return nil 47 | } 48 | 49 | return srv.Shutdown(context.Background()) 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /plugins/pprof/yagostatus.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | load: 3 | - plugin: pprof.so 4 | listen: localhost:8080 5 | -------------------------------------------------------------------------------- /plugins/pprof_builtin.go: -------------------------------------------------------------------------------- 1 | // +build plugin_pprof 2 | 3 | package plugins 4 | 5 | import ( 6 | "github.com/burik666/yagostatus/plugins/pprof/plugin" 7 | "github.com/burik666/yagostatus/ygs" 8 | ) 9 | 10 | func init() { 11 | if err := ygs.RegisterPlugin(plugin.Spec); err != nil { 12 | panic(err) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Version contains YaGoStatus version. 4 | const Version = "1.1.0" 5 | -------------------------------------------------------------------------------- /widgets/clock.go: -------------------------------------------------------------------------------- 1 | package widgets 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/burik666/yagostatus/ygs" 7 | ) 8 | 9 | // ClockWidgetParams are widget parameters. 10 | type ClockWidgetParams struct { 11 | Interval uint 12 | Format string 13 | } 14 | 15 | // ClockWidget implements a clock. 16 | type ClockWidget struct { 17 | ygs.BlankWidget 18 | 19 | params ClockWidgetParams 20 | } 21 | 22 | func init() { 23 | if err := ygs.RegisterWidget(ygs.WidgetSpec{ 24 | Name: "clock", 25 | NewFunc: NewClockWidget, 26 | DefaultParams: ClockWidgetParams{ 27 | Interval: 1, 28 | Format: "Jan _2 Mon 15:04:05", 29 | }, 30 | }); err != nil { 31 | panic(err) 32 | } 33 | } 34 | 35 | // NewClockWidget returns a new ClockWidget. 36 | func NewClockWidget(params interface{}, wlogger ygs.Logger) (ygs.Widget, error) { 37 | w := &ClockWidget{ 38 | params: params.(ClockWidgetParams), 39 | } 40 | 41 | return w, nil 42 | } 43 | 44 | // Run starts the main loop. 45 | func (w *ClockWidget) Run(c chan<- []ygs.I3BarBlock) error { 46 | res := []ygs.I3BarBlock{ 47 | {}, 48 | } 49 | res[0].FullText = time.Now().Format(w.params.Format) 50 | 51 | c <- res 52 | 53 | ticker := time.NewTicker(time.Duration(w.params.Interval) * time.Second) 54 | for t := range ticker.C { 55 | res[0].FullText = t.Format(w.params.Format) 56 | c <- res 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /widgets/exec.go: -------------------------------------------------------------------------------- 1 | package widgets 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "sync" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/burik666/yagostatus/pkg/executor" 13 | "github.com/burik666/yagostatus/pkg/signals" 14 | "github.com/burik666/yagostatus/ygs" 15 | ) 16 | 17 | // ExecWidgetParams are widget parameters. 18 | type ExecWidgetParams struct { 19 | Command string 20 | Interval int 21 | Retry *int 22 | Silent bool 23 | EventsUpdate bool `yaml:"events_update"` 24 | Signal *int 25 | OutputFormat executor.OutputFormat `yaml:"output_format"` 26 | WorkDir string 27 | Env []string 28 | } 29 | 30 | // ExecWidget implements the exec widget. 31 | type ExecWidget struct { 32 | ygs.BlankWidget 33 | 34 | params ExecWidgetParams 35 | 36 | logger ygs.Logger 37 | 38 | signal os.Signal 39 | c chan<- []ygs.I3BarBlock 40 | upd chan struct{} 41 | tickerC *chan struct{} 42 | env []string 43 | 44 | outputWG sync.WaitGroup 45 | exc *executor.Executor 46 | shutdown bool 47 | } 48 | 49 | func init() { 50 | if err := ygs.RegisterWidget(ygs.WidgetSpec{ 51 | Name: "exec", 52 | NewFunc: NewExecWidget, 53 | DefaultParams: ExecWidgetParams{}, 54 | }); err != nil { 55 | panic(err) 56 | } 57 | } 58 | 59 | // NewExecWidget returns a new ExecWidget. 60 | func NewExecWidget(params interface{}, wlogger ygs.Logger) (ygs.Widget, error) { 61 | w := &ExecWidget{ 62 | params: params.(ExecWidgetParams), 63 | logger: wlogger, 64 | } 65 | 66 | if len(w.params.Command) == 0 { 67 | return nil, errors.New("missing 'command'") 68 | } 69 | 70 | if w.params.Retry != nil && 71 | *w.params.Retry > 0 && 72 | w.params.Interval > 0 && 73 | *w.params.Retry >= w.params.Interval { 74 | return nil, errors.New("restart value should be less than interval") 75 | } 76 | 77 | if w.params.Signal != nil { 78 | sig := *w.params.Signal 79 | if sig < 0 || signals.SIGRTMIN+sig > signals.SIGRTMAX { 80 | return nil, fmt.Errorf("signal should be between 0 AND %d", signals.SIGRTMAX-signals.SIGRTMIN) 81 | } 82 | 83 | w.signal = syscall.Signal(signals.SIGRTMIN + sig) 84 | } 85 | 86 | w.upd = make(chan struct{}, 1) 87 | w.upd <- struct{}{} 88 | 89 | return w, nil 90 | } 91 | 92 | func (w *ExecWidget) exec() error { 93 | exc, err := executor.Exec("sh", "-c", w.params.Command) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | w.exc = exc 99 | 100 | exc.SetWD(w.params.WorkDir) 101 | 102 | exc.AddEnv(w.env...) 103 | exc.AddEnv(w.params.Env...) 104 | 105 | c := make(chan []ygs.I3BarBlock) 106 | 107 | defer close(c) 108 | 109 | w.outputWG.Add(1) 110 | 111 | go (func() { 112 | defer w.outputWG.Done() 113 | 114 | for { 115 | blocks, ok := <-c 116 | if !ok { 117 | return 118 | } 119 | w.c <- blocks 120 | w.setEnv(blocks) 121 | } 122 | })() 123 | 124 | err = exc.Run(w.logger, c, w.params.OutputFormat) 125 | if err == nil { 126 | if state := exc.ProcessState(); state != nil && state.ExitCode() != 0 { 127 | if w.params.Retry != nil { 128 | go (func() { 129 | time.Sleep(time.Second * time.Duration(*w.params.Retry)) 130 | w.upd <- struct{}{} 131 | w.resetTicker() 132 | })() 133 | } 134 | 135 | if w.shutdown { 136 | return nil 137 | } 138 | 139 | return fmt.Errorf("process exited unexpectedly: %s", state.String()) 140 | } 141 | } 142 | 143 | return err 144 | } 145 | 146 | // Run starts the main loop. 147 | func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { 148 | w.c = c 149 | if w.params.Interval == 0 && w.signal == nil && w.params.Retry == nil { 150 | err := w.exec() 151 | if w.params.Silent { 152 | if err != nil { 153 | w.logger.Errorf("exec failed: %s", err) 154 | } 155 | 156 | return nil 157 | } 158 | 159 | return err 160 | } 161 | 162 | if w.params.Interval > 0 { 163 | w.resetTicker() 164 | } 165 | 166 | if w.params.Interval == -1 { 167 | go (func() { 168 | for { 169 | w.upd <- struct{}{} 170 | } 171 | })() 172 | } 173 | 174 | if w.signal != nil { 175 | sigc := make(chan os.Signal, 1) 176 | signal.Notify(sigc, w.signal) 177 | 178 | go (func() { 179 | for { 180 | <-sigc 181 | w.upd <- struct{}{} 182 | } 183 | })() 184 | } 185 | 186 | for range w.upd { 187 | if err := w.exec(); err != nil { 188 | if !w.params.Silent { 189 | w.outputWG.Wait() 190 | 191 | c <- []ygs.I3BarBlock{{ 192 | FullText: err.Error(), 193 | Color: "#ff0000", 194 | }} 195 | } 196 | 197 | w.logger.Errorf("exec failed: %s", err) 198 | } 199 | } 200 | 201 | return nil 202 | } 203 | 204 | // Event processes the widget events. 205 | func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error { 206 | w.setEnv(blocks) 207 | 208 | if w.params.EventsUpdate { 209 | w.upd <- struct{}{} 210 | } 211 | 212 | return nil 213 | } 214 | 215 | func (w *ExecWidget) setEnv(blocks []ygs.I3BarBlock) { 216 | env := make([]string, 0) 217 | 218 | for i, block := range blocks { 219 | suffix := "" 220 | if i > 0 { 221 | suffix = fmt.Sprintf("_%d", i) 222 | } 223 | 224 | env = append(env, block.Env(suffix)...) 225 | } 226 | 227 | w.env = env 228 | } 229 | 230 | // Shutdown shutdowns the widget. 231 | func (w *ExecWidget) Shutdown() error { 232 | w.shutdown = true 233 | 234 | if w.exc != nil { 235 | if err := w.exc.Shutdown(); err != nil { 236 | return err 237 | } 238 | } 239 | 240 | return nil 241 | } 242 | 243 | func (w *ExecWidget) resetTicker() { 244 | if w.tickerC != nil { 245 | *w.tickerC <- struct{}{} 246 | } 247 | 248 | if w.params.Interval > 0 { 249 | tickerC := make(chan struct{}, 1) 250 | w.tickerC = &tickerC 251 | 252 | go (func() { 253 | ticker := time.NewTicker(time.Duration(w.params.Interval) * time.Second) 254 | 255 | defer ticker.Stop() 256 | 257 | for { 258 | select { 259 | case <-tickerC: 260 | return 261 | case <-ticker.C: 262 | w.upd <- struct{}{} 263 | } 264 | } 265 | })() 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /widgets/http.go: -------------------------------------------------------------------------------- 1 | package widgets 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "sync" 13 | 14 | "github.com/burik666/yagostatus/ygs" 15 | 16 | "golang.org/x/net/websocket" 17 | ) 18 | 19 | // HTTPWidgetParams are widget parameters. 20 | type HTTPWidgetParams struct { 21 | Network string 22 | Listen string 23 | Path string 24 | } 25 | 26 | // HTTPWidget implements the http server widget. 27 | type HTTPWidget struct { 28 | ygs.BlankWidget 29 | 30 | params HTTPWidgetParams 31 | 32 | logger ygs.Logger 33 | 34 | c chan<- []ygs.I3BarBlock 35 | instance *httpInstance 36 | 37 | clients map[*websocket.Conn]chan interface{} 38 | cm sync.RWMutex 39 | } 40 | 41 | type httpInstance struct { 42 | l net.Listener 43 | server *http.Server 44 | mux *http.ServeMux 45 | paths map[string]struct{} 46 | } 47 | 48 | var instances map[string]*httpInstance 49 | 50 | func init() { 51 | if err := ygs.RegisterWidget(ygs.WidgetSpec{ 52 | Name: "http", 53 | NewFunc: NewHTTPWidget, 54 | DefaultParams: HTTPWidgetParams{ 55 | Network: "tcp", 56 | }, 57 | }); err != nil { 58 | panic(err) 59 | } 60 | 61 | instances = make(map[string]*httpInstance, 1) 62 | } 63 | 64 | // NewHTTPWidget returns a new HTTPWidget. 65 | func NewHTTPWidget(params interface{}, wlogger ygs.Logger) (ygs.Widget, error) { 66 | w := &HTTPWidget{ 67 | params: params.(HTTPWidgetParams), 68 | logger: wlogger, 69 | } 70 | 71 | if len(w.params.Listen) == 0 { 72 | return nil, errors.New("missing 'listen'") 73 | } 74 | 75 | if len(w.params.Path) == 0 { 76 | return nil, errors.New("missing 'path'") 77 | } 78 | 79 | if w.params.Network != "tcp" && w.params.Network != "unix" { 80 | return nil, errors.New("invalid 'net' (may be 'tcp' or 'unix')") 81 | } 82 | 83 | instanceKey := w.params.Listen 84 | instance, ok := instances[instanceKey] 85 | 86 | if ok { 87 | if _, ok := instance.paths[w.params.Path]; ok { 88 | return nil, fmt.Errorf("path '%s' already in use", w.params.Path) 89 | } 90 | } else { 91 | mux := http.NewServeMux() 92 | instance = &httpInstance{ 93 | mux: mux, 94 | paths: make(map[string]struct{}, 1), 95 | server: &http.Server{ 96 | Addr: w.params.Listen, 97 | Handler: mux, 98 | }, 99 | } 100 | 101 | instances[w.params.Listen] = instance 102 | w.instance = instance 103 | } 104 | 105 | instance.mux.HandleFunc(w.params.Path, w.httpHandler) 106 | instance.paths[instanceKey] = struct{}{} 107 | 108 | w.clients = make(map[*websocket.Conn]chan interface{}) 109 | 110 | return w, nil 111 | } 112 | 113 | // Run starts the main loop. 114 | func (w *HTTPWidget) Run(c chan<- []ygs.I3BarBlock) error { 115 | w.c = c 116 | 117 | if w.instance == nil { 118 | return nil 119 | } 120 | 121 | l, err := net.Listen(w.params.Network, w.params.Listen) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | w.instance.l = l 127 | 128 | err = w.instance.server.Serve(l) 129 | if errors.Is(err, http.ErrServerClosed) { 130 | return nil 131 | } 132 | 133 | return err 134 | } 135 | 136 | // Event processes the widget events. 137 | func (w *HTTPWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error { 138 | return w.broadcast(event) 139 | } 140 | 141 | func (w *HTTPWidget) Shutdown() error { 142 | if w.instance == nil || w.instance.l == nil { 143 | return nil 144 | } 145 | 146 | if err := w.instance.server.Shutdown(context.Background()); err != nil { 147 | return err 148 | } 149 | 150 | return nil 151 | } 152 | 153 | func (w *HTTPWidget) httpHandler(response http.ResponseWriter, request *http.Request) { 154 | if request.Method == "GET" { 155 | serv := websocket.Server{ 156 | Handshake: func(cfg *websocket.Config, r *http.Request) error { 157 | return nil 158 | }, 159 | Handler: w.wsHandler, 160 | } 161 | 162 | serv.ServeHTTP(response, request) 163 | 164 | return 165 | } 166 | 167 | if request.Method == "POST" { 168 | body, err := ioutil.ReadAll(request.Body) 169 | if err != nil { 170 | w.logger.Errorf("%s", err) 171 | } 172 | 173 | var messages []ygs.I3BarBlock 174 | if err := json.Unmarshal(body, &messages); err != nil { 175 | w.logger.Errorf("%s", err) 176 | response.WriteHeader(http.StatusBadRequest) 177 | fmt.Fprintf(response, "%s", err) 178 | } 179 | 180 | w.c <- messages 181 | 182 | return 183 | } 184 | 185 | response.WriteHeader(http.StatusBadRequest) 186 | 187 | _, err := response.Write([]byte("bad request method, allow GET for websocket and POST for HTTP update")) 188 | if err != nil { 189 | w.logger.Errorf("failed to write response: %s", err) 190 | } 191 | } 192 | 193 | func (w *HTTPWidget) wsHandler(ws *websocket.Conn) { 194 | defer ws.Close() 195 | 196 | ch := make(chan interface{}) 197 | 198 | w.cm.RLock() 199 | w.clients[ws] = ch 200 | w.cm.RUnlock() 201 | 202 | var blocks []ygs.I3BarBlock 203 | 204 | go func() { 205 | for { 206 | msg, ok := <-ch 207 | if !ok { 208 | return 209 | } 210 | 211 | if err := websocket.JSON.Send(ws, msg); err != nil { 212 | w.logger.Errorf("failed to send msg: %s", err) 213 | } 214 | } 215 | }() 216 | 217 | for { 218 | if err := websocket.JSON.Receive(ws, &blocks); err != nil { 219 | if errors.Is(err, io.EOF) { 220 | break 221 | } 222 | 223 | w.logger.Errorf("invalid message: %s", err) 224 | 225 | break 226 | } 227 | 228 | w.c <- blocks 229 | } 230 | 231 | w.cm.Lock() 232 | delete(w.clients, ws) 233 | w.cm.Unlock() 234 | 235 | close(ch) 236 | } 237 | 238 | func (w *HTTPWidget) broadcast(msg interface{}) error { 239 | w.cm.RLock() 240 | defer w.cm.RUnlock() 241 | 242 | for _, ch := range w.clients { 243 | ch <- msg 244 | } 245 | 246 | return nil 247 | } 248 | -------------------------------------------------------------------------------- /widgets/static.go: -------------------------------------------------------------------------------- 1 | package widgets 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "github.com/burik666/yagostatus/ygs" 8 | ) 9 | 10 | // StaticWidgetParams are widget parameters. 11 | type StaticWidgetParams struct { 12 | Blocks string 13 | } 14 | 15 | // StaticWidget implements a static widget. 16 | type StaticWidget struct { 17 | ygs.BlankWidget 18 | 19 | params StaticWidgetParams 20 | 21 | blocks []ygs.I3BarBlock 22 | } 23 | 24 | func init() { 25 | if err := ygs.RegisterWidget(ygs.WidgetSpec{ 26 | Name: "static", 27 | NewFunc: NewStaticWidget, 28 | DefaultParams: StaticWidgetParams{}, 29 | }); err != nil { 30 | panic(err) 31 | } 32 | } 33 | 34 | // NewStaticWidget returns a new StaticWidget. 35 | func NewStaticWidget(params interface{}, wlogger ygs.Logger) (ygs.Widget, error) { 36 | w := &StaticWidget{ 37 | params: params.(StaticWidgetParams), 38 | } 39 | 40 | if len(w.params.Blocks) == 0 { 41 | return nil, errors.New("missing 'blocks'") 42 | } 43 | 44 | if err := json.Unmarshal([]byte(w.params.Blocks), &w.blocks); err != nil { 45 | return nil, err 46 | } 47 | 48 | return w, nil 49 | } 50 | 51 | // Run returns configured blocks. 52 | func (w *StaticWidget) Run(c chan<- []ygs.I3BarBlock) error { 53 | c <- w.blocks 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /widgets/wrapper.go: -------------------------------------------------------------------------------- 1 | package widgets 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "syscall" 9 | 10 | "github.com/burik666/yagostatus/pkg/executor" 11 | "github.com/burik666/yagostatus/ygs" 12 | ) 13 | 14 | // WrapperWidgetParams are widget parameters. 15 | type WrapperWidgetParams struct { 16 | Command string 17 | WorkDir string 18 | Env []string 19 | } 20 | 21 | // WrapperWidget implements the wrapper of other status commands. 22 | type WrapperWidget struct { 23 | ygs.BlankWidget 24 | 25 | params WrapperWidgetParams 26 | 27 | logger ygs.Logger 28 | 29 | exc *executor.Executor 30 | stdin io.WriteCloser 31 | 32 | eventBracketWritten bool 33 | shutdown bool 34 | } 35 | 36 | func init() { 37 | if err := ygs.RegisterWidget(ygs.WidgetSpec{ 38 | Name: "wrapper", 39 | NewFunc: NewWrapperWidget, 40 | DefaultParams: WrapperWidgetParams{}, 41 | }); err != nil { 42 | panic(err) 43 | } 44 | } 45 | 46 | // NewWrapperWidget returns a new WrapperWidget. 47 | func NewWrapperWidget(params interface{}, wlogger ygs.Logger) (ygs.Widget, error) { 48 | w := &WrapperWidget{ 49 | params: params.(WrapperWidgetParams), 50 | logger: wlogger, 51 | } 52 | 53 | if len(w.params.Command) == 0 { 54 | return nil, errors.New("missing 'command'") 55 | } 56 | 57 | exc, err := executor.Exec(w.params.Command) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | exc.SetWD(w.params.WorkDir) 63 | 64 | exc.AddEnv(w.params.Env...) 65 | 66 | w.exc = exc 67 | 68 | return w, nil 69 | } 70 | 71 | // Run starts the main loop. 72 | func (w *WrapperWidget) Run(c chan<- []ygs.I3BarBlock) error { 73 | var err error 74 | 75 | w.stdin, err = w.exc.Stdin() 76 | if err != nil { 77 | return err 78 | } 79 | 80 | defer w.stdin.Close() 81 | 82 | err = w.exc.Run(w.logger, c, executor.OutputFormatJSON) 83 | if err == nil { 84 | if w.shutdown { 85 | return nil 86 | } 87 | 88 | if state := w.exc.ProcessState(); state != nil { 89 | return fmt.Errorf("process exited unexpectedly: %s", state.String()) 90 | } 91 | } 92 | 93 | return err 94 | } 95 | 96 | // Event processes the widget events. 97 | func (w *WrapperWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error { 98 | if w.stdin == nil { 99 | return nil 100 | } 101 | 102 | if header := w.exc.I3BarHeader(); header != nil && header.ClickEvents { 103 | if !w.eventBracketWritten { 104 | w.eventBracketWritten = true 105 | if _, err := w.stdin.Write([]byte("[")); err != nil { 106 | return err 107 | } 108 | } 109 | 110 | msg, err := json.Marshal(event) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | msg = append(msg, []byte(",\n")...) 116 | 117 | if _, err := w.stdin.Write(msg); err != nil { 118 | return err 119 | } 120 | } 121 | 122 | return nil 123 | } 124 | 125 | // Stop stops the widdget. 126 | func (w *WrapperWidget) Stop() error { 127 | if header := w.exc.I3BarHeader(); header != nil { 128 | if header.StopSignal != 0 { 129 | return w.exc.Signal(syscall.Signal(header.StopSignal)) 130 | } 131 | } 132 | 133 | return w.exc.Signal(syscall.SIGSTOP) 134 | } 135 | 136 | // Continue continues the widdget. 137 | func (w *WrapperWidget) Continue() error { 138 | if header := w.exc.I3BarHeader(); header != nil { 139 | if header.ContSignal != 0 { 140 | return w.exc.Signal(syscall.Signal(header.ContSignal)) 141 | } 142 | } 143 | 144 | return w.exc.Signal(syscall.SIGCONT) 145 | } 146 | 147 | // Shutdown shutdowns the widget. 148 | func (w *WrapperWidget) Shutdown() error { 149 | w.shutdown = true 150 | 151 | if w.exc != nil { 152 | if err := w.exc.Shutdown(); err != nil { 153 | return err 154 | } 155 | } 156 | 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /yagostatus.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "os" 12 | "os/exec" 13 | "reflect" 14 | "runtime/debug" 15 | "strconv" 16 | "strings" 17 | "sync" 18 | "sync/atomic" 19 | "time" 20 | 21 | "github.com/burik666/yagostatus/internal/config" 22 | "github.com/burik666/yagostatus/internal/registry" 23 | "github.com/burik666/yagostatus/pkg/executor" 24 | "github.com/burik666/yagostatus/ygs" 25 | 26 | _ "github.com/burik666/yagostatus/plugins" 27 | _ "github.com/burik666/yagostatus/widgets" 28 | 29 | "go.i3wm.org/i3/v4" 30 | ) 31 | 32 | type widgetContainer struct { 33 | instance ygs.Widget 34 | output []ygs.I3BarBlock 35 | config config.WidgetConfig 36 | ch chan []ygs.I3BarBlock 37 | logger ygs.Logger 38 | m sync.RWMutex 39 | } 40 | 41 | // YaGoStatus is the main struct. 42 | type YaGoStatus struct { 43 | widgets []widgetContainer 44 | 45 | upd chan int 46 | 47 | workspaces []i3.Workspace 48 | visibleWorkspaces []string 49 | 50 | cfg config.Config 51 | sway bool 52 | 53 | logger ygs.Logger 54 | } 55 | 56 | // NewYaGoStatus returns a new YaGoStatus instance. 57 | func NewYaGoStatus(cfg config.Config, sway bool, l ygs.Logger) *YaGoStatus { 58 | status := &YaGoStatus{ 59 | cfg: cfg, 60 | sway: sway, 61 | logger: l, 62 | } 63 | 64 | if sway { 65 | i3.SocketPathHook = func() (string, error) { 66 | out, err := exec.Command("sway", "--get-socketpath").CombinedOutput() 67 | if err != nil { 68 | return "", fmt.Errorf("getting sway socketpath: %w (output: %s)", err, out) 69 | } 70 | 71 | return string(out), nil 72 | } 73 | 74 | i3.IsRunningHook = func() bool { 75 | out, err := exec.Command("pgrep", "-c", "sway\\$").CombinedOutput() 76 | if err != nil { 77 | l.Errorf("sway running: %w (output: %s)", err, out) 78 | } 79 | 80 | return bytes.Equal(out, []byte("1")) 81 | } 82 | } 83 | 84 | for wi := range cfg.Widgets { 85 | status.addWidget(cfg.Widgets[wi]) 86 | } 87 | 88 | return status 89 | } 90 | 91 | func (status *YaGoStatus) errorWidget(text string) { 92 | status.addWidget(config.ErrorWidget(text)) 93 | } 94 | 95 | func (status *YaGoStatus) addWidget(wcfg config.WidgetConfig) { 96 | wlogger := status.logger.WithPrefix(fmt.Sprintf("[%s#%d]", wcfg.File, wcfg.Index+1)) 97 | 98 | (func() { 99 | defer (func() { 100 | if r := recover(); r != nil { 101 | wlogger.Errorf("NewWidget panic: %s", r) 102 | debug.PrintStack() 103 | status.errorWidget("widget panic") 104 | } 105 | })() 106 | 107 | widget, err := registry.NewWidget(wcfg, wlogger) 108 | if err != nil { 109 | wlogger.Errorf("Failed to create widget: %s", err) 110 | status.errorWidget(err.Error()) 111 | 112 | return 113 | } 114 | 115 | status.widgets = append(status.widgets, widgetContainer{ 116 | instance: widget, 117 | config: wcfg, 118 | logger: wlogger, 119 | }) 120 | })() 121 | } 122 | 123 | func (status *YaGoStatus) processWidgetEvents(wi int, block ygs.I3BarBlock, event ygs.I3BarClickEvent) error { 124 | defer (func() { 125 | if r := recover(); r != nil { 126 | status.widgets[wi].logger.Errorf("widget event panic: %s", r) 127 | debug.PrintStack() 128 | 129 | status.widgets[wi].ch <- []ygs.I3BarBlock{{ 130 | FullText: "widget panic", 131 | Color: "#ff0000", 132 | }} 133 | } 134 | 135 | status.widgets[wi].m.Lock() 136 | 137 | blocks := make([]ygs.I3BarBlock, len(status.widgets[wi].output)) 138 | copy(blocks, status.widgets[wi].output) 139 | 140 | status.widgets[wi].m.Unlock() 141 | 142 | if err := status.widgets[wi].instance.Event(event, blocks); err != nil { 143 | status.widgets[wi].logger.Errorf("Failed to process widget event: %s", err) 144 | } 145 | })() 146 | 147 | for _, widgetEvent := range status.widgets[wi].config.Events { 148 | if (widgetEvent.Button == 0 || widgetEvent.Button == event.Button) && 149 | (widgetEvent.Name == "" || widgetEvent.Name == event.Name) && 150 | (widgetEvent.Instance == "" || widgetEvent.Instance == event.Instance) && 151 | checkModifiers(widgetEvent.Modifiers, event.Modifiers) { 152 | exc, err := executor.Exec("sh", "-c", widgetEvent.Command) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | exc.SetWD(widgetEvent.WorkDir) 158 | 159 | exc.AddEnv( 160 | fmt.Sprintf("I3_%s=%s", "NAME", event.Name), 161 | fmt.Sprintf("I3_%s=%s", "INSTANCE", event.Instance), 162 | fmt.Sprintf("I3_%s=%d", "BUTTON", event.Button), 163 | fmt.Sprintf("I3_%s=%d", "X", event.X), 164 | fmt.Sprintf("I3_%s=%d", "Y", event.Y), 165 | fmt.Sprintf("I3_%s=%d", "RELATIVE_X", event.RelativeX), 166 | fmt.Sprintf("I3_%s=%d", "RELATIVE_Y", event.RelativeY), 167 | fmt.Sprintf("I3_%s=%d", "OUTPUT_X", event.OutputX), 168 | fmt.Sprintf("I3_%s=%d", "OUTPUT_Y", event.OutputY), 169 | fmt.Sprintf("I3_%s=%d", "WIDTH", event.Width), 170 | fmt.Sprintf("I3_%s=%d", "HEIGHT", event.Height), 171 | fmt.Sprintf("I3_%s=%s", "MODIFIERS", strings.Join(event.Modifiers, ",")), 172 | ) 173 | 174 | exc.AddEnv(widgetEvent.Env...) 175 | 176 | exc.AddEnv(block.Env("")...) 177 | 178 | stdin, err := exc.Stdin() 179 | if err != nil { 180 | return err 181 | } 182 | 183 | eventJSON, err := json.Marshal(event) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | eventJSON = append(eventJSON, []byte("\n")...) 189 | 190 | _, err = stdin.Write(eventJSON) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | err = stdin.Close() 196 | if err != nil { 197 | return err 198 | } 199 | 200 | err = exc.Run( 201 | status.widgets[wi].logger, 202 | status.widgets[wi].ch, 203 | executor.OutputFormat(widgetEvent.OutputFormat), 204 | ) 205 | if err != nil { 206 | return err 207 | } 208 | 209 | if state := exc.ProcessState(); state != nil && state.ExitCode() != 0 { 210 | return fmt.Errorf("process exited unexpectedly: %s", state.String()) 211 | } 212 | } 213 | } 214 | 215 | return nil 216 | } 217 | 218 | func (status *YaGoStatus) addWidgetOutput(wi int, blocks []ygs.I3BarBlock) { 219 | output := make([]ygs.I3BarBlock, len(blocks)) 220 | tplc := len(status.widgets[wi].config.Templates) 221 | 222 | for blockIndex := range blocks { 223 | block := blocks[blockIndex] 224 | 225 | if tplc == 1 { 226 | block.Apply(status.widgets[wi].config.Templates[0]) 227 | } else if blockIndex < tplc { 228 | block.Apply(status.widgets[wi].config.Templates[blockIndex]) 229 | } 230 | 231 | block.Name = fmt.Sprintf("yagostatus-%d-%s", wi, block.Name) 232 | block.Instance = fmt.Sprintf("yagostatus-%d-%d-%s", wi, blockIndex, block.Instance) 233 | 234 | output[blockIndex] = block 235 | } 236 | 237 | status.widgets[wi].m.Lock() 238 | status.widgets[wi].output = output 239 | status.widgets[wi].m.Unlock() 240 | 241 | status.upd <- wi 242 | } 243 | 244 | func (status *YaGoStatus) eventReader() error { 245 | reader := bufio.NewReader(os.Stdin) 246 | 247 | for { 248 | line, err := reader.ReadString('\n') 249 | if err != nil { 250 | if errors.Is(err, io.EOF) { 251 | return err 252 | } 253 | 254 | break 255 | } 256 | 257 | line = strings.Trim(line, "[], \n") 258 | if line == "" { 259 | continue 260 | } 261 | 262 | var event ygs.I3BarClickEvent 263 | if err := json.Unmarshal([]byte(line), &event); err != nil { 264 | status.logger.Errorf("%s (%s)", err, line) 265 | 266 | continue 267 | } 268 | 269 | go func(event ygs.I3BarClickEvent) { 270 | wi, name, err := splitName(event.Name) 271 | if err != nil { 272 | status.logger.Errorf("failed to parse event name '%s': %s", event.Name, err) 273 | 274 | return 275 | } 276 | 277 | _, oi, instance, err := splitInstance(event.Instance) 278 | if err != nil { 279 | status.logger.Errorf("failed to parse event instance '%s': %s", event.Name, err) 280 | 281 | return 282 | } 283 | 284 | e := event 285 | e.Name = name 286 | e.Instance = instance 287 | 288 | status.widgets[wi].m.RLock() 289 | if len(status.widgets[wi].output) < oi { 290 | status.widgets[wi].m.RUnlock() 291 | 292 | return 293 | } 294 | 295 | block := status.widgets[wi].output[oi] 296 | status.widgets[wi].m.RUnlock() 297 | 298 | if (event.Name != "" && event.Name == block.Name) && (event.Instance != "" && event.Instance == block.Instance) { 299 | block.Name = e.Name 300 | block.Instance = e.Instance 301 | 302 | if err := status.processWidgetEvents(wi, block, e); err != nil { 303 | status.widgets[wi].logger.Errorf("event error: %s", err) 304 | 305 | status.widgets[wi].ch <- []ygs.I3BarBlock{{ 306 | FullText: fmt.Sprintf("event error: %s", err.Error()), 307 | Color: "#ff0000", 308 | Name: event.Name, 309 | Instance: event.Instance, 310 | }} 311 | } 312 | } 313 | }(event) 314 | } 315 | 316 | return nil 317 | } 318 | 319 | // Run starts the main loop. 320 | func (status *YaGoStatus) Run() error { 321 | status.upd = make(chan int) 322 | 323 | go (func() { 324 | status.updateWorkspaces() 325 | 326 | recv := i3.Subscribe(i3.WorkspaceEventType) 327 | for recv.Next() { 328 | e := recv.Event().(*i3.WorkspaceEvent) 329 | if e.Change == "empty" { 330 | continue 331 | } 332 | 333 | status.updateWorkspaces() 334 | status.upd <- -1 335 | } 336 | })() 337 | 338 | widgetChans := make([]reflect.SelectCase, len(status.widgets)) 339 | 340 | var wcounter int32 = int32(len(status.widgets)) 341 | 342 | for wi := range status.widgets { 343 | status.widgets[wi].ch = make(chan []ygs.I3BarBlock) 344 | 345 | widgetChans[wi] = reflect.SelectCase{ 346 | Dir: reflect.SelectRecv, 347 | Chan: reflect.ValueOf(status.widgets[wi].ch), 348 | } 349 | 350 | go func(wi int) { 351 | defer (func() { 352 | atomic.AddInt32(&wcounter, -1) 353 | if r := recover(); r != nil { 354 | status.widgets[wi].logger.Errorf("widget panic: %s", r) 355 | debug.PrintStack() 356 | status.widgets[wi].ch <- []ygs.I3BarBlock{{ 357 | FullText: "widget panic", 358 | Color: "#ff0000", 359 | }} 360 | } 361 | })() 362 | 363 | if err := status.widgets[wi].instance.Run(status.widgets[wi].ch); err != nil { 364 | status.widgets[wi].logger.Errorf("Widget done: %s", err) 365 | status.widgets[wi].ch <- []ygs.I3BarBlock{{ 366 | FullText: err.Error(), 367 | Color: "#ff0000", 368 | }} 369 | } 370 | }(wi) 371 | } 372 | 373 | go func() { 374 | for wcounter > 0 { 375 | wi, out, _ := reflect.Select(widgetChans) 376 | status.addWidgetOutput(wi, out.Interface().([]ygs.I3BarBlock)) 377 | } 378 | }() 379 | 380 | encoder := json.NewEncoder(os.Stdout) 381 | encoder.SetEscapeHTML(false) 382 | 383 | if err := encoder.Encode(ygs.I3BarHeader{ 384 | Version: 1, 385 | ClickEvents: true, 386 | StopSignal: int(status.cfg.Signals.StopSignal), 387 | ContSignal: int(status.cfg.Signals.ContSignal), 388 | }); err != nil { 389 | status.logger.Errorf("Failed to encode I3BarHeader: %s", err) 390 | } 391 | 392 | fmt.Print("\n[\n[]") 393 | 394 | go func() { 395 | for range status.upd { 396 | var result []ygs.I3BarBlock 397 | 398 | for wi := range status.widgets { 399 | if checkWorkspaceConditions(status.widgets[wi].config.Workspaces, status.visibleWorkspaces) { 400 | status.widgets[wi].m.RLock() 401 | result = append(result, status.widgets[wi].output...) 402 | status.widgets[wi].m.RUnlock() 403 | } 404 | } 405 | 406 | fmt.Print(",") 407 | 408 | if result == nil { 409 | fmt.Print("[]") 410 | 411 | continue 412 | } 413 | 414 | if err := encoder.Encode(result); err != nil { 415 | status.logger.Errorf("Failed to encode result: %s", err) 416 | 417 | break 418 | } 419 | } 420 | }() 421 | 422 | return status.eventReader() 423 | } 424 | 425 | // Shutdown shutdowns widgets and main loop. 426 | func (status *YaGoStatus) Shutdown() { 427 | var wg sync.WaitGroup 428 | 429 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) 430 | defer cancel() 431 | 432 | for wi := range status.widgets { 433 | wg.Add(1) 434 | 435 | done := make(chan struct{}) 436 | defer close(done) 437 | 438 | go func(wi int) { 439 | defer wg.Done() 440 | 441 | defer (func() { 442 | if r := recover(); r != nil { 443 | status.widgets[wi].logger.Errorf("widget panic: %s", r) 444 | debug.PrintStack() 445 | } 446 | })() 447 | 448 | go func() { 449 | if err := status.widgets[wi].instance.Shutdown(); err != nil { 450 | status.widgets[wi].logger.Errorf("Failed to shutdown widget: %s", err) 451 | } 452 | 453 | done <- struct{}{} 454 | }() 455 | 456 | select { 457 | case <-ctx.Done(): 458 | status.widgets[wi].logger.Errorf("Failed to shutdown widget: %s", ctx.Err()) 459 | case <-done: 460 | } 461 | }(wi) 462 | } 463 | 464 | wg.Wait() 465 | } 466 | 467 | // Stop stops widgets and main loop. 468 | func (status *YaGoStatus) Stop() { 469 | for wi := range status.widgets { 470 | go func(wi int) { 471 | defer (func() { 472 | if r := recover(); r != nil { 473 | status.widgets[wi].logger.Errorf("widget panic: %s", r) 474 | debug.PrintStack() 475 | } 476 | })() 477 | 478 | if err := status.widgets[wi].instance.Stop(); err != nil { 479 | status.widgets[wi].logger.Errorf("Failed to stop widget: %s", err) 480 | } 481 | }(wi) 482 | } 483 | } 484 | 485 | // Continue continues widgets and main loop. 486 | func (status *YaGoStatus) Continue() { 487 | for wi := range status.widgets { 488 | go func(wi int) { 489 | defer (func() { 490 | if r := recover(); r != nil { 491 | status.widgets[wi].logger.Errorf("widget panic: %s", r) 492 | debug.PrintStack() 493 | } 494 | })() 495 | 496 | if err := status.widgets[wi].instance.Continue(); err != nil { 497 | status.widgets[wi].logger.Errorf("Failed to continue widget: %s", err) 498 | } 499 | }(wi) 500 | } 501 | } 502 | 503 | func (status *YaGoStatus) updateWorkspaces() { 504 | var err error 505 | 506 | status.workspaces, err = i3.GetWorkspaces() 507 | 508 | if err != nil { 509 | status.logger.Errorf("Failed to get workspaces: %s", err) 510 | } 511 | 512 | var vw []string 513 | 514 | for i := range status.workspaces { 515 | if status.workspaces[i].Visible { 516 | vw = append(vw, status.workspaces[i].Name) 517 | } 518 | } 519 | 520 | status.visibleWorkspaces = vw 521 | } 522 | 523 | func checkModifiers(conditions []string, values []string) bool { 524 | for _, c := range conditions { 525 | isNegative := c[0] == '!' 526 | c = strings.TrimLeft(c, "!") 527 | 528 | found := false 529 | 530 | for _, v := range values { 531 | if c == v { 532 | found = true 533 | 534 | break 535 | } 536 | } 537 | 538 | if found && isNegative { 539 | return false 540 | } 541 | 542 | if !found && !isNegative { 543 | return false 544 | } 545 | } 546 | 547 | return true 548 | } 549 | 550 | func checkWorkspaceConditions(conditions []string, values []string) bool { 551 | if len(conditions) == 0 { 552 | return true 553 | } 554 | 555 | pass := 0 556 | 557 | for _, c := range conditions { 558 | isNegative := c[0] == '!' 559 | c = strings.TrimLeft(c, "!") 560 | 561 | found := false 562 | 563 | for _, v := range values { 564 | if c == v { 565 | found = true 566 | 567 | break 568 | } 569 | } 570 | 571 | if found && !isNegative { 572 | return true 573 | } 574 | 575 | if !found && isNegative { 576 | pass++ 577 | } 578 | } 579 | 580 | return len(conditions) == pass 581 | } 582 | 583 | func splitName(name string) (int, string, error) { 584 | parts := strings.SplitN(name, "-", 3) 585 | 586 | wi, err := strconv.ParseInt(parts[1], 10, 64) 587 | if err != nil { 588 | return 0, "", err 589 | } 590 | 591 | return int(wi), parts[2], nil 592 | } 593 | 594 | func splitInstance(name string) (int, int, string, error) { 595 | parts := strings.SplitN(name, "-", 4) 596 | 597 | wi, err := strconv.ParseInt(parts[1], 10, 64) 598 | if err != nil { 599 | return 0, 0, "", err 600 | } 601 | 602 | oi, err := strconv.ParseInt(parts[2], 10, 64) 603 | if err != nil { 604 | return 0, 0, "", err 605 | } 606 | 607 | return int(wi), int(oi), parts[3], nil 608 | } 609 | -------------------------------------------------------------------------------- /yagostatus.yml: -------------------------------------------------------------------------------- 1 | widgets: 2 | - widget: static 3 | blocks: > 4 | [ 5 | { 6 | "full_text": "YaGoStatus", 7 | "color": "#2e9ef4" 8 | } 9 | ] 10 | events: 11 | - button: 1 12 | command: xdg-open https://github.com/burik666/yagostatus/ 13 | 14 | - widget: wrapper 15 | command: /usr/bin/i3status 16 | 17 | - widget: clock 18 | format: Jan _2 Mon 15:04:05 # https://golang.org/pkg/time/#Time.Format 19 | templates: > 20 | [{ 21 | "color": "#ffffff", 22 | "separator": true, 23 | "separator_block_width": 21 24 | }] 25 | -------------------------------------------------------------------------------- /ygs/blankWidget.go: -------------------------------------------------------------------------------- 1 | package ygs 2 | 3 | // BlankWidgetParams are widget parameters. 4 | type BlankWidgetParams struct{} 5 | 6 | // BlankWidget is a widgets template. 7 | type BlankWidget struct{} 8 | 9 | // NewBlankWidget returns a new BlankWidget. 10 | func NewBlankWidget(params interface{}, wlogger Logger) (Widget, error) { 11 | return &BlankWidget{}, nil 12 | } 13 | 14 | // Run starts the main loop. 15 | func (w *BlankWidget) Run(c chan<- []I3BarBlock) error { 16 | return nil 17 | } 18 | 19 | // Event processes the widget events. 20 | func (w *BlankWidget) Event(event I3BarClickEvent, blocks []I3BarBlock) error { 21 | return nil 22 | } 23 | 24 | // Stop stops the widdget. 25 | func (w *BlankWidget) Stop() error { 26 | return nil 27 | } 28 | 29 | // Continue continues the widdget. 30 | func (w *BlankWidget) Continue() error { 31 | return nil 32 | } 33 | 34 | // Shutdown shutdowns the widget. 35 | func (w *BlankWidget) Shutdown() error { 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /ygs/looger.go: -------------------------------------------------------------------------------- 1 | package ygs 2 | 3 | // Logger represents yagostatus logger. 4 | type Logger interface { 5 | Infof(format string, v ...interface{}) 6 | Errorf(format string, v ...interface{}) 7 | Debugf(format string, v ...interface{}) 8 | WithPrefix(prefix string) Logger 9 | } 10 | -------------------------------------------------------------------------------- /ygs/parser.go: -------------------------------------------------------------------------------- 1 | package ygs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func (b *I3BarBlock) FromJSON(data []byte, strict bool) error { 12 | type dataWrapped I3BarBlock 13 | 14 | var block dataWrapped 15 | 16 | // copy 17 | tmp, err := json.Marshal(b) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | if err := json.Unmarshal(tmp, &block); err != nil { 23 | return err 24 | } 25 | 26 | if block.Custom == nil { 27 | block.Custom = make(map[string]Vary) 28 | } 29 | 30 | if err := parseBlock(&block, block.Custom, data, strict); err != nil { 31 | return err 32 | } 33 | 34 | *b = I3BarBlock(block) 35 | 36 | return nil 37 | } 38 | 39 | func (b *I3BarBlock) ToVaryMap() map[string]Vary { 40 | tmp, _ := json.Marshal(b) 41 | 42 | varyMap := make(map[string]Vary) 43 | 44 | _ = json.Unmarshal(tmp, &varyMap) 45 | 46 | return varyMap 47 | } 48 | 49 | func parseBlock(block interface{}, custom map[string]Vary, data []byte, strict bool) error { 50 | var jfields map[string]Vary 51 | 52 | if err := json.Unmarshal(data, &jfields); err != nil { 53 | return err 54 | } 55 | 56 | val := reflect.ValueOf(block).Elem() 57 | fieldsByJSONTag := make(map[string]reflect.Value) 58 | 59 | for i := 0; i < val.NumField(); i++ { 60 | typeField := val.Type().Field(i) 61 | if tag, ok := typeField.Tag.Lookup("json"); ok { 62 | tagName := strings.Split(tag, ",")[0] 63 | if tagName == "-" || tagName == "" { 64 | continue 65 | } 66 | 67 | fieldsByJSONTag[tagName] = val.Field(i) 68 | } 69 | } 70 | 71 | for k, v := range jfields { 72 | f, ok := fieldsByJSONTag[k] 73 | if !ok { 74 | if len(k) == 0 { 75 | continue 76 | } 77 | 78 | if strict && k[0] != byte('_') { 79 | return fmt.Errorf("uknown field: %s", k) 80 | } 81 | 82 | custom[k] = v 83 | 84 | continue 85 | } 86 | 87 | if err := convertFieldValue(f, k, v, strict); err != nil { 88 | return err 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func convertFieldValue(f reflect.Value, k string, v Vary, strict bool) error { 96 | var val reflect.Value 97 | 98 | if f.Type().Kind() == reflect.Ptr { 99 | val = reflect.New(f.Type().Elem()) 100 | } else { 101 | val = reflect.New(f.Type()).Elem() 102 | } 103 | 104 | sv := string(v) 105 | 106 | switch reflect.Indirect(val).Kind() { 107 | case reflect.String: 108 | s := sv 109 | 110 | err := json.Unmarshal([]byte(s), &s) 111 | if strict && err != nil { 112 | return fmt.Errorf("invalid value for %s (string): %s", k, sv) 113 | } 114 | 115 | reflect.Indirect(val).SetString(s) 116 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 117 | s := sv 118 | 119 | if !strict { 120 | s = strings.Trim(sv, "\"") 121 | } 122 | 123 | if n, err := strconv.ParseInt(s, 10, 64); err == nil { 124 | reflect.Indirect(val).SetInt(n) 125 | } else { 126 | return fmt.Errorf("invalid value for %s (int): %s", k, sv) 127 | } 128 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 129 | s := sv 130 | 131 | if !strict { 132 | s = strings.Trim(sv, "\"") 133 | } 134 | 135 | if n, err := strconv.ParseUint(s, 10, 64); err == nil { 136 | reflect.Indirect(val).SetUint(n) 137 | } else { 138 | return fmt.Errorf("invalid value for %s (uint): %s", k, sv) 139 | } 140 | 141 | case reflect.Bool: 142 | if strict { 143 | switch sv { 144 | case "true": 145 | reflect.Indirect(val).SetBool(true) 146 | case "false": 147 | reflect.Indirect(val).SetBool(false) 148 | default: 149 | return fmt.Errorf("invalid value for %s: %s", k, sv) 150 | } 151 | } else { 152 | s := strings.Trim(strings.ToLower(sv), "\"") 153 | if s == "false" || s == "0" || s == "f" { 154 | reflect.Indirect(val).SetBool(false) 155 | } else { 156 | reflect.Indirect(val).SetBool(true) 157 | } 158 | } 159 | case reflect.Slice: // Vary 160 | if strings.HasPrefix(sv, "\"") { 161 | reflect.Indirect(val).SetBytes(v) 162 | 163 | break 164 | } 165 | 166 | s := strings.Trim(sv, "\"") 167 | if _, err := strconv.ParseUint(s, 10, 64); err != nil { 168 | return fmt.Errorf("invalid value for %s: %s", k, sv) 169 | } 170 | 171 | reflect.Indirect(val).SetBytes([]byte(s)) 172 | default: 173 | panic(fmt.Sprintf("unsuported type %s: %s", k, reflect.Indirect(val).Kind())) 174 | } 175 | 176 | f.Set(val) 177 | 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /ygs/protocol.go: -------------------------------------------------------------------------------- 1 | package ygs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | // I3BarHeader represents the header of an i3bar message. 10 | type I3BarHeader struct { 11 | Version uint8 `json:"version"` 12 | StopSignal int `json:"stop_signal,omitempty"` 13 | ContSignal int `json:"cont_signal,omitempty"` 14 | ClickEvents bool `json:"click_events,omitempty"` 15 | } 16 | 17 | // I3BarBlock represents a block of i3bar message. 18 | type I3BarBlock struct { 19 | FullText string `json:"full_text,omitempty"` 20 | ShortText string `json:"short_text,omitempty"` 21 | Color string `json:"color,omitempty"` 22 | BorderColor string `json:"border,omitempty"` 23 | BorderTop *uint16 `json:"border_top,omitempty"` 24 | BorderBottom *uint16 `json:"border_bottom,omitempty"` 25 | BorderLeft *uint16 `json:"border_left,omitempty"` 26 | BorderRight *uint16 `json:"border_right,omitempty"` 27 | BackgroundColor string `json:"background,omitempty"` 28 | Markup string `json:"markup,omitempty"` 29 | MinWidth Vary `json:"min_width,omitempty"` 30 | Align string `json:"align,omitempty"` 31 | Name string `json:"name,omitempty"` 32 | Instance string `json:"instance,omitempty"` 33 | Urgent bool `json:"urgent,omitempty"` 34 | Separator *bool `json:"separator,omitempty"` 35 | SeparatorBlockWidth uint16 `json:"separator_block_width,omitempty"` 36 | Custom map[string]Vary `json:"-"` 37 | } 38 | 39 | // I3BarClickEvent represents a user click event message. 40 | type I3BarClickEvent struct { 41 | Name string `json:"name,omitempty"` 42 | Instance string `json:"instance,omitempty"` 43 | Button uint8 `json:"button"` 44 | X uint16 `json:"x"` 45 | Y uint16 `json:"y"` 46 | RelativeX uint16 `json:"relative_x"` 47 | RelativeY uint16 `json:"relative_y"` 48 | OutputX uint16 `json:"output_x"` 49 | OutputY uint16 `json:"output_y"` 50 | Width uint16 `json:"width"` 51 | Height uint16 `json:"height"` 52 | Modifiers []string `json:"modifiers"` 53 | } 54 | 55 | // UnmarshalJSON unmarshals json with custom keys (with _ prefix). 56 | func (b *I3BarBlock) UnmarshalJSON(data []byte) error { 57 | return b.FromJSON(data, true) 58 | } 59 | 60 | // MarshalJSON marshals json with custom keys (with _ prefix). 61 | func (b I3BarBlock) MarshalJSON() ([]byte, error) { 62 | type dataWrapped I3BarBlock 63 | 64 | wd := dataWrapped(b) 65 | 66 | if len(wd.Custom) == 0 { 67 | buf := &bytes.Buffer{} 68 | encoder := json.NewEncoder(buf) 69 | encoder.SetEscapeHTML(false) 70 | err := encoder.Encode(wd) 71 | 72 | return buf.Bytes(), err 73 | } 74 | 75 | var resmap map[string]interface{} 76 | 77 | var tmp []byte 78 | 79 | tmp, _ = json.Marshal(wd) 80 | if err := json.Unmarshal(tmp, &resmap); err != nil { 81 | return nil, err 82 | } 83 | 84 | tmp, _ = json.Marshal(wd.Custom) 85 | if err := json.Unmarshal(tmp, &resmap); err != nil { 86 | return nil, err 87 | } 88 | 89 | buf := &bytes.Buffer{} 90 | encoder := json.NewEncoder(buf) 91 | encoder.SetEscapeHTML(false) 92 | err := encoder.Encode(resmap) 93 | 94 | return buf.Bytes(), err 95 | } 96 | 97 | func (b *I3BarBlock) Apply(tpl I3BarBlock) { 98 | jb, _ := json.Marshal(b) 99 | *b = tpl 100 | 101 | _ = json.Unmarshal(jb, b) 102 | } 103 | 104 | func (b I3BarBlock) Env(suffix string) []string { 105 | env := make([]string, 0) 106 | for k, v := range b.Custom { 107 | env = append(env, fmt.Sprintf("I3_%s%s=%s", k, suffix, v)) 108 | } 109 | 110 | ob, _ := json.Marshal(b) 111 | 112 | var rawOutput map[string]Vary 113 | _ = json.Unmarshal(ob, &rawOutput) 114 | 115 | for k, v := range rawOutput { 116 | env = append(env, fmt.Sprintf("I3_%s%s=%s", k, suffix, v.String())) 117 | } 118 | 119 | return env 120 | } 121 | -------------------------------------------------------------------------------- /ygs/registry.go: -------------------------------------------------------------------------------- 1 | package ygs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | rs "github.com/burik666/yagostatus/internal/registry/store" 9 | ) 10 | 11 | // WidgetSpec describes constructor for widgets. 12 | type WidgetSpec struct { 13 | Name string 14 | DefaultParams interface{} 15 | NewFunc NewWidgetFunc 16 | } 17 | 18 | // WidgetSpec describes plugins initialization. 19 | type PluginSpec struct { 20 | Name string 21 | DefaultParams interface{} 22 | InitFunc func(params interface{}, l Logger) error 23 | ShutdownFunc func() error 24 | } 25 | 26 | // NewWidgetFunc function to create a new instance of a widget. 27 | type NewWidgetFunc = func(params interface{}, l Logger) (Widget, error) 28 | 29 | // RegisterWidget registers widget. 30 | func RegisterWidget(rw WidgetSpec) error { 31 | if rw.DefaultParams != nil { 32 | def := reflect.ValueOf(rw.DefaultParams) 33 | if def.Kind() != reflect.Struct { 34 | return fmt.Errorf("defaultParams should be a struct") 35 | } 36 | } 37 | 38 | if _, loaded := rs.LoadOrStore("widget_"+rw.Name, rw); loaded { 39 | return fmt.Errorf("widget '%s' already registered", rw.Name) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // UnregisterWidget unregisters widget. 46 | func UnregisterWidget(name string) bool { 47 | _, ok := rs.LoadAndDelete("widget_" + name) 48 | 49 | return ok 50 | } 51 | 52 | // RegisteredWidgets returns list of registered plugins. 53 | func RegisteredWidgets() []WidgetSpec { 54 | var widgets []WidgetSpec 55 | 56 | rs.Range(func(k, v interface{}) bool { 57 | if strings.HasPrefix(k.(string), "widget_") { 58 | widgets = append(widgets, v.(WidgetSpec)) 59 | } 60 | 61 | return true 62 | }) 63 | 64 | return widgets 65 | } 66 | 67 | // RegisterPlugin registers plugin. 68 | func RegisterPlugin(rw PluginSpec) error { 69 | if rw.DefaultParams != nil { 70 | def := reflect.ValueOf(rw.DefaultParams) 71 | if def.Kind() != reflect.Struct { 72 | return fmt.Errorf("defaultParams should be a struct") 73 | } 74 | } 75 | 76 | if _, loaded := rs.LoadOrStore("plugin_"+rw.Name, rw); loaded { 77 | return fmt.Errorf("plugin '%s' already registered", rw.Name) 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // UnregisterPlugin unregisters widget. 84 | func UnregisterPlugin(name string) bool { 85 | _, ok := rs.LoadAndDelete("plugin_" + name) 86 | 87 | return ok 88 | } 89 | 90 | // RegisteredPlugins returns list of registered plugins. 91 | func RegisteredPlugins() []PluginSpec { 92 | var plugins []PluginSpec 93 | 94 | rs.Range(func(k, v interface{}) bool { 95 | if strings.HasPrefix(k.(string), "plugin_") { 96 | plugins = append(plugins, v.(PluginSpec)) 97 | } 98 | 99 | return true 100 | }) 101 | 102 | return plugins 103 | } 104 | -------------------------------------------------------------------------------- /ygs/vary.go: -------------------------------------------------------------------------------- 1 | package ygs 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | ) 7 | 8 | type Vary []byte 9 | 10 | func (v Vary) MarshalJSON() ([]byte, error) { 11 | if v == nil { 12 | return []byte("null"), nil 13 | } 14 | 15 | return v, nil 16 | } 17 | 18 | func (v *Vary) UnmarshalJSON(data []byte) error { 19 | if v == nil { 20 | return errors.New("ygs.Vary: UnmarshalJSON on nil pointer") 21 | } 22 | 23 | *v = append((*v)[0:0], data...) 24 | 25 | return nil 26 | } 27 | 28 | func (v Vary) String() string { 29 | s, err := strconv.Unquote(string(v)) 30 | if err != nil { 31 | return string(v) 32 | } 33 | 34 | return s 35 | } 36 | -------------------------------------------------------------------------------- /ygs/widget.go: -------------------------------------------------------------------------------- 1 | // Package ygs contains the YaGoStatus structures. 2 | package ygs 3 | 4 | // Widget represents a widget struct. 5 | type Widget interface { 6 | Run(chan<- []I3BarBlock) error 7 | Event(I3BarClickEvent, []I3BarBlock) error 8 | Stop() error 9 | Continue() error 10 | Shutdown() error 11 | } 12 | --------------------------------------------------------------------------------