├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── .goreleaser.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE_FONT ├── Makefile ├── README.md ├── app.go ├── assets ├── .gitignore ├── GlacialIndifference-Regular.ttf ├── assets.go ├── screenshot_01.png └── screenshot_02.png ├── config.go ├── go.mod ├── go.sum ├── input.go ├── logger.go ├── login_events.sh ├── main.go └── render.go /.gitignore: -------------------------------------------------------------------------------- 1 | go-home 2 | dist 3 | 4 | dist/ 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | modules-download-mode: readonly 3 | 4 | issues: 5 | exclude-rules: 6 | - linters: 7 | - errcheck 8 | text: "Error return value of `txt.WriteString` is not checked" 9 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | - go mod tidy 6 | builds: 7 | goos: 8 | - linux 9 | 10 | archives: 11 | - format: tar.gz 12 | # this name template makes the OS and Arch compatible with the results of uname. 13 | name_template: >- 14 | {{ .ProjectName }}_ 15 | {{- title .Os }}_ 16 | {{- if eq .Arch "amd64" }}x86_64 17 | {{- else if eq .Arch "386" }}i386 18 | {{- else }}{{ .Arch }}{{ end }} 19 | {{- if .Arm }}v{{ .Arm }}{{ end }} 20 | # use zip for windows archives 21 | format_overrides: 22 | - goos: windows 23 | format: zip 24 | checksum: 25 | name_template: 'checksums.txt' 26 | snapshot: 27 | name_template: "{{ incpatch .Version }}-next" 28 | changelog: 29 | sort: asc 30 | filters: 31 | exclude: 32 | - '^docs:' 33 | - '^test:' 34 | 35 | # The lines beneath this are called `modelines`. See `:help modeline` 36 | # Feel free to remove those if you don't want/use them. 37 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 38 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 39 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | 5 | builds: 6 | - 7 | env: 8 | - CGO_ENABLED=1 9 | goos: 10 | - linux 11 | goarch: 12 | - amd64 13 | 14 | archives: 15 | - replacements: 16 | darwin: Darwin 17 | linux: Linux 18 | windows: Windows 19 | 386: i386 20 | amd64: x86_64 21 | 22 | checksum: 23 | name_template: 'checksums.txt' 24 | 25 | snapshot: 26 | name_template: "{{ .Tag }}-next" 27 | 28 | changelog: 29 | sort: asc 30 | filters: 31 | exclude: 32 | - '^docs:' 33 | - '^test:' 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | - Nothing so far 10 | 11 | ## [v1.1.0] - 2019-06-16 12 | - Allow moving the window with Shift+ArrowKey 13 | 14 | ## [v1.0.1] - 2019-06-02 15 | - Fix panic when running in debug mode without Font file 16 | 17 | ## [v1.0.0] - 2019-06-02 18 | - Initial release. 19 | 20 | [Unreleased]: https://github.com/fgrosse/go-home/compare/v1.1.0...HEAD 21 | [v1.1.0]: https://github.com/fgrosse/go-home/releases/tag/v1.1.0 22 | [v1.0.0]: https://github.com/fgrosse/go-home/releases/tag/v1.0.0 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish 4 | to make via an [issue on Github][issues] *before* making a change. 5 | 6 | Please note we have a code of conduct. Please follow it in all your interactions 7 | with the project. 8 | 9 | ## Pull Request Process 10 | 11 | 0. Everything should start with an issue: ["Talk, then code"][talk-code] 12 | 1. Implement your feature and make sure it compiles on Linux AMD64 13 | 2. Run the linters locally via `golangci-lint run` 14 | 3. Update the [CHANGELOG.md](CHANGELOG.md) with the changes you made (in the "Unreleased" section) 15 | 4. Consider updating the [README.md](README.md) with details of your changes. 16 | When in doubt, lets discuss the need together in the corresponding Github issue. 17 | 18 | ## Code of Conduct 19 | 20 | This project follows the **Gopher Code of Conduct** as described at https://golang.org/conduct `\ʕ◔ϖ◔ʔ/` 21 | 22 | ### Our Pledge 23 | 24 | In the interest of fostering an open and welcoming environment, we as 25 | contributors and maintainers pledge to making participation in our project and 26 | our community a harassment-free experience for everyone, regardless of age, body 27 | size, disability, ethnicity, gender identity and expression, level of experience, 28 | nationality, personal appearance, race, religion, or sexual identity and 29 | orientation. 30 | 31 | ### Our Standards 32 | 33 | Examples of behavior that contributes to creating a positive environment 34 | include: 35 | 36 | * Using welcoming and inclusive language 37 | * Being respectful of differing viewpoints and experiences 38 | * Gracefully accepting constructive criticism 39 | * Focusing on what is best for the community 40 | * Showing empathy towards other community members 41 | 42 | Examples of unacceptable behavior by participants include: 43 | 44 | * The use of sexualized language or imagery and unwelcome sexual attention or 45 | advances 46 | * Trolling, insulting/derogatory comments, and personal or political attacks 47 | * Public or private harassment 48 | * Publishing others' private information, such as a physical or electronic 49 | address, without explicit permission 50 | * Other conduct which could reasonably be considered inappropriate in a 51 | professional setting 52 | 53 | ### Our Responsibilities 54 | 55 | Project maintainers are responsible for clarifying the standards of acceptable 56 | behavior and are expected to take appropriate and fair corrective action in 57 | response to any instances of unacceptable behavior. 58 | 59 | Project maintainers have the right and responsibility to remove, edit, or 60 | reject comments, commits, code, wiki edits, issues, and other contributions 61 | that are not aligned to this Code of Conduct, or to ban temporarily or 62 | permanently any contributor for other behaviors that they deem inappropriate, 63 | threatening, offensive, or harmful. 64 | 65 | ### Scope 66 | 67 | This Code of Conduct applies both within project spaces and in public spaces 68 | when an individual is representing the project or its community. Examples of 69 | representing a project or community include using an official project e-mail 70 | address, posting via an official social media account, or acting as an appointed 71 | representative at an online or offline event. Representation of a project may be 72 | further defined and clarified by project maintainers. 73 | 74 | ### Conflict Resolution 75 | 76 | We do not believe that all conflict is bad; healthy debate and disagreement 77 | often yield positive results. However, it is never okay to be disrespectful or 78 | to engage in behavior that violates the project’s code of conduct. 79 | 80 | If you see someone violating the code of conduct, you are encouraged to address 81 | the behavior directly with those involved. Many issues can be resolved quickly 82 | and easily, and this gives people more control over the outcome of their dispute. 83 | If you are unable to resolve the matter for any reason, or if the behavior is 84 | threatening or harassing, report it. We are dedicated to providing an environment 85 | where participants feel welcome and safe. 86 | 87 | ### Attribution 88 | 89 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 90 | available at [http://contributor-covenant.org/version/1/4][version] 91 | 92 | [issues]: https://github.com/fgrosse/go-home/issues 93 | [talk-code]: https://dave.cheney.net/2019/02/18/talk-then-code 94 | [homepage]: http://contributor-covenant.org 95 | [version]: http://contributor-covenant.org/version/1/4/ 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Friedrich Große 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /LICENSE_FONT: -------------------------------------------------------------------------------- 1 | Copyright (c) , (), 2 | with Reserved Font Name . 3 | Copyright (c) , (), 4 | with Reserved Font Name . 5 | Copyright (c) , (). 6 | 7 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 8 | This license is copied below, and is also available with a FAQ at: 9 | http://scripts.sil.org/OFL 10 | 11 | 12 | ----------------------------------------------------------- 13 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 14 | ----------------------------------------------------------- 15 | 16 | PREAMBLE 17 | The goals of the Open Font License (OFL) are to stimulate worldwide 18 | development of collaborative font projects, to support the font creation 19 | efforts of academic and linguistic communities, and to provide a free and 20 | open framework in which fonts may be shared and improved in partnership 21 | with others. 22 | 23 | The OFL allows the licensed fonts to be used, studied, modified and 24 | redistributed freely as long as they are not sold by themselves. The 25 | fonts, including any derivative works, can be bundled, embedded, 26 | redistributed and/or sold with any software provided that any reserved 27 | names are not used by derivative works. The fonts and derivatives, 28 | however, cannot be released under any other type of license. The 29 | requirement for fonts to remain under this license does not apply 30 | to any document created using the fonts or their derivatives. 31 | 32 | DEFINITIONS 33 | "Font Software" refers to the set of files released by the Copyright 34 | Holder(s) under this license and clearly marked as such. This may 35 | include source files, build scripts and documentation. 36 | 37 | "Reserved Font Name" refers to any names specified as such after the 38 | copyright statement(s). 39 | 40 | "Original Version" refers to the collection of Font Software components as 41 | distributed by the Copyright Holder(s). 42 | 43 | "Modified Version" refers to any derivative made by adding to, deleting, 44 | or substituting -- in part or in whole -- any of the components of the 45 | Original Version, by changing formats or by porting the Font Software to a 46 | new environment. 47 | 48 | "Author" refers to any designer, engineer, programmer, technical 49 | writer or other person who contributed to the Font Software. 50 | 51 | PERMISSION & CONDITIONS 52 | Permission is hereby granted, free of charge, to any person obtaining 53 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 54 | redistribute, and sell modified and unmodified copies of the Font 55 | Software, subject to the following conditions: 56 | 57 | 1) Neither the Font Software nor any of its individual components, 58 | in Original or Modified Versions, may be sold by itself. 59 | 60 | 2) Original or Modified Versions of the Font Software may be bundled, 61 | redistributed and/or sold with any software, provided that each copy 62 | contains the above copyright notice and this license. These can be 63 | included either as stand-alone text files, human-readable headers or 64 | in the appropriate machine-readable metadata fields within text or 65 | binary files as long as those fields can be easily viewed by the user. 66 | 67 | 3) No Modified Version of the Font Software may use the Reserved Font 68 | Name(s) unless explicit written permission is granted by the corresponding 69 | Copyright Holder. This restriction only applies to the primary font name as 70 | presented to the users. 71 | 72 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 73 | Software shall not be used to promote, endorse or advertise any 74 | Modified Version, except to acknowledge the contribution(s) of the 75 | Copyright Holder(s) and the Author(s) or with their explicit written 76 | permission. 77 | 78 | 5) The Font Software, modified or unmodified, in part or in whole, 79 | must be distributed entirely under this license, and must not be 80 | distributed under any other license. The requirement for fonts to 81 | remain under this license does not apply to any document created 82 | using the Font Software. 83 | 84 | TERMINATION 85 | This license becomes null and void if any of the above conditions are 86 | not met. 87 | 88 | DISCLAIMER 89 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 90 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 91 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 92 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 93 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 94 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 95 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 96 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 97 | OTHER DEALINGS IN THE FONT SOFTWARE. 98 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup 2 | 3 | # The setup-fedora task installs all required non-go development dependencies 4 | # This list is based on https://engoengine.github.io/tutorials/00-foreword which 5 | # lists the dependencies for Ubuntu. 6 | # Also https://github.com/go-gl/glfw/blob/master/README.md lists concrete 7 | # package names for Fedora (CentOS) 8 | setup-fedora: 9 | dnf install \ 10 | alsa-lib-devel \ 11 | mesa-libGLU-devel \ 12 | mesa-libGL-devel \ 13 | freeglut-devel \ 14 | git-all \ 15 | libX11-devel \ 16 | libXcursor-devel \ 17 | libXrandr-devel \ 18 | libXinerama-devel \ 19 | libXi-devel \ 20 | libXxf86vm-devel 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Time to Go Home :clock10:

2 |

3 | 4 | 5 |

6 | 7 | --- 8 | 9 | Go Home is a small OpenGL based progress bar widget for your Desktop that 10 | displays for how long you have been working each day. This is helpful for people 11 | who tend to loose track of time and thus do overhours when they actually wanted 12 | to leave home. I made this to learn a bit about simple OpenGL programming using 13 | [the Go programming language][go]. 14 | 15 |

16 | 17 | 18 |

19 | 20 | ## Installation 21 | 22 | ### Precompiled binaries 23 | 24 | You can find precompiled binaries at the [releases] page of the GitHub 25 | repository. 26 | 27 | ### From Source 28 | 29 | Go Home is packaged using [Go modules][go-modules]. Since this is not a library 30 | but a runnable application within a `main` package you need to clone this 31 | repository first. Typically this should be done outside of the `$GOPATH` or Go 32 | will complain due Modules being enabled. 33 | 34 | After you cloned the repo you should make sure to install the external 35 | dependencies (i.e. OpenGL bindings) as explained at the 36 | [GLFW repository][external-deps]. There is a [Makefile](Makefile) to install the 37 | requires libraries on RedHead/Fedora. 38 | 39 | Afterwards you simply use `go build` or `go install` and Go will fetch the 40 | correct Go dependencies for you: 41 | 42 | ```bash 43 | $ git clone https://github.com/fgrosse/go-home.git 44 | Cloning into 'go-home'... 45 | remote: Enumerating objects: 122, done. 46 | remote: Counting objects: 100% (122/122), done. 47 | remote: Compressing objects: 100% (69/69), done. 48 | remote: Total 122 (delta 62), reused 109 (delta 49), pack-reused 0 49 | Receiving objects: 100% (122/122), 71.62 KiB | 601.00 KiB/s, done. 50 | Resolving deltas: 100% (62/62), done. 51 | 52 | $ cd go-home 53 | $ make setup 54 | … 55 | 56 | $ go install && go-home --debug 57 | 2019-06-16 13:16 DEBUG go-home/config.go:50 Running in debug mode 58 | 2019-06-16 13:16 INFO go-home/config.go:54 Loading configuration {"path": "/home/fgrosse/.go-home.yml"} 59 | 2019-06-16 13:16 INFO go-home/app.go:56 Starting application {"config": {"check_in": "2019-06-16 12:18", "work_duration": "8h0m0s", "lunch_duration": "1h0m0s", "day_end": "20:00"}} 60 | ``` 61 | 62 | ## Usage 63 | 64 | You can start the program without any arguments which will create an 65 | undecorated window that displays when you started the program the first time 66 | today and when its time to go home. The default configuration assumes you are 67 | working 8 hours a day and do 1 hour of lunch break. 68 | 69 | As time goes by the progress bar will slowly fill up from green to red. If you 70 | are working overtime it will start to pulse red to catch your attention. At this 71 | point you should leave home and enjoy your free time with your family and 72 | friends :relaxed:. 73 | 74 | ### Configuration 75 | 76 | Go Home reads configuration from `$HOME/.go-home.yml`. If this file does not 77 | exist on the first start it will be created using sensible default values. 78 | The available options in there should be pretty self explanatory. 79 | 80 | ## Built With 81 | 82 | * [pixel](https://github.com/faiface/pixel) - A hand-crafted 2D game library in Go 83 | * [glfw](https://github.com/go-gl/glfw) - Go bindings for GLFW 3 84 | * [cobra](https://github.com/spf13/cobra) - A Commander for modern Go CLI interactions 85 | * [zap](https://github.com/uber-go/zap) - Blazing fast, structured, leveled logging in Go 86 | * [pkg/errors](https://github.com/pkg/errors) - Simple error handling primitives 87 | * [gopkg.in/yaml](https://gopkg.in/yaml.v3) - YAML support for the Go language 88 | * [Glacial Indifference Font](https://fontlibrary.org/en/font/glacial-indifference) - An open source typeface by Alfredo Marco Pradil ([SIL Open Font License](LICENSE_FONT)) 89 | 90 | ## Contributing 91 | 92 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of 93 | conduct and on the process for submitting pull requests to this repository. 94 | 95 | ## Versioning 96 | 97 | This software uses [SemVer] for versioning. 98 | For the versions available, see the [tags on this repository][tags]. 99 | 100 | ## Authors 101 | 102 | - **Friedrich Große** - *Initial work* - [fgrosse] 103 | 104 | See also the list of [contributors] who participated in this project. 105 | 106 | ## License 107 | 108 | This project is licensed under the BSD-3-Clause License - see the [LICENSE](LICENSE) file for details. 109 | 110 | [releases]: https://github.com/fgrosse/go-home/releases 111 | [external-deps]: https://github.com/go-gl/glfw/blob/master/README.md 112 | [go]: https://golang.org 113 | [go-modules]: https://github.com/golang/go/wiki/Modules 114 | [SemVer]: http://semver.org 115 | [tags]: https://github.com/fgrosse/go-home/tags 116 | [fgrosse]: https://github.com/fgrosse 117 | [contributors]: https://github.com/github.com/fgrosse/go-home/contributors 118 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | "os" 6 | "time" 7 | 8 | "github.com/faiface/pixel" 9 | "github.com/faiface/pixel/pixelgl" 10 | "github.com/pkg/errors" 11 | "github.com/spf13/cobra" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | type App struct { 16 | *cobra.Command 17 | logger *zap.Logger 18 | conf Config 19 | win *pixelgl.Window 20 | render *Render 21 | 22 | initErr error 23 | shutdown bool 24 | limitFPS bool 25 | } 26 | 27 | func NewApp() *App { 28 | app := &App{Command: &cobra.Command{ 29 | Use: "go-home", 30 | }} 31 | 32 | app.SilenceUsage = true // do not output usage in case of an error 33 | app.SilenceErrors = true // we log them manually in the main function 34 | app.Command.RunE = app.Run 35 | 36 | var ( 37 | debug bool 38 | config string 39 | ) 40 | 41 | flags := app.PersistentFlags() 42 | flags.StringVar(&config, "config", os.ExpandEnv("$HOME/.go-home.yml"), "config file") 43 | flags.BoolVar(&debug, "debug", false, "enable debug mode") 44 | 45 | cobra.OnInitialize(app.loadConfig(&debug, &config)) 46 | 47 | return app 48 | } 49 | 50 | func (app *App) Run(_ *cobra.Command, _ []string) error { 51 | if app.initErr != nil { 52 | return app.initErr 53 | } 54 | 55 | var err error 56 | app.logger.Info("Starting application", zap.Object("config", app.conf)) 57 | err = app.createWindow(app.conf.UI) 58 | if err != nil { 59 | return errors.Wrap(err, "failed to create window") 60 | } 61 | 62 | app.render, err = NewRender(app.conf) 63 | if err != nil { 64 | return errors.Wrap(err, "failed to create renderer") 65 | } 66 | 67 | app.runLoop() 68 | 69 | err = app.conf.Save() 70 | if err != nil { 71 | app.logger.Error("Failed to save configuration on shutdown", zap.Error(err)) 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func (app *App) createWindow(conf UIConfig) error { 78 | width := float64(conf.WindowWidth) 79 | height := float64(conf.WindowHeight) 80 | 81 | cfg := pixelgl.WindowConfig{ 82 | Title: "Go Home", 83 | Bounds: pixel.R(0, 0, width, height), 84 | VSync: true, 85 | Undecorated: true, 86 | Resizable: false, 87 | AlwaysOnTop: true, 88 | } 89 | 90 | // TODO: we need GLFW 3.3 where we get the GLFW_TRANSPARENT_FRAMEBUFFER option 91 | // See: https://www.glfw.org/docs/3.3/window_guide.html#window_hints_wnd 92 | 93 | var err error 94 | app.win, err = pixelgl.NewWindow(cfg) 95 | if err != nil { 96 | return errors.WithStack(err) 97 | } 98 | 99 | app.win.SetSmooth(true) 100 | app.win.SetPos(app.conf.UI.WindowPos) 101 | app.win.Update() 102 | 103 | return nil 104 | } 105 | 106 | func (app *App) runLoop() { 107 | app.limitFPS = true // might be disabled when moving the window via Shift+Arrow 108 | fps := time.Tick(time.Second / time.Duration(app.conf.UI.FPS)) 109 | last := time.Now() 110 | for !app.win.Closed() { 111 | dt := time.Since(last).Seconds() 112 | last = time.Now() 113 | 114 | // TODO: deal with a new dawn 115 | 116 | app.win.Clear(color.White) 117 | app.handleInput(app.win, dt) 118 | app.render.Draw(app.win) 119 | app.win.Update() 120 | 121 | if app.shutdown { 122 | return 123 | } 124 | 125 | if app.limitFPS { 126 | <-fps 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /assets/.gitignore: -------------------------------------------------------------------------------- 1 | *.otf 2 | -------------------------------------------------------------------------------- /assets/GlacialIndifference-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgrosse/go-home/201fa662449e668493b361f62f8b7a4d2f92927c/assets/GlacialIndifference-Regular.ttf -------------------------------------------------------------------------------- /assets/assets.go: -------------------------------------------------------------------------------- 1 | // Code generated by "esc -o=assets/assets.go -pkg=assets -include=.+\.ttf assets"; DO NOT EDIT. 2 | 3 | package assets 4 | 5 | import ( 6 | "bytes" 7 | "compress/gzip" 8 | "encoding/base64" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "net/http" 13 | "os" 14 | "path" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | type _escLocalFS struct{} 20 | 21 | var _escLocal _escLocalFS 22 | 23 | type _escStaticFS struct{} 24 | 25 | var _escStatic _escStaticFS 26 | 27 | type _escDirectory struct { 28 | fs http.FileSystem 29 | name string 30 | } 31 | 32 | type _escFile struct { 33 | compressed string 34 | size int64 35 | modtime int64 36 | local string 37 | isDir bool 38 | 39 | once sync.Once 40 | data []byte 41 | name string 42 | } 43 | 44 | func (_escLocalFS) Open(name string) (http.File, error) { 45 | f, present := _escData[path.Clean(name)] 46 | if !present { 47 | return nil, os.ErrNotExist 48 | } 49 | return os.Open(f.local) 50 | } 51 | 52 | func (_escStaticFS) prepare(name string) (*_escFile, error) { 53 | f, present := _escData[path.Clean(name)] 54 | if !present { 55 | return nil, os.ErrNotExist 56 | } 57 | var err error 58 | f.once.Do(func() { 59 | f.name = path.Base(name) 60 | if f.size == 0 { 61 | return 62 | } 63 | var gr *gzip.Reader 64 | b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(f.compressed)) 65 | gr, err = gzip.NewReader(b64) 66 | if err != nil { 67 | return 68 | } 69 | f.data, err = ioutil.ReadAll(gr) 70 | }) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return f, nil 75 | } 76 | 77 | func (fs _escStaticFS) Open(name string) (http.File, error) { 78 | f, err := fs.prepare(name) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return f.File() 83 | } 84 | 85 | func (dir _escDirectory) Open(name string) (http.File, error) { 86 | return dir.fs.Open(dir.name + name) 87 | } 88 | 89 | func (f *_escFile) File() (http.File, error) { 90 | type httpFile struct { 91 | *bytes.Reader 92 | *_escFile 93 | } 94 | return &httpFile{ 95 | Reader: bytes.NewReader(f.data), 96 | _escFile: f, 97 | }, nil 98 | } 99 | 100 | func (f *_escFile) Close() error { 101 | return nil 102 | } 103 | 104 | func (f *_escFile) Readdir(count int) ([]os.FileInfo, error) { 105 | if !f.isDir { 106 | return nil, fmt.Errorf(" escFile.Readdir: '%s' is not directory", f.name) 107 | } 108 | 109 | fis, ok := _escDirs[f.local] 110 | if !ok { 111 | return nil, fmt.Errorf(" escFile.Readdir: '%s' is directory, but we have no info about content of this dir, local=%s", f.name, f.local) 112 | } 113 | limit := count 114 | if count <= 0 || limit > len(fis) { 115 | limit = len(fis) 116 | } 117 | 118 | if len(fis) == 0 && count > 0 { 119 | return nil, io.EOF 120 | } 121 | 122 | return fis[0:limit], nil 123 | } 124 | 125 | func (f *_escFile) Stat() (os.FileInfo, error) { 126 | return f, nil 127 | } 128 | 129 | func (f *_escFile) Name() string { 130 | return f.name 131 | } 132 | 133 | func (f *_escFile) Size() int64 { 134 | return f.size 135 | } 136 | 137 | func (f *_escFile) Mode() os.FileMode { 138 | return 0 139 | } 140 | 141 | func (f *_escFile) ModTime() time.Time { 142 | return time.Unix(f.modtime, 0) 143 | } 144 | 145 | func (f *_escFile) IsDir() bool { 146 | return f.isDir 147 | } 148 | 149 | func (f *_escFile) Sys() interface{} { 150 | return f 151 | } 152 | 153 | // FS returns a http.Filesystem for the embedded assets. If useLocal is true, 154 | // the filesystem's contents are instead used. 155 | func FS(useLocal bool) http.FileSystem { 156 | if useLocal { 157 | return _escLocal 158 | } 159 | return _escStatic 160 | } 161 | 162 | // Dir returns a http.Filesystem for the embedded assets on a given prefix dir. 163 | // If useLocal is true, the filesystem's contents are instead used. 164 | func Dir(useLocal bool, name string) http.FileSystem { 165 | if useLocal { 166 | return _escDirectory{fs: _escLocal, name: name} 167 | } 168 | return _escDirectory{fs: _escStatic, name: name} 169 | } 170 | 171 | // FSByte returns the named file from the embedded assets. If useLocal is 172 | // true, the filesystem's contents are instead used. 173 | func FSByte(useLocal bool, name string) ([]byte, error) { 174 | if useLocal { 175 | f, err := _escLocal.Open(name) 176 | if err != nil { 177 | return nil, err 178 | } 179 | b, err := ioutil.ReadAll(f) 180 | _ = f.Close() 181 | return b, err 182 | } 183 | f, err := _escStatic.prepare(name) 184 | if err != nil { 185 | return nil, err 186 | } 187 | return f.data, nil 188 | } 189 | 190 | // FSMustByte is the same as FSByte, but panics if name is not present. 191 | func FSMustByte(useLocal bool, name string) []byte { 192 | b, err := FSByte(useLocal, name) 193 | if err != nil { 194 | panic(err) 195 | } 196 | return b 197 | } 198 | 199 | // FSString is the string version of FSByte. 200 | func FSString(useLocal bool, name string) (string, error) { 201 | b, err := FSByte(useLocal, name) 202 | return string(b), err 203 | } 204 | 205 | // FSMustString is the string version of FSMustByte. 206 | func FSMustString(useLocal bool, name string) string { 207 | return string(FSMustByte(useLocal, name)) 208 | } 209 | 210 | var _escData = map[string]*_escFile{ 211 | 212 | "/assets/GlacialIndifference-Regular.ttf": { 213 | name: "GlacialIndifference-Regular.ttf", 214 | local: "assets/GlacialIndifference-Regular.ttf", 215 | size: 33084, 216 | modtime: 1558258674, 217 | compressed: ` 218 | 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 484 | lUtve/D/FwAA//+pogaLPIEAAA== 485 | `, 486 | }, 487 | 488 | "/assets": { 489 | name: "assets", 490 | local: `assets`, 491 | isDir: true, 492 | }, 493 | } 494 | 495 | var _escDirs = map[string][]os.FileInfo{ 496 | 497 | "assets": { 498 | _escData["/assets/GlacialIndifference-Regular.ttf"], 499 | }, 500 | } 501 | -------------------------------------------------------------------------------- /assets/screenshot_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgrosse/go-home/201fa662449e668493b361f62f8b7a4d2f92927c/assets/screenshot_01.png -------------------------------------------------------------------------------- /assets/screenshot_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgrosse/go-home/201fa662449e668493b361f62f8b7a4d2f92927c/assets/screenshot_02.png -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "math" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/faiface/pixel" 14 | "github.com/pkg/errors" 15 | "go.uber.org/zap" 16 | "go.uber.org/zap/zapcore" 17 | "gopkg.in/yaml.v3" 18 | ) 19 | 20 | type Config struct { 21 | CheckIn time.Time `yaml:"check_in"` 22 | CheckOut time.Time `yaml:"-"` 23 | EndOfDay time.Time `yaml:"-"` 24 | 25 | WorkDuration time.Duration `yaml:"work_duration"` 26 | LunchDuration time.Duration `yaml:"lunch_duration"` 27 | DayEnd ClockTime `yaml:"day_end"` 28 | 29 | UI UIConfig `yaml:"ui"` 30 | Debug bool `yaml:"-"` 31 | 32 | path string `yaml:"-"` 33 | } 34 | 35 | type UIConfig struct { 36 | FPS int `yaml:"fps"` 37 | WindowWidth int `yaml:"width"` 38 | WindowHeight int `yaml:"height"` 39 | WindowPos pixel.Vec `yaml:"pos"` 40 | ShowRemainingTime bool `yaml:"show_remaining_time"` 41 | } 42 | 43 | func (app *App) loadConfig(debug *bool, path *string) func() { 44 | return func() { 45 | app.logger = newLogger(*debug) 46 | if app.initErr != nil { 47 | return 48 | } 49 | 50 | app.logger.Debug("Running in debug mode") 51 | 52 | var r io.Reader 53 | if f, err := os.Open(*path); err == nil { 54 | app.logger.Info("Loading configuration", zap.String("path", *path)) 55 | r = f 56 | defer f.Close() 57 | } else if os.IsNotExist(err) { 58 | app.logger.Info("Configuration file not found. Creating new file", zap.String("path", *path)) 59 | } else { 60 | app.initErr = errors.Wrap(err, "failed to open config file") 61 | return 62 | } 63 | 64 | app.conf, app.initErr = LoadConfig(r, app.logger, *path, *debug) 65 | if app.initErr != nil { 66 | return 67 | } 68 | 69 | app.initErr = app.conf.Save() 70 | } 71 | } 72 | 73 | func LoadConfig(r io.Reader, logger *zap.Logger, path string, debug bool) (Config, error) { 74 | conf := Config{path: path} 75 | if r != nil { 76 | dec := yaml.NewDecoder(r) 77 | dec.KnownFields(true) 78 | err := dec.Decode(&conf) 79 | if err != nil { 80 | return conf, errors.Wrap(err, "failed to decode config") 81 | } 82 | } 83 | 84 | if conf.UI.WindowWidth == 0 { 85 | conf.UI.WindowWidth = 512 86 | } 87 | if conf.UI.WindowHeight == 0 { 88 | conf.UI.WindowHeight = 32 89 | } 90 | 91 | if conf.UI.WindowPos.X == 0 && conf.UI.WindowPos.Y == 0 { 92 | var displayWidth float64 = 1920 // TODO: make dynamic 93 | conf.UI.WindowPos = pixel.Vec{ 94 | X: displayWidth/2 - float64(conf.UI.WindowWidth)/2, 95 | Y: 35, 96 | } 97 | } 98 | 99 | if conf.WorkDuration == 0 { 100 | conf.WorkDuration = 8 * time.Hour 101 | } 102 | if conf.LunchDuration == 0 { 103 | conf.LunchDuration = 1 * time.Hour 104 | } 105 | if conf.DayEnd.Hour == 0 { 106 | conf.DayEnd = ClockTime{Hour: 20, Minute: 00} 107 | } 108 | if conf.UI.FPS == 0 { 109 | conf.UI.FPS = 10 110 | } 111 | if conf.CheckIn.IsZero() || isDifferentDay(conf.CheckIn, time.Now()) { 112 | conf.CheckIn = time.Now() 113 | logger.Info("Detected start of new day", zap.String("date", conf.CheckIn.Format("2006-01-02"))) 114 | } 115 | 116 | conf.CheckIn = conf.CheckIn.Round(time.Second) 117 | conf.CheckOut = conf.CheckIn.Add(conf.WorkDuration).Add(conf.LunchDuration) 118 | conf.EndOfDay = conf.DayEnd.Time(conf.CheckIn) 119 | conf.Debug = debug 120 | 121 | return conf, nil 122 | } 123 | 124 | func isDifferentDay(a, b time.Time) bool { 125 | yearA, monthA, dayA := a.Date() 126 | yearB, monthB, dayB := b.Date() 127 | return yearA != yearB || monthA != monthB || dayA != dayB 128 | } 129 | 130 | func (conf Config) Save() error { 131 | conf.UI.WindowPos.X = math.Round(conf.UI.WindowPos.X) 132 | conf.UI.WindowPos.Y = math.Round(conf.UI.WindowPos.Y) 133 | 134 | data, err := yaml.Marshal(conf) 135 | if err != nil { 136 | return errors.Wrap(err, "failed to encode config as YAML") 137 | } 138 | 139 | err = ioutil.WriteFile(conf.path, data, 0666) 140 | if err != nil { 141 | return errors.Wrap(err, "failed to save config") 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func (conf Config) MarshalLogObject(enc zapcore.ObjectEncoder) error { 148 | if conf.CheckIn.IsZero() { 149 | enc.AddString("check_in", "-") 150 | } else { 151 | enc.AddString("check_in", conf.CheckIn.Format("2006-01-02 15:04")) 152 | } 153 | 154 | enc.AddDuration("work_duration", conf.WorkDuration) 155 | enc.AddDuration("lunch_duration", conf.LunchDuration) 156 | enc.AddString("day_end", conf.DayEnd.String()) 157 | 158 | return nil 159 | } 160 | 161 | type ClockTime struct { 162 | Hour, Minute int 163 | } 164 | 165 | func (t *ClockTime) UnmarshalText(text []byte) error { 166 | parts := strings.Split(string(text), ":") 167 | if len(parts) != 2 { 168 | return errors.New(`ClockTime string is not formatted as "hh:mm"`) 169 | } 170 | 171 | var err error 172 | t.Hour, err = strconv.Atoi(parts[0]) 173 | if err != nil { 174 | return errors.Errorf("hour part is not an integer") 175 | } 176 | 177 | t.Minute, err = strconv.Atoi(parts[1]) 178 | if err != nil { 179 | return errors.Errorf("minute part is not an integer") 180 | } 181 | 182 | if t.Hour < 0 || t.Hour > 24 { 183 | return errors.Errorf("invalid hour") 184 | } 185 | 186 | if t.Minute < 0 || t.Minute > 59 { 187 | return errors.Errorf("invalid minute") 188 | } 189 | 190 | return nil 191 | } 192 | 193 | func (t ClockTime) Time(ref time.Time) time.Time { 194 | year, month, day := ref.Date() 195 | return time.Date(year, month, day, t.Hour, t.Minute, 0, 0, ref.Location()) 196 | } 197 | 198 | func (t ClockTime) String() string { 199 | return fmt.Sprintf("%02d:%02d", t.Hour, t.Minute) 200 | } 201 | 202 | func (t ClockTime) MarshalYAML() (interface{}, error) { 203 | return t.String(), nil 204 | } 205 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fgrosse/go-home 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/faiface/pixel v0.9.0 7 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 8 | github.com/pkg/errors v0.8.1 9 | github.com/spf13/cobra v0.0.4 10 | go.uber.org/atomic v1.4.0 // indirect 11 | go.uber.org/multierr v1.1.0 // indirect 12 | go.uber.org/zap v1.10.0 13 | golang.org/x/image v0.6.0 14 | gopkg.in/yaml.v3 v3.0.0-20190502103701-55513cacd4ae 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/faiface/glhf v0.0.0-20181018222622-82a6317ac380 h1:FvZ0mIGh6b3kOITxUnxS3tLZMh7yEoHo75v3/AgUqg0= 11 | github.com/faiface/glhf v0.0.0-20181018222622-82a6317ac380/go.mod h1:zqnPFFIuYFFxl7uH2gYByJwIVKG7fRqlqQCbzAnHs9g= 12 | github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q= 13 | github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M= 14 | github.com/faiface/pixel v0.9.0 h1:EtOO20jUkJ+SQAtWy19acwmhn/gowQNcfxpvfL8MTE0= 15 | github.com/faiface/pixel v0.9.0/go.mod h1:WkLfLymV31e/Ogv5OR3vtrNxRktTO3WXGWXiiSEg/j4= 16 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 17 | github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw= 18 | github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= 19 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= 20 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 21 | github.com/go-gl/mathgl v0.0.0-20190416160123-c4601bc793c7 h1:THttjeRn1iiz69E875U6gAik8KTWk/JYAHoSVpUxBBI= 22 | github.com/go-gl/mathgl v0.0.0-20190416160123-c4601bc793c7/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= 23 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 24 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 25 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 26 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 27 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 28 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 29 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 30 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 31 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 32 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 33 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 37 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 38 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 39 | github.com/spf13/cobra v0.0.4 h1:S0tLZ3VOKl2Te0hpq8+ke0eSJPfCnNTPiDlsfwi1/NE= 40 | github.com/spf13/cobra v0.0.4/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 41 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 42 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 43 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 44 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 45 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 46 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 47 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 48 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 49 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 50 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 51 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 52 | go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= 53 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 54 | go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= 55 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 56 | go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= 57 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 58 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 59 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 60 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 61 | golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 62 | golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 63 | golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= 64 | golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= 65 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 66 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 67 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 68 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 69 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 70 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 71 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 72 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 73 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 74 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 75 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 76 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 82 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 83 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 84 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 85 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 86 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 87 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 88 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 89 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 90 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 91 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 92 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 93 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 95 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 97 | gopkg.in/yaml.v3 v3.0.0-20190502103701-55513cacd4ae h1:ehhBuCxzgQEGk38YjhFv/97fMIc2JGHZAhAWMmEjmu0= 98 | gopkg.in/yaml.v3 v3.0.0-20190502103701-55513cacd4ae/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 99 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "time" 6 | 7 | "github.com/faiface/pixel/pixelgl" 8 | ) 9 | 10 | func (app *App) handleInput(win *pixelgl.Window, dt float64) { 11 | if win.Pressed(pixelgl.KeyLeftShift) { 12 | app.limitFPS = false 13 | 14 | const speed = 100.0 // pixel per second 15 | delta := speed * dt 16 | if win.Pressed(pixelgl.KeyRight) { 17 | app.conf.UI.WindowPos.X += delta 18 | } else if win.Pressed(pixelgl.KeyLeft) { 19 | app.conf.UI.WindowPos.X -= delta 20 | } 21 | 22 | if win.Pressed(pixelgl.KeyUp) { 23 | app.conf.UI.WindowPos.Y -= delta 24 | } else if win.Pressed(pixelgl.KeyDown) { 25 | app.conf.UI.WindowPos.Y += delta 26 | } 27 | 28 | currPos := app.win.GetPos() 29 | if math.Round(currPos.X) != math.Round(app.conf.UI.WindowPos.X) || 30 | math.Round(currPos.Y) != math.Round(app.conf.UI.WindowPos.Y) { 31 | app.win.SetPos(app.conf.UI.WindowPos) 32 | } 33 | 34 | } else { 35 | app.limitFPS = true 36 | 37 | if app.conf.Debug { 38 | const speed = 2 * float64(time.Hour) 39 | switch { 40 | case win.Pressed(pixelgl.KeyRight): 41 | app.render.timeShift += time.Duration(speed * dt) 42 | case win.Pressed(pixelgl.KeyLeft): 43 | app.render.timeShift -= time.Duration(speed * dt) 44 | case win.Pressed(pixelgl.KeyEscape): 45 | app.shutdown = true 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | func newLogger(debug bool) *zap.Logger { 12 | encConf := zap.NewDevelopmentEncoderConfig() 13 | encConf.EncodeDuration = zapcore.StringDurationEncoder 14 | encConf.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 15 | enc.AppendString(t.Format("2006-01-02 15:04")) 16 | } 17 | 18 | conf := zap.Config{ 19 | Level: zap.NewAtomicLevelAt(zap.InfoLevel), 20 | Encoding: "console", 21 | EncoderConfig: encConf, 22 | DisableCaller: true, 23 | OutputPaths: []string{"stdout"}, 24 | ErrorOutputPaths: []string{"stdout"}, 25 | } 26 | 27 | if debug { 28 | conf.Level = zap.NewAtomicLevelAt(zap.DebugLevel) 29 | conf.Development = true 30 | conf.DisableCaller = false 31 | } 32 | 33 | logger, err := conf.Build() 34 | if err != nil { 35 | panic(errors.Wrap(err, "failed to create logger")) 36 | } 37 | 38 | return logger 39 | } 40 | -------------------------------------------------------------------------------- /login_events.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | dbus-monitor --session "type='signal',interface='org.gnome.ScreenSaver'" | 4 | while read x; do 5 | case "$x" in 6 | *"boolean true"*) echo SCREEN_LOCKED;; 7 | *"boolean false"*) echo SCREEN_UNLOCKED;; 8 | esac 9 | done 10 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/faiface/pixel/pixelgl" 8 | ) 9 | 10 | func main() { 11 | var err error 12 | pixelgl.Run(func() { 13 | cmd := NewApp() 14 | err = cmd.Execute() 15 | }) 16 | 17 | if err != nil { 18 | fmt.Fprintln(os.Stderr, "ERROR:", err) 19 | os.Exit(1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | "time" 7 | 8 | "github.com/faiface/pixel" 9 | "github.com/faiface/pixel/imdraw" 10 | "github.com/faiface/pixel/text" 11 | "github.com/fgrosse/go-home/assets" 12 | "github.com/golang/freetype/truetype" 13 | "github.com/pkg/errors" 14 | "golang.org/x/image/colornames" 15 | "golang.org/x/image/font" 16 | ) 17 | 18 | type Render struct { 19 | Width, Height float64 20 | CheckIn time.Time 21 | CheckOut time.Time 22 | EOD time.Time 23 | Atlas *text.Atlas 24 | MarkerColor color.Color 25 | BorderColor color.Color 26 | ShowRemainingTime bool // show how much time is left instead of the current time 27 | timeShift time.Duration // for debugging 28 | } 29 | 30 | func NewRender(conf Config) (*Render, error) { 31 | fnt, err := loadTTF("/assets/GlacialIndifference-Regular.ttf", 12) 32 | if err != nil { 33 | return nil, errors.Wrap(err, "failed to load font") 34 | } 35 | 36 | return &Render{ 37 | Width: float64(conf.UI.WindowWidth), 38 | Height: float64(conf.UI.WindowHeight), 39 | CheckIn: conf.CheckIn, 40 | CheckOut: conf.CheckOut, 41 | EOD: conf.EndOfDay, 42 | Atlas: text.NewAtlas(fnt, text.ASCII), 43 | MarkerColor: colornames.Mediumblue, 44 | ShowRemainingTime: conf.UI.ShowRemainingTime, 45 | }, nil 46 | } 47 | 48 | func loadTTF(path string, size float64) (font.Face, error) { 49 | bytes := assets.FSMustByte(false, path) 50 | fnt, err := truetype.Parse(bytes) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return truetype.NewFace(fnt, &truetype.Options{ 56 | Size: size, 57 | GlyphCacheEntries: 1, 58 | }), nil 59 | } 60 | 61 | func (r *Render) Draw(t pixel.Target) { 62 | now := time.Now().Add(r.timeShift) 63 | progress := r.progress(now) 64 | 65 | markerTxt := r.markerText(progress, now) 66 | checkoutTxt := r.checkoutText(now) 67 | 68 | r.drawGradient(t, progress) 69 | r.drawCells(t, markerTxt, checkoutTxt) 70 | r.drawTargetMarker(t, progress, markerTxt) 71 | r.drawCurrentMarker(t, progress, now, markerTxt) 72 | r.drawText(t, now, markerTxt, checkoutTxt) 73 | r.drawRectangle(t, now) 74 | } 75 | 76 | func (r *Render) drawGradient(t pixel.Target, progress float64) { 77 | leftColor := pixel.RGB(0, 1, 0) 78 | rightColor := pixel.RGB(progress, 1-progress, 0) 79 | 80 | rect := imdraw.New(nil) 81 | rect.Color = leftColor 82 | rect.Push(pixel.V(0, 0)) 83 | rect.Push(pixel.V(0, r.Height)) 84 | 85 | rect.Color = rightColor 86 | rect.Push(pixel.V(r.Width*progress, r.Height)) 87 | rect.Push(pixel.V(r.Width*progress, 0)) 88 | rect.Polygon(0) 89 | rect.Draw(t) 90 | 91 | // draw gray scale gradient 92 | rect = imdraw.New(nil) 93 | gray := pixel.RGB(0.333, 0.333, 0.333) 94 | rect.Color = gray.Scaled(progress) 95 | rect.Push(pixel.V(r.Width*progress+1, 0)) 96 | rect.Push(pixel.V(r.Width*progress+1, r.Height)) 97 | 98 | rect.Color = gray 99 | rect.Push(pixel.V(r.Width, r.Height)) 100 | rect.Push(pixel.V(r.Width, 0)) 101 | rect.Polygon(0) 102 | rect.Draw(t) 103 | } 104 | 105 | func (r *Render) drawCells(t pixel.Target, markerTxt, checkoutTxt *text.Text) { 106 | numCells := 9 107 | txtBounds := markerTxt.Bounds() 108 | checkoutBounds := checkoutTxt.Bounds() 109 | for i := 0; i < numCells; i++ { 110 | x := r.Width * float64(i) / float64(numCells) 111 | line := imdraw.New(nil) 112 | line.Color = color.Black 113 | 114 | v := pixel.V(x, r.Height/2) 115 | if txtBounds.Contains(v) || checkoutBounds.Contains(v) { 116 | col := pixel.ToRGBA(line.Color) 117 | col.A = 0.1 118 | line.Color = col 119 | } 120 | 121 | line.Push(pixel.V(x, 0)) 122 | line.Push(pixel.V(x, r.Height)) 123 | line.Line(1) 124 | line.Draw(t) 125 | } 126 | } 127 | 128 | func (r *Render) drawTargetMarker(t pixel.Target, progress float64, markerTxt *text.Text) { 129 | posX := r.position(r.CheckOut) 130 | 131 | imd := imdraw.New(nil) 132 | col := pixel.RGB(0, 0, 0) 133 | if markerTxt.Bounds().Contains(pixel.V(posX, r.Height/2)) { 134 | col.A = 0.1 135 | } 136 | 137 | imd.Color = col 138 | imd.Push(pixel.V(posX, 2)) 139 | imd.Push(pixel.V(posX, r.Height-2)) 140 | imd.Line(3) 141 | imd.Draw(t) 142 | } 143 | 144 | func (r *Render) drawCurrentMarker(t pixel.Target, progress float64, now time.Time, txt *text.Text) { 145 | posX := progress * r.Width 146 | col := r.MarkerColor 147 | if now.After(r.CheckOut) { 148 | col = r.BorderColor 149 | } 150 | 151 | imd := imdraw.New(nil) 152 | imd.Color = col 153 | imd.Push(pixel.V(posX, 2)) 154 | imd.Push(pixel.V(posX, r.Height-2)) 155 | imd.Line(2) 156 | imd.Draw(t) 157 | 158 | imd = imdraw.New(nil) 159 | imd.Color = col 160 | imd.Push(pixel.V(posX-5, 2)) 161 | imd.Push(pixel.V(posX+1+5, 2)) 162 | imd.Push(pixel.V(posX+1, 5+2)) 163 | imd.Push(pixel.V(posX, 5+2)) 164 | imd.Polygon(0) 165 | imd.Draw(t) 166 | 167 | imd = imdraw.New(nil) 168 | imd.Color = col 169 | imd.Push(pixel.V(posX-5, r.Height-2)) 170 | imd.Push(pixel.V(posX+1+5, r.Height-2)) 171 | imd.Push(pixel.V(posX+1, r.Height-5-2)) 172 | imd.Push(pixel.V(posX, r.Height-5-2)) 173 | imd.Polygon(0) 174 | imd.Draw(t) 175 | } 176 | 177 | func (r *Render) markerText(progress float64, now time.Time) *text.Text { 178 | posX := progress * r.Width 179 | txt := text.New(pixel.V(posX+5, 4), r.Atlas) 180 | if progress > 0.7 { 181 | txt.Color = color.White 182 | } else { 183 | txt.Color = color.Black 184 | } 185 | 186 | if r.ShowRemainingTime { 187 | remaining := r.CheckOut.Sub(now).Round(time.Minute) 188 | s := remaining.String() 189 | s = s[:len(s)-2] // strip away trailing seconds 190 | txt.WriteString(s) 191 | } else { 192 | txt.WriteString(now.Format("15:04")) 193 | } 194 | 195 | return txt 196 | } 197 | 198 | func (r *Render) checkoutText(now time.Time) *text.Text { 199 | txt := text.New(pixel.V(4+r.position(r.CheckOut), 4), r.Atlas) 200 | if now.Before(r.CheckOut) { 201 | txt.Color = color.White 202 | } else { 203 | txt.Color = color.Black 204 | } 205 | 206 | txt.WriteString(r.CheckOut.Format("15:04")) 207 | return txt 208 | } 209 | 210 | func (r *Render) drawRectangle(t pixel.Target, now time.Time) { 211 | var borderWidth float64 = 2 212 | 213 | rect := imdraw.New(nil) 214 | if now.Before(r.CheckOut) { 215 | r.BorderColor = color.Black 216 | } else { 217 | // Pulsating red border 218 | totalMillis := int(float64(now.UnixNano()) / float64(time.Millisecond)) 219 | scale := (1 + math.Sin(float64(totalMillis)/500)) / 2 220 | r.BorderColor = pixel.ToRGBA(colornames.Red).Mul(pixel.RGB(scale, scale, scale)) 221 | borderWidth = 4 222 | } 223 | 224 | rect.Color = r.BorderColor 225 | rect.EndShape = imdraw.RoundEndShape 226 | rect.Push(pixel.V(1, 1)) 227 | rect.Push(pixel.V(1, r.Height-1)) 228 | rect.Push(pixel.V(r.Width-1, r.Height-1)) 229 | rect.Push(pixel.V(r.Width-1, 1)) 230 | rect.Push(pixel.V(1, 1)) 231 | rect.Line(borderWidth) 232 | rect.Draw(t) 233 | } 234 | 235 | func (r *Render) drawText(t pixel.Target, now time.Time, markerTxt, checkoutTxt *text.Text) { 236 | txt := text.New(pixel.V(4, 4), r.Atlas) 237 | txt.Color = color.Black 238 | txt.WriteString(r.CheckIn.Format("15:04")) 239 | bounds := txt.Bounds() 240 | 241 | shift := r.Height/2 - bounds.Size().Y/2 - 1 242 | m := pixel.IM.Moved(pixel.V(0, shift)) 243 | 244 | if bounds.Intersect(markerTxt.Bounds()) == pixel.ZR { 245 | txt.Draw(t, m) 246 | } 247 | 248 | if checkoutTxt.Bounds().Intersect(markerTxt.Bounds()) == pixel.ZR { 249 | checkoutTxt.Draw(t, m) 250 | } 251 | 252 | markerTxt.Draw(t, m) 253 | } 254 | 255 | func (r *Render) position(t time.Time) float64 { 256 | return r.Width * r.progress(t) 257 | } 258 | 259 | func (r *Render) progress(now time.Time) float64 { 260 | left := float64(r.CheckIn.Unix()) 261 | right := float64(r.EOD.Unix()) 262 | nowUnix := float64(now.Unix()) 263 | 264 | totalSec := right - left 265 | diffSec := nowUnix - left 266 | return diffSec / totalSec 267 | } 268 | --------------------------------------------------------------------------------