├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── assets ├── demo.gif └── logo.svg ├── cmd └── lazyorg │ └── main.go ├── go.mod ├── go.sum ├── internal ├── calendar │ ├── calendar.go │ ├── day.go │ ├── event.go │ └── week.go ├── database │ └── database.go ├── ui │ └── keybindings.go └── utils │ └── utils.go └── pkg └── views ├── app-view.go ├── base-view.go ├── constants.go ├── day-view.go ├── event-view.go ├── hover-view.go ├── keybindings-view.go ├── main-view.go ├── notepad-view.go ├── popup-view.go ├── side-view.go ├── time-view.go ├── title-view.go └── week-view.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | 3 | dist/ 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | builds: 8 | - main: ./cmd/lazyorg/ 9 | goos: 10 | - linux 11 | goarch: 12 | - amd64 13 | env: 14 | - CGO_ENABLED=1 15 | ldflags: 16 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} 17 | 18 | archives: 19 | - format: tar.gz 20 | name_template: >- 21 | {{ .ProjectName }}_ 22 | {{- .Version }}_ 23 | {{- .Os }}_ 24 | {{- .Arch }} 25 | files: 26 | - LICENSE* 27 | - README* 28 | checksum: 29 | name_template: "checksums.txt" 30 | 31 | aurs: 32 | - name: lazyorg-bin 33 | homepage: "https://github.com/HubertBel/lazyorg" 34 | description: "A simple terminal-based calendar and note-taking application." 35 | license: "MIT" 36 | maintainers: 37 | - "Hubert Belanger " 38 | private_key: "{{ .Env.AUR_KEY }}" 39 | git_url: "ssh://aur@aur.archlinux.org/lazyorg-bin.git" 40 | provides: 41 | - lazyorg 42 | conflicts: 43 | - lazyorg 44 | depends: 45 | - glibc 46 | - sqlite 47 | package: |- 48 | install -Dm755 "./lazyorg" "${pkgdir}/usr/bin/lazyorg" 49 | 50 | if [ -f "./LICENSE" ]; then 51 | install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/lazyorg/LICENSE" 52 | fi 53 | if [ -f "./README.md" ]; then 54 | install -Dm644 "./README.md" "${pkgdir}/usr/share/doc/lazyorg/README.md" 55 | fi 56 | commit_author: 57 | name: HubertBel 58 | email: hubertolino@icloud.com 59 | commit_msg_template: "Update to version {{ .Version }}" 60 | 61 | changelog: 62 | sort: asc 63 | filters: 64 | exclude: 65 | - "^docs:" 66 | - "^test:" 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hubert Belanger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Lazyorg 3 |

4 | 5 |

6 | Lazyorg Logo 7 |

8 | 9 |
10 | A simple terminal-based calendar and note-taking application. 11 |
12 | 13 |
14 | 15 |

16 | Lazyorg Demo 17 |

18 | 19 | 20 | ## Features 21 | - 📅 Terminal-based calendar interface 22 | - ✨ Event creation and management 23 | - 📝 Integrated simple notepad 24 | - ⌨️ Vim-style keybindings 25 | 26 | ## Installation 27 | 28 | ### Prerequisites 29 | - Go 1.23 or higher 30 | 31 | ### Arch 32 | ```bash 33 | yay -S lazyorg-bin 34 | ``` 35 | 36 | ### Docker Image 37 | 38 | ```bash 39 | docker pull defnotgustavom/lazyorg 40 | docker run -it --log-driver none --cap-drop=ALL --net none --security-opt=no-new-privileges --name lazyorg -v /usr/share/zoneinfo/Your/Location:/usr/share/zoneinfo/Your/Location:ro -e TZ=Your/Location defnotgustavom/lazyorg 41 | ``` 42 | Switch **Your/location** to your current location. Use ```timedatectl list-timezones``` to fetch a list of possible locations. 43 | 44 | To rerun the container: 45 | ```bash 46 | docker start -ai lazyorg 47 | ``` 48 | ### Binary Installation 49 | Download pre-compiled binary from the latest release. 50 | MacOS and Windows have not been tested yet. 51 | 52 | ### From Source 53 | ```bash 54 | git clone https://github.com/HubertBel/lazyorg.git 55 | cd lazyorg 56 | go build 57 | ``` 58 | 59 | ## Usage 60 | 61 | ### Navigation 62 | - `h/l` - Previous/Next day 63 | - `H/L` - Previous/Next week 64 | - `j/k` - Move time cursor down/up 65 | 66 | ### Events 67 | - `a` - Add new event 68 | - `d` - Delete current event 69 | - `D` - Delete all events with same name 70 | 71 | When creating a new event (`a`), you'll be prompted to fill in the following fields: 72 | - **Name**: Title of event 73 | - **Time**: Date and time of the event 74 | - **Location** (optional): Location of the event 75 | - **Duration**: Duration of the event in hours (0.5 is 30 minutes) 76 | - **Frequency**: The frequency of the event in days, by default 7 or once a week 77 | - **Occurence**: The number of occurence of the event, by default 1 78 | - **Description** (optional): Additional notes or details about the event 79 | 80 | ### View Controls 81 | - `Ctrl+s` - Show/Hide side view 82 | - `Ctrl+n` - Open/Close notepad 83 | - `Ctrl+r` - Clear notepad content 84 | - `?` - Toggle help menu 85 | 86 | ### Global 87 | - `Ctrl+c` - Quit 88 | 89 | ## Configuration 90 | 91 | Configuration file will come in future releases. For now, when you open the app for the first time, the database is created at `~/.local/share/lazyorg/data.db` in case you want to do a backup. 92 | 93 | ## Contributing 94 | Please feel free to submit a Pull Request! 95 | 96 | 1. Fork the Project 97 | 2. Create your Feature Branch (`git checkout -b feature/NewFeature`) 98 | 3. Commit your Changes (`git commit -m 'Add some NewFeature'`) 99 | 4. Push to the Branch (`git push origin feature/NewFeature`) 100 | 5. Open a Pull Request 101 | 102 | ## Acknowledgments 103 | - Inspired by [lazygit](https://github.com/jesseduffield/lazygit) 104 | - Built with [gocui](https://github.com/jroimartin/gocui) TUI framework 105 | - Thanks to _defnotgustavom_ for the [docker image](https://hub.docker.com/r/defnotgustavom/lazyorg) 106 | - Thanks to [zeckrust](https://github.com/zeckrust) for the logo 107 | 108 | ## Roadmap 109 | 110 | - [ ] Time range modification 111 | - [ ] CLI help 112 | - [ ] Undo/Redo 113 | - [ ] Configuration file 114 | - [ ] Synchronization between devices 115 | - [ ] Import calendar from other apps (Google Calendar, Outlook) 116 | 117 | ## Buy me a coffee 118 | Buy Me A Coffee 119 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubertBel/lazyorg/e76a51ae023e4906d87fce38c9cda87633b7072d/assets/demo.gif -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /cmd/lazyorg/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/HubertBel/lazyorg/internal/database" 9 | "github.com/HubertBel/lazyorg/internal/ui" 10 | "github.com/HubertBel/lazyorg/pkg/views" 11 | "github.com/jroimartin/gocui" 12 | ) 13 | 14 | func main() { 15 | homeDir, err := os.UserHomeDir() 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | dbDirPath := filepath.Join(homeDir, ".local", "share", "lazyorg") 20 | dbFilePath := filepath.Join(dbDirPath, "data.db") 21 | 22 | err = os.MkdirAll(dbDirPath, 0755) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | database := &database.Database{} 28 | err = database.InitDatabase(dbFilePath) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer database.CloseDatabase() 33 | 34 | g, err := gocui.NewGui(gocui.Output256) 35 | if err != nil { 36 | log.Panicln(err) 37 | } 38 | defer g.Close() 39 | 40 | av := views.NewAppView(g, database) 41 | g.SetManager(av) 42 | 43 | if err := ui.InitKeybindings(g, av); err != nil { 44 | log.Panicln(err) 45 | } 46 | 47 | if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { 48 | log.Panicln(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/HubertBel/lazyorg 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/j-04/gocui-component v0.0.0-20190406233618-9b1c71353c96 7 | github.com/jroimartin/gocui v0.5.0 8 | github.com/mattn/go-sqlite3 v1.14.23 9 | github.com/nsf/termbox-go v1.1.1 10 | github.com/wk8/go-ordered-map/v2 v2.1.8 11 | ) 12 | 13 | require ( 14 | github.com/bahlo/generic-list-go v0.2.0 // indirect 15 | github.com/buger/jsonparser v1.1.1 // indirect 16 | github.com/mailru/easyjson v0.7.7 // indirect 17 | github.com/mattn/go-runewidth v0.0.9 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= 2 | github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= 3 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 4 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/j-04/gocui-component v0.0.0-20190406233618-9b1c71353c96 h1:c4NslfBnajkiwn0ROavKllbF3EcWKi27+SckSI1kbU4= 8 | github.com/j-04/gocui-component v0.0.0-20190406233618-9b1c71353c96/go.mod h1:ugru4j/uix8l/Baxa/+bsqwMl86AD9bDu7bK/VLXHCM= 9 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 10 | github.com/jroimartin/gocui v0.5.0 h1:DCZc97zY9dMnHXJSJLLmx9VqiEnAj0yh0eTNpuEtG/4= 11 | github.com/jroimartin/gocui v0.5.0/go.mod h1:l7Hz8DoYoL6NoYnlnaX6XCNR62G7J5FfSW5jEogzaxE= 12 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 13 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 14 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 15 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 16 | github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= 17 | github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 18 | github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= 19 | github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= 20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 23 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 24 | github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= 25 | github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 29 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | -------------------------------------------------------------------------------- /internal/calendar/calendar.go: -------------------------------------------------------------------------------- 1 | package calendar 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | type Calendar struct { 9 | CurrentDay *Day 10 | CurrentWeek *Week 11 | } 12 | 13 | func NewCalendar(currentDay *Day) *Calendar { 14 | c := &Calendar{CurrentDay: currentDay} 15 | 16 | c.CurrentWeek = NewWeek() 17 | c.UpdateWeek() 18 | 19 | return c 20 | } 21 | 22 | func (c *Calendar) setWeekLimits() { 23 | c.RoundTime() 24 | d := c.CurrentDay.Date 25 | 26 | diffToSunday := d.Weekday() 27 | diffToSaturday := 6 - d.Weekday() 28 | 29 | c.CurrentWeek.StartDate = d.AddDate(0, 0, -int(diffToSunday)) 30 | c.CurrentWeek.EndDate = d.AddDate(0, 0, int(diffToSaturday)) 31 | } 32 | 33 | func (c *Calendar) FormatWeekBody() string { 34 | startDay := c.CurrentWeek.StartDate 35 | endDay := c.CurrentWeek.EndDate 36 | month := endDay.Month().String() 37 | 38 | return month + " " + strconv.Itoa(startDay.Day()) + " to " + strconv.Itoa(endDay.Day()) 39 | } 40 | 41 | func (c *Calendar) UpdateWeek() { 42 | c.setWeekLimits() 43 | 44 | for i := range c.CurrentWeek.Days { 45 | d := c.CurrentWeek.StartDate.AddDate(0, 0, i) 46 | c.CurrentWeek.Days[i].Date = d 47 | } 48 | } 49 | 50 | func (c *Calendar) RoundTime() { 51 | min := c.CurrentDay.Date.Minute() 52 | 53 | if min >= 0 && min <= 14 { 54 | c.CurrentDay.Date = c.CurrentDay.Date.Add(time.Minute * time.Duration(-min)) 55 | } else if min >= 14 && min <= 44 { 56 | diff := 30 - min 57 | c.CurrentDay.Date = c.CurrentDay.Date.Add(time.Minute * time.Duration(diff)) 58 | } else { 59 | diff := 60 - min 60 | c.CurrentDay.Date = c.CurrentDay.Date.Add(time.Minute * time.Duration(diff)) 61 | } 62 | } 63 | 64 | func (c *Calendar) JumpToToday() { 65 | now := time.Now() 66 | c.CurrentDay.Date = time.Date(now.Year(), now.Month(), now.Day(), c.CurrentDay.Date.Hour(), c.CurrentDay.Date.Minute(), 0, 0, now.Location()) 67 | c.UpdateWeek() 68 | } 69 | 70 | func (c *Calendar) UpdateToNextWeek() { 71 | c.CurrentDay.Date = c.CurrentDay.Date.AddDate(0, 0, 7) 72 | c.UpdateWeek() 73 | } 74 | 75 | func (c *Calendar) UpdateToPrevWeek() { 76 | c.CurrentDay.Date = c.CurrentDay.Date.AddDate(0, 0, -7) 77 | c.UpdateWeek() 78 | } 79 | 80 | func (c *Calendar) UpdateToNextDay() { 81 | c.CurrentDay.Date = c.CurrentDay.Date.AddDate(0, 0, 1) 82 | c.UpdateWeek() 83 | } 84 | 85 | func (c *Calendar) UpdateToPrevDay() { 86 | c.CurrentDay.Date = c.CurrentDay.Date.AddDate(0, 0, -1) 87 | c.UpdateWeek() 88 | } 89 | 90 | func (c *Calendar) UpdateToNextTime() { 91 | c.CurrentDay.Date = c.CurrentDay.Date.Add(time.Minute * time.Duration(30)) 92 | c.UpdateWeek() 93 | } 94 | 95 | func (c *Calendar) UpdateToPrevTime() { 96 | c.CurrentDay.Date = c.CurrentDay.Date.Add(time.Minute * time.Duration(-30)) 97 | c.UpdateWeek() 98 | } 99 | 100 | func (c *Calendar) GetDayFromTime(time time.Time) *Day { 101 | for _, v := range c.CurrentWeek.Days { 102 | vYear, vMonth, vDay := v.Date.Date() 103 | tYear, tMonth, tDay := time.Date() 104 | if vYear == tYear && vMonth == tMonth && vDay == tDay { 105 | return v 106 | } 107 | } 108 | return &Day{} 109 | } 110 | -------------------------------------------------------------------------------- /internal/calendar/day.go: -------------------------------------------------------------------------------- 1 | package calendar 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/HubertBel/lazyorg/internal/utils" 11 | ) 12 | 13 | type Day struct { 14 | Date time.Time 15 | Events []*Event 16 | } 17 | 18 | func NewDay(date time.Time) *Day { 19 | return &Day{Date: date} 20 | } 21 | 22 | func (d *Day) FormatTitle() string { 23 | return d.Date.Weekday().String() + "-" + strconv.Itoa(d.Date.Day()) 24 | } 25 | 26 | func (d *Day) FormatTimeAndHour() string { 27 | return fmt.Sprintf("%s %d | %s", d.Date.Month().String(), d.Date.Day(), utils.FormatHourFromTime(d.Date)) 28 | } 29 | 30 | func (d *Day) FormatBody() string { 31 | var sb strings.Builder 32 | 33 | sb.WriteString("\n") 34 | sb.WriteString("\nEvents :\n") 35 | sb.WriteString( "---------\n") 36 | for _, v := range d.Events { 37 | s := "-> " + v.FormatTimeAndName() + "\n" 38 | sb.WriteString(s) 39 | } 40 | 41 | return sb.String() 42 | } 43 | 44 | func (d *Day) SortEventsByTime() { 45 | sort.Slice(d.Events, func(i, j int) bool { 46 | return d.Events[i].Time.Before(d.Events[j].Time) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /internal/calendar/event.go: -------------------------------------------------------------------------------- 1 | package calendar 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/HubertBel/lazyorg/internal/utils" 9 | ) 10 | 11 | type Event struct { 12 | Id int 13 | Name string 14 | Description string 15 | Location string 16 | Time time.Time 17 | DurationHour float64 18 | FrequencyDay int 19 | Occurence int 20 | } 21 | 22 | func NewEvent(name, description, location string, time time.Time, duration float64, frequency, occurence int) *Event { 23 | return &Event{Name: name, Description: description, Location: location, Time: time, DurationHour: duration, FrequencyDay: frequency, Occurence: occurence} 24 | } 25 | 26 | func (e *Event) FormatTimeAndName() string { 27 | return fmt.Sprintf("%s | %s", e.FormatDurationTime(), e.Name) 28 | } 29 | 30 | func (e *Event) FormatDurationTime() string { 31 | startTimeString := utils.FormatHourFromTime(e.Time) 32 | 33 | duration := time.Duration(e.DurationHour * float64(time.Hour)) 34 | endTime := e.Time.Add(duration) 35 | endTimeString := utils.FormatHourFromTime(endTime) 36 | 37 | return fmt.Sprintf("%s-%s", startTimeString, endTimeString) 38 | } 39 | 40 | func (e *Event) FormatBody() string { 41 | var sb strings.Builder 42 | 43 | sb.WriteString("\n") 44 | sb.WriteString(fmt.Sprintf("\n%s | %s\n", e.FormatDurationTime(), e.Location)) 45 | sb.WriteString("\nDescription :\n") 46 | sb.WriteString( "--------------\n") 47 | sb.WriteString(e.Description) 48 | 49 | return sb.String() 50 | } 51 | 52 | func (e Event) GetReccuringEvents() []Event { 53 | 54 | var events []Event 55 | f := e.FrequencyDay 56 | initTime := e.Time 57 | 58 | for i := range e.Occurence { 59 | e.Time = initTime.AddDate(0, 0, i*f) 60 | events = append(events, e) 61 | } 62 | 63 | return events 64 | } 65 | -------------------------------------------------------------------------------- /internal/calendar/week.go: -------------------------------------------------------------------------------- 1 | package calendar 2 | 3 | import "time" 4 | 5 | type Week struct { 6 | StartDate time.Time 7 | EndDate time.Time 8 | Days []*Day 9 | } 10 | 11 | func NewWeek() *Week { 12 | return &Week{ 13 | Days: []*Day { 14 | NewDay(time.Time{}), 15 | NewDay(time.Time{}), 16 | NewDay(time.Time{}), 17 | NewDay(time.Time{}), 18 | NewDay(time.Time{}), 19 | NewDay(time.Time{}), 20 | NewDay(time.Time{}), 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | 7 | "github.com/HubertBel/lazyorg/internal/calendar" 8 | "github.com/HubertBel/lazyorg/internal/utils" 9 | _ "github.com/mattn/go-sqlite3" 10 | ) 11 | 12 | type Database struct { 13 | db *sql.DB 14 | } 15 | 16 | func (database *Database) InitDatabase(path string) error { 17 | db, err := sql.Open("sqlite3", path) 18 | if err != nil { 19 | return err 20 | } 21 | database.db = db 22 | 23 | return database.createTables() 24 | } 25 | 26 | func (database *Database) createTables() error { 27 | _, err := database.db.Exec(` 28 | CREATE TABLE IF NOT EXISTS events ( 29 | id INTEGER NOT NULL PRIMARY KEY, 30 | name TEXT NOT NULL, 31 | description TEXT, 32 | location TEXT, 33 | time DATETIME NOT NULL, 34 | duration REAL NOT NULL, 35 | frequency INTEGER, 36 | occurence INTEGER 37 | )`) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | _, err = database.db.Exec(` 43 | CREATE TABLE IF NOT EXISTS notes ( 44 | id INTEGER NOT NULL PRIMARY KEY, 45 | content TEXT NOT NULL, 46 | updated_at DATETIME NOT NULL 47 | )`) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (database *Database) AddEvent(event calendar.Event) (int, error) { 56 | result, err := database.db.Exec(` 57 | INSERT INTO events ( 58 | name, description, location, time, duration, frequency, occurence 59 | ) VALUES (?, ?, ?, ?, ?, ?, ?)`, 60 | event.Name, 61 | event.Description, 62 | event.Location, 63 | event.Time, 64 | event.DurationHour, 65 | event.FrequencyDay, 66 | event.Occurence, 67 | ) 68 | if err != nil { 69 | return -1, err 70 | } 71 | 72 | id, err := result.LastInsertId() 73 | if err != nil { 74 | return -1, err 75 | } 76 | 77 | return int(id), err 78 | } 79 | 80 | func (database *Database) GetEventById(id int) (*calendar.Event, error) { 81 | rows, err := database.db.Query(` 82 | SELECT * FROM events WHERE id = ?`, 83 | id, 84 | ) 85 | if err != nil { 86 | return nil, err 87 | } 88 | defer rows.Close() 89 | 90 | if rows.Next() { 91 | var event calendar.Event 92 | if err := rows.Scan( 93 | &event.Id, 94 | &event.Name, 95 | &event.Description, 96 | &event.Location, 97 | &event.Time, 98 | &event.DurationHour, 99 | &event.FrequencyDay, 100 | &event.Occurence, 101 | ); err != nil { 102 | return nil, err 103 | } 104 | return &event, nil 105 | } 106 | 107 | return nil, nil 108 | } 109 | 110 | func (database *Database) GetEventsByDate(date time.Time) ([]*calendar.Event, error) { 111 | formattedDate := utils.FormatDate(date) 112 | 113 | rows, err := database.db.Query(` 114 | SELECT * FROM events WHERE date(time) = ?`, 115 | formattedDate, 116 | ) 117 | if err != nil { 118 | return nil, err 119 | } 120 | defer rows.Close() 121 | 122 | var events []*calendar.Event 123 | for rows.Next() { 124 | var event calendar.Event 125 | 126 | if err := rows.Scan( 127 | &event.Id, 128 | &event.Name, 129 | &event.Description, 130 | &event.Location, 131 | &event.Time, 132 | &event.DurationHour, 133 | &event.FrequencyDay, 134 | &event.Occurence, 135 | ); err != nil { 136 | return nil, err 137 | } 138 | 139 | events = append(events, &event) 140 | } 141 | 142 | return events, nil 143 | } 144 | 145 | func (database *Database) DeleteEventById(id int) error { 146 | _, err := database.db.Exec("DELETE FROM events WHERE id = ?", id) 147 | return err 148 | } 149 | 150 | func (database *Database) DeleteEventsByName(name string) error { 151 | _, err := database.db.Exec("DELETE FROM events WHERE name = ?", name) 152 | return err 153 | } 154 | 155 | func (database *Database) UpdateEventById(id int, event *calendar.Event) error { 156 | _, err := database.db.Exec( 157 | `UPDATE events SET 158 | name = ?, 159 | description = ?, 160 | location = ?, 161 | time = ?, 162 | duration = ?, 163 | frequency = ?, 164 | occurence = ? 165 | WHERE id = ?`, 166 | event.Name, 167 | event.Description, 168 | event.Location, 169 | event.Time, 170 | event.DurationHour, 171 | event.FrequencyDay, 172 | event.Occurence, 173 | id, 174 | ) 175 | 176 | return err 177 | } 178 | 179 | func (database *Database) UpdateEventByName(name string) error { 180 | return nil 181 | } 182 | 183 | func (database *Database) SaveNote(content string) error { 184 | _, err := database.db.Exec("DELETE FROM notes") 185 | if err != nil { 186 | return err 187 | } 188 | 189 | _, err = database.db.Exec(`INSERT INTO notes ( 190 | content, updated_at 191 | ) VALUES (?, datetime('now'))`, content) 192 | 193 | return err 194 | } 195 | 196 | func (database *Database) GetLatestNote() (string, error) { 197 | var content string 198 | err := database.db.QueryRow( 199 | "SELECT content FROM notes ORDER BY updated_at DESC LIMIT 1", 200 | ).Scan(&content) 201 | 202 | return content, err 203 | } 204 | 205 | func (database *Database) CloseDatabase() error { 206 | if database.db == nil { 207 | return nil 208 | } 209 | 210 | return database.db.Close() 211 | } 212 | -------------------------------------------------------------------------------- /internal/ui/keybindings.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/HubertBel/lazyorg/pkg/views" 5 | "github.com/jroimartin/gocui" 6 | ) 7 | 8 | type Keybind struct { 9 | key interface{} 10 | handler func(*gocui.Gui, *gocui.View) error 11 | } 12 | 13 | func InitKeybindings(g *gocui.Gui, av *views.AppView) error { 14 | g.InputEsc = true 15 | 16 | if err := initMainKeybindings(g, av); err != nil { 17 | return err 18 | } 19 | if err := initNotepadKeybindings(g, av); err != nil { 20 | return err 21 | } 22 | if err := initHelpKeybindings(g, av); err != nil { 23 | return err 24 | } 25 | 26 | return nil 27 | } 28 | 29 | func initMainKeybindings(g *gocui.Gui, av *views.AppView) error { 30 | mainKeybindings := []Keybind{ 31 | {'a', func(g *gocui.Gui, v *gocui.View) error { return av.ShowNewEventPopup(g) }}, 32 | {'e', func(g *gocui.Gui, v *gocui.View) error { return av.ShowEditEventPopup(g) }}, 33 | {'h', func(g *gocui.Gui, v *gocui.View) error { av.UpdateToPrevDay(g); return nil }}, 34 | {'l', func(g *gocui.Gui, v *gocui.View) error { av.UpdateToNextDay(g); return nil }}, 35 | {'j', func(g *gocui.Gui, v *gocui.View) error { av.UpdateToNextTime(g); return nil }}, 36 | {'k', func(g *gocui.Gui, v *gocui.View) error { av.UpdateToPrevTime(g); return nil }}, 37 | {gocui.KeyArrowLeft, func(g *gocui.Gui, v *gocui.View) error { av.UpdateToPrevDay(g); return nil }}, 38 | {gocui.KeyArrowRight, func(g *gocui.Gui, v *gocui.View) error { av.UpdateToNextDay(g); return nil }}, 39 | {gocui.KeyArrowDown, func(g *gocui.Gui, v *gocui.View) error { av.UpdateToNextTime(g); return nil }}, 40 | {gocui.KeyArrowUp, func(g *gocui.Gui, v *gocui.View) error { av.UpdateToPrevTime(g); return nil }}, 41 | {'T', func(g *gocui.Gui, v *gocui.View) error { av.JumpToToday(); return nil }}, 42 | {'H', func(g *gocui.Gui, v *gocui.View) error { av.UpdateToPrevWeek(); return nil }}, 43 | {'L', func(g *gocui.Gui, v *gocui.View) error { av.UpdateToNextWeek(); return nil }}, 44 | {'d', func(g *gocui.Gui, v *gocui.View) error { av.DeleteEvent(g); return nil }}, 45 | {'D', func(g *gocui.Gui, v *gocui.View) error { av.DeleteEvents(g); return nil }}, 46 | {gocui.KeyCtrlN, func(g *gocui.Gui, v *gocui.View) error { return av.ChangeToNotepadView(g) }}, 47 | {gocui.KeyCtrlS, func(g *gocui.Gui, v *gocui.View) error { return av.ShowOrHideSideView(g) }}, 48 | {'?', func(g *gocui.Gui, v *gocui.View) error { return av.ShowKeybinds(g) }}, 49 | {'q', func(g *gocui.Gui, v *gocui.View) error { return quit(g, v) }}, 50 | } 51 | for _, viewName := range views.WeekdayNames { 52 | for _, kb := range mainKeybindings { 53 | if err := g.SetKeybinding(viewName, kb.key, gocui.ModNone, kb.handler); err != nil { 54 | return err 55 | } 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func initNotepadKeybindings(g *gocui.Gui, av *views.AppView) error { 63 | notepadKeybindings := []Keybind{ 64 | {gocui.KeyCtrlR, func(g *gocui.Gui, v *gocui.View) error { return av.ClearNotepadContent(g) }}, 65 | {gocui.KeyCtrlN, func(g *gocui.Gui, v *gocui.View) error { return av.ReturnToMainView(g) }}, 66 | {gocui.KeyEsc, func(g *gocui.Gui, v *gocui.View) error { return av.ReturnToMainView(g) }}, 67 | } 68 | for _, kb := range notepadKeybindings { 69 | if err := g.SetKeybinding("notepad", kb.key, gocui.ModNone, kb.handler); err != nil { 70 | return err 71 | } 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func initHelpKeybindings(g *gocui.Gui, av *views.AppView) error { 78 | notepadKeybindings := []Keybind{ 79 | {gocui.KeyEsc, func(g *gocui.Gui, v *gocui.View) error { return av.ShowKeybinds(g) }}, 80 | {'?', func(g *gocui.Gui, v *gocui.View) error { return av.ShowKeybinds(g) }}, 81 | {'q', func(g *gocui.Gui, v *gocui.View) error { return quit(g, v) }}, 82 | } 83 | for _, kb := range notepadKeybindings { 84 | if err := g.SetKeybinding("keybinds", kb.key, gocui.ModNone, kb.handler); err != nil { 85 | return err 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func quit(g *gocui.Gui, v *gocui.View) error { 93 | return gocui.ErrQuit 94 | } 95 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func DurationToHeight(d float64) int { 13 | return int(d * 2) 14 | } 15 | 16 | func FormatDate(t time.Time) string { 17 | return fmt.Sprintf("%04d-%02d-%02d", t.Year(), t.Month(), t.Day()) 18 | } 19 | 20 | func FormatHourFromTime(t time.Time) string { 21 | return fmt.Sprintf("%02d:%02d", t.Hour(), t.Minute()) 22 | } 23 | 24 | func FormatHour(hour, minute int) string { 25 | return fmt.Sprintf("%02d:%02d", hour, minute) 26 | } 27 | 28 | func TimeToPosition(t time.Time, s string) int { 29 | time := FormatHourFromTime(t) 30 | lines := strings.Split(s, "\n") 31 | 32 | for i, v := range lines { 33 | if strings.Contains(v, time) { 34 | return i 35 | } 36 | } 37 | 38 | return -1 39 | } 40 | 41 | func ValidateTime(value string) bool { 42 | regex := regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`) 43 | 44 | if !regex.MatchString(value) { 45 | return false 46 | } 47 | 48 | parts := strings.Split(value, " ") 49 | dateParts := strings.Split(parts[0], "-") 50 | timeParts := strings.Split(parts[1], ":") 51 | 52 | year, err := strconv.Atoi(dateParts[0]) 53 | if err != nil || year <= 0 { 54 | return false 55 | } 56 | 57 | month, err := strconv.Atoi(dateParts[1]) 58 | if err != nil || month <= 0 || month > 12 { 59 | return false 60 | } 61 | 62 | day, err := strconv.Atoi(dateParts[2]) 63 | if err != nil || day <= 1 || day > 31 { 64 | return false 65 | } 66 | 67 | hours, err := strconv.Atoi(timeParts[0]) 68 | if err != nil || hours < 0 || hours > 23 { 69 | return false 70 | } 71 | 72 | minutes, err := strconv.Atoi(timeParts[1]) 73 | if err != nil || (minutes != 0 && minutes != 30) { 74 | return false 75 | } 76 | 77 | _, err = time.Parse("2006-01-02 15:04", value) 78 | return err == nil 79 | } 80 | 81 | func ValidateName(value string) bool { 82 | if value == "" { 83 | return false 84 | } 85 | 86 | return true 87 | } 88 | 89 | func ValidateNumber(value string) bool { 90 | n, err := strconv.Atoi(value) 91 | if err != nil { 92 | return false 93 | } 94 | 95 | if n <= 0 { 96 | return false 97 | } 98 | 99 | return true 100 | } 101 | 102 | func ValidateDuration(value string) bool { 103 | duration, err := strconv.ParseFloat(value, 64) 104 | if err != nil { 105 | return false 106 | } 107 | 108 | if duration <= 0.0 { 109 | return false 110 | } 111 | 112 | if math.Mod(duration, 0.5) != 0 { 113 | return false 114 | } 115 | 116 | return true 117 | } 118 | -------------------------------------------------------------------------------- /pkg/views/app-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/HubertBel/lazyorg/internal/calendar" 7 | "github.com/HubertBel/lazyorg/internal/database" 8 | "github.com/HubertBel/lazyorg/internal/utils" 9 | "github.com/jroimartin/gocui" 10 | "github.com/nsf/termbox-go" 11 | ) 12 | 13 | var ( 14 | MainViewWidthRatio = 0.8 15 | SideViewWidthRatio = 0.2 16 | ) 17 | 18 | type AppView struct { 19 | *BaseView 20 | 21 | Database *database.Database 22 | Calendar *calendar.Calendar 23 | } 24 | 25 | func NewAppView(g *gocui.Gui, db *database.Database) *AppView { 26 | now := time.Now() 27 | t := time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, now.Location()) 28 | 29 | c := calendar.NewCalendar(calendar.NewDay(t)) 30 | 31 | av := &AppView{ 32 | BaseView: NewBaseView("app"), 33 | Database: db, 34 | Calendar: c, 35 | } 36 | 37 | av.AddChild("title", NewTitleView(c)) 38 | av.AddChild("popup", NewEvenPopup(g, c, db)) 39 | av.AddChild("main", NewMainView(c)) 40 | av.AddChild("side", NewSideView(c, db)) 41 | av.AddChild("keybinds", NewKeybindsView()) 42 | 43 | return av 44 | } 45 | 46 | func (av *AppView) Layout(g *gocui.Gui) error { 47 | return av.Update(g) 48 | } 49 | 50 | func (av *AppView) Update(g *gocui.Gui) error { 51 | maxX, maxY := g.Size() 52 | av.SetProperties(0, 1, maxX-1, maxY-1) 53 | 54 | v, err := g.SetView( 55 | av.Name, 56 | av.X, 57 | av.Y, 58 | av.X+av.W, 59 | av.Y+av.H, 60 | ) 61 | if err != nil { 62 | if err != gocui.ErrUnknownView { 63 | return err 64 | } 65 | v.Frame = false 66 | } 67 | 68 | if err = av.updateEventsFromDatabase(); err != nil { 69 | return err 70 | } 71 | 72 | av.updateChildViewProperties() 73 | 74 | if err = av.UpdateChildren(g); err != nil { 75 | return err 76 | } 77 | 78 | if err = av.updateCurrentView(g); err != nil { 79 | return err 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func (av *AppView) updateEventsFromDatabase() error { 86 | for _, v := range av.Calendar.CurrentWeek.Days { 87 | clear(v.Events) 88 | 89 | var err error 90 | events, err := av.Database.GetEventsByDate(v.Date) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | v.Events = events 96 | v.SortEventsByTime() 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func (av *AppView) ShowOrHideSideView(g *gocui.Gui) error { 103 | if sideView, ok := av.GetChild("side"); ok { 104 | if err := sideView.ClearChildren(g); err != nil { 105 | return err 106 | } 107 | SideViewWidthRatio = 0.0 108 | MainViewWidthRatio = 1.0 109 | 110 | av.children.Delete("side") 111 | return g.DeleteView("side") 112 | } 113 | 114 | SideViewWidthRatio = 0.2 115 | MainViewWidthRatio = 0.8 116 | 117 | av.AddChild("side", NewSideView(av.Calendar, av.Database)) 118 | 119 | return nil 120 | } 121 | 122 | func (av *AppView) JumpToToday() { 123 | av.Calendar.JumpToToday() 124 | } 125 | 126 | func (av *AppView) UpdateToNextWeek() { 127 | av.Calendar.UpdateToNextWeek() 128 | } 129 | 130 | func (av *AppView) UpdateToPrevWeek() { 131 | av.Calendar.UpdateToPrevWeek() 132 | } 133 | 134 | func (av *AppView) UpdateToNextDay(g *gocui.Gui) { 135 | av.Calendar.UpdateToNextDay() 136 | av.updateCurrentView(g) 137 | } 138 | 139 | func (av *AppView) UpdateToPrevDay(g *gocui.Gui) { 140 | av.Calendar.UpdateToPrevDay() 141 | av.updateCurrentView(g) 142 | } 143 | 144 | func (av *AppView) UpdateToNextTime(g *gocui.Gui) { 145 | _, height := g.CurrentView().Size() 146 | if _, y := g.CurrentView().Cursor(); y < height-1 { 147 | av.Calendar.UpdateToNextTime() 148 | } 149 | } 150 | 151 | func (av *AppView) UpdateToPrevTime(g *gocui.Gui) { 152 | if _, y := g.CurrentView().Cursor(); y > 0 { 153 | av.Calendar.UpdateToPrevTime() 154 | } 155 | } 156 | 157 | func (av *AppView) ChangeToNotepadView(g *gocui.Gui) error { 158 | _, err := g.SetCurrentView("notepad") 159 | if err != nil { 160 | return err 161 | } 162 | if view, ok := av.FindChildView("notepad"); ok { 163 | if notepadView, ok := view.(*NotepadView); ok { 164 | notepadView.IsActive = true 165 | } 166 | } 167 | 168 | return nil 169 | } 170 | 171 | func (av *AppView) ClearNotepadContent(g *gocui.Gui) error { 172 | if view, ok := av.FindChildView("notepad"); ok { 173 | if notepadView, ok := view.(*NotepadView); ok { 174 | return notepadView.ClearContent(g) 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func (av *AppView) SaveNotepadContent(g *gocui.Gui) error { 182 | if view, ok := av.FindChildView("notepad"); ok { 183 | if notepadView, ok := view.(*NotepadView); ok { 184 | return notepadView.SaveContent(g) 185 | } 186 | } 187 | 188 | return nil 189 | } 190 | 191 | func (av *AppView) ReturnToMainView(g *gocui.Gui) error { 192 | if err := av.SaveNotepadContent(g); err != nil { 193 | return err 194 | } 195 | if view, ok := av.FindChildView("notepad"); ok { 196 | if notepadView, ok := view.(*NotepadView); ok { 197 | notepadView.IsActive = false 198 | } 199 | } 200 | 201 | viewName := WeekdayNames[av.Calendar.CurrentDay.Date.Weekday()] 202 | g.SetCurrentView(viewName) 203 | return av.updateCurrentView(g) 204 | } 205 | 206 | func (av *AppView) DeleteEvent(g *gocui.Gui) { 207 | _, y := g.CurrentView().Cursor() 208 | 209 | if view, ok := av.FindChildView(WeekdayNames[av.Calendar.CurrentDay.Date.Weekday()]); ok { 210 | if dayView, ok := view.(*DayView); ok { 211 | if eventView, ok := dayView.IsOnEvent(y); ok { 212 | av.Database.DeleteEventById(eventView.Event.Id) 213 | } 214 | } 215 | } 216 | } 217 | 218 | func (av *AppView) DeleteEvents(g *gocui.Gui) { 219 | _, y := g.CurrentView().Cursor() 220 | 221 | if view, ok := av.FindChildView(WeekdayNames[av.Calendar.CurrentDay.Date.Weekday()]); ok { 222 | if dayView, ok := view.(*DayView); ok { 223 | if eventView, ok := dayView.IsOnEvent(y); ok { 224 | av.Database.DeleteEventsByName(eventView.Event.Name) 225 | } 226 | } 227 | } 228 | } 229 | 230 | func (av *AppView) ShowNewEventPopup(g *gocui.Gui) error { 231 | if view, ok := av.GetChild("popup"); ok { 232 | if popupView, ok := view.(*EventPopupView); ok { 233 | view.SetProperties( 234 | av.X+(av.W-PopupWidth)/2, 235 | av.Y+(av.H-PopupHeight)/2, 236 | PopupWidth, 237 | PopupHeight, 238 | ) 239 | return popupView.ShowNewEventPopup(g) 240 | } 241 | } 242 | return nil 243 | } 244 | 245 | func (av *AppView) ShowEditEventPopup(g *gocui.Gui) error { 246 | if view, ok := av.GetChild("popup"); ok { 247 | if popupView, ok := view.(*EventPopupView); ok { 248 | view.SetProperties( 249 | av.X+(av.W-PopupWidth)/2, 250 | av.Y+(av.H-PopupHeight)/2, 251 | PopupWidth, 252 | PopupHeight, 253 | ) 254 | hoveredView := av.GetHoveredOnView(g) 255 | if eventView, ok := hoveredView.(*EventView); ok { 256 | err := popupView.ShowEditEventPopup(g, eventView) 257 | if err != nil { 258 | return err 259 | } 260 | } 261 | } 262 | } 263 | return nil 264 | } 265 | 266 | func (av *AppView) ShowKeybinds(g *gocui.Gui) error { 267 | if view, ok := av.GetChild("keybinds"); ok { 268 | if keybindsView, ok := view.(*KeybindsView); ok { 269 | if keybindsView.IsVisible { 270 | keybindsView.IsVisible = false 271 | return g.DeleteView(keybindsView.Name) 272 | } 273 | 274 | keybindsView.IsVisible = true 275 | keybindsView.SetProperties( 276 | av.X+(av.W-KeybindsWidth)/2, 277 | av.Y+(av.H-KeybindsHeight)/2, 278 | KeybindsWidth, 279 | KeybindsHeight, 280 | ) 281 | 282 | return keybindsView.Update(g) 283 | } 284 | } 285 | 286 | return nil 287 | } 288 | 289 | func (av *AppView) updateChildViewProperties() { 290 | mainViewWidth := int(float64(av.W-1) * MainViewWidthRatio) 291 | sideViewWidth := int(float64(av.W) * SideViewWidthRatio) 292 | 293 | if titleView, ok := av.GetChild("title"); ok { 294 | titleView.SetProperties( 295 | av.X+sideViewWidth+1, 296 | av.Y, 297 | mainViewWidth, 298 | TitleViewHeight, 299 | ) 300 | } 301 | 302 | if mainView, ok := av.GetChild("main"); ok { 303 | y := av.Y + TitleViewHeight + 1 304 | mainView.SetProperties( 305 | av.X+sideViewWidth+1, 306 | y, 307 | mainViewWidth, 308 | av.H-y, 309 | ) 310 | } 311 | 312 | if sideView, ok := av.GetChild("side"); ok { 313 | sideView.SetProperties( 314 | av.X, 315 | av.Y, 316 | sideViewWidth, 317 | av.H, 318 | ) 319 | } 320 | } 321 | 322 | func (av *AppView) updateCurrentView(g *gocui.Gui) error { 323 | if view, ok := av.GetChild("popup"); ok { 324 | if popupView, ok := view.(*EventPopupView); ok { 325 | if popupView.IsVisible { 326 | return nil 327 | } 328 | } 329 | } 330 | if view, ok := av.GetChild("keybinds"); ok { 331 | if keybindsView, ok := view.(*KeybindsView); ok { 332 | if keybindsView.IsVisible { 333 | g.Cursor = false 334 | g.SetCurrentView("keybinds") 335 | return nil 336 | } 337 | } 338 | } 339 | if g.CurrentView() != nil && g.CurrentView().Name() == "notepad" { 340 | return nil 341 | } 342 | 343 | g.Cursor = true 344 | if view, ok := av.FindChildView("hover"); ok { 345 | if hoverView, ok := view.(*HoverView); ok { 346 | hoverView.CurrentView = av.GetHoveredOnView(g) 347 | hoverView.Update(g) 348 | } 349 | } 350 | 351 | g.SetCurrentView(WeekdayNames[av.Calendar.CurrentDay.Date.Weekday()]) 352 | g.CurrentView().BgColor = gocui.Attribute(termbox.ColorBlack) 353 | g.CurrentView().SetCursor(1, av.GetCursorY()) 354 | 355 | return nil 356 | } 357 | 358 | func (av *AppView) GetHoveredOnView(g *gocui.Gui) View { 359 | viewName := WeekdayNames[av.Calendar.CurrentDay.Date.Weekday()] 360 | var hoveredView View 361 | 362 | if view, ok := av.FindChildView(viewName); ok { 363 | if dayView, ok := view.(*DayView); ok { 364 | if eventView, ok := dayView.IsOnEvent(av.GetCursorY()); ok { 365 | hoveredView = eventView 366 | } else { 367 | hoveredView = dayView 368 | } 369 | } 370 | } 371 | 372 | return hoveredView 373 | } 374 | 375 | func (av *AppView) GetCursorY() int { 376 | y := 0 377 | 378 | if view, ok := av.FindChildView("time"); ok { 379 | if timeView, ok := view.(*TimeView); ok { 380 | y = utils.TimeToPosition(av.Calendar.CurrentDay.Date, timeView.Body) 381 | } 382 | } 383 | 384 | return y 385 | } 386 | -------------------------------------------------------------------------------- /pkg/views/base-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/jroimartin/gocui" 5 | orderedmap "github.com/wk8/go-ordered-map/v2" 6 | ) 7 | 8 | type View interface { 9 | Update(g *gocui.Gui) error 10 | SetProperties(x, y, w, h int) 11 | GetName() string 12 | GetProperties() (int, int, int, int) 13 | AddChild(name string, child View) (View, bool) 14 | GetChild(name string) (View, bool) 15 | ClearChildren(g *gocui.Gui) error 16 | Children() *orderedmap.OrderedMap[string, View] 17 | FindChildView(name string) (View, bool) 18 | } 19 | 20 | type BaseView struct { 21 | Name string 22 | X, Y, W, H int 23 | children *orderedmap.OrderedMap[string, View] 24 | } 25 | 26 | func NewBaseView(name string) *BaseView { 27 | return &BaseView{ 28 | Name: name, 29 | children: orderedmap.New[string, View](), 30 | } 31 | } 32 | 33 | func (bv *BaseView) Update(g *gocui.Gui) error { 34 | return nil 35 | } 36 | 37 | func (bv *BaseView) SetProperties(x, y, w, h int) { 38 | bv.X, bv.Y, bv.W, bv.H = x, y, w, h 39 | } 40 | 41 | func (bv *BaseView) GetProperties() (int, int, int, int) { 42 | return bv.X, bv.Y, bv.W, bv.H 43 | } 44 | 45 | func (bv *BaseView) GetName() string { 46 | return bv.Name 47 | } 48 | 49 | func (bv *BaseView) ClearChildren(g *gocui.Gui) error { 50 | for pair := bv.children.Oldest(); pair != nil; pair = pair.Next() { 51 | if err := g.DeleteView(pair.Value.GetName()); err != gocui.ErrUnknownView { 52 | continue 53 | } else { 54 | return err 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (bv *BaseView) AddChild(name string, child View) (View, bool) { 62 | child, ok := bv.children.Set(name, child) 63 | return child, ok 64 | } 65 | 66 | func (bv *BaseView) GetChild(name string) (View, bool) { 67 | child, ok := bv.children.Get(name) 68 | return child, ok 69 | } 70 | 71 | func (bv *BaseView) Children() *orderedmap.OrderedMap[string, View] { 72 | return bv.children 73 | } 74 | 75 | func (bv *BaseView) UpdateChildren(g *gocui.Gui) error { 76 | for pair := bv.children.Oldest(); pair != nil; pair = pair.Next() { 77 | if err := pair.Value.Update(g); err != nil { 78 | return err 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func (bv *BaseView) FindChildView(name string) (View, bool) { 86 | if view, ok := bv.GetChild(name); ok { 87 | return view, ok 88 | } 89 | 90 | for pair := bv.children.Oldest(); pair != nil; pair = pair.Next() { 91 | if childView, ok := pair.Value.(View); ok { 92 | if view, ok := childView.FindChildView(name); ok { 93 | return view, ok 94 | } 95 | } 96 | } 97 | 98 | return nil, false 99 | } 100 | -------------------------------------------------------------------------------- /pkg/views/constants.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | const ( 4 | KeybindsWidth = 48 5 | KeybindsHeight = 23 6 | 7 | LabelWidth = 12 8 | FieldWidth = 20 9 | 10 | PopupWidth = LabelWidth + FieldWidth 11 | PopupHeight = 16 12 | 13 | TimeFormat = "2006-01-02 15:04" 14 | 15 | TimeViewWidth = 10 16 | 17 | TitleViewHeight = 3 18 | 19 | Padding = 1 20 | ) 21 | -------------------------------------------------------------------------------- /pkg/views/day-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/HubertBel/lazyorg/internal/calendar" 8 | "github.com/HubertBel/lazyorg/internal/utils" 9 | "github.com/jroimartin/gocui" 10 | "github.com/nsf/termbox-go" 11 | ) 12 | 13 | type DayView struct { 14 | *BaseView 15 | 16 | Day *calendar.Day 17 | TimeView *TimeView 18 | } 19 | 20 | func NewDayView(name string, d *calendar.Day, tv *TimeView) *DayView { 21 | dv := &DayView{ 22 | BaseView: NewBaseView(name), 23 | Day: d, 24 | TimeView: tv, 25 | } 26 | 27 | return dv 28 | } 29 | 30 | func (dv *DayView) Update(g *gocui.Gui) error { 31 | v, err := g.SetView( 32 | dv.Name, 33 | dv.X, 34 | dv.Y, 35 | dv.X+dv.W, 36 | dv.Y+dv.H, 37 | ) 38 | if err != nil { 39 | if err != gocui.ErrUnknownView { 40 | return err 41 | } 42 | } 43 | 44 | dv.updateBgColor(v) 45 | 46 | v.Title = dv.Day.FormatTitle() 47 | 48 | if err = dv.updateChildViewProperties(g); err != nil { 49 | return err 50 | } 51 | 52 | if err = dv.UpdateChildren(g); err != nil { 53 | return err 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (dv *DayView) updateBgColor(v *gocui.View) { 60 | if dv.Day.Date.YearDay() == time.Now().YearDay() { 61 | v.BgColor = gocui.Attribute(termbox.ColorDarkGray) 62 | } else { 63 | v.BgColor = gocui.ColorDefault 64 | } 65 | } 66 | 67 | func (dv *DayView) updateChildViewProperties(g *gocui.Gui) error { 68 | eventViews := make(map[string]*EventView) 69 | for pair := dv.children.Oldest(); pair != nil; pair = pair.Next() { 70 | if eventView, ok := pair.Value.(*EventView); ok { 71 | eventViews[eventView.GetName()] = eventView 72 | } 73 | } 74 | 75 | for _, event := range dv.Day.Events { 76 | x := dv.X + 1 77 | y := dv.Y + utils.TimeToPosition(event.Time, dv.TimeView.Body) + 1 78 | w := dv.W - 2 79 | h := utils.DurationToHeight(event.DurationHour) 80 | 81 | if (y + h) >= (dv.Y + dv.H) { 82 | newHeight := (dv.Y + dv.H) - y 83 | if newHeight <= 0 { 84 | continue 85 | } 86 | h = newHeight 87 | } 88 | if y <= dv.Y { 89 | continue 90 | } 91 | 92 | viewName := fmt.Sprintf("%s-%d", event.Name, event.Id) 93 | if existingView, exists := eventViews[viewName]; exists { 94 | existingView.X, existingView.Y, existingView.W, existingView.H = x, y, w, h 95 | existingView.Event = event 96 | delete(eventViews, viewName) 97 | } else { 98 | ev := NewEvenView(viewName, event) 99 | ev.X, ev.Y, ev.W, ev.H = x, y, w, h 100 | dv.AddChild(viewName, ev) 101 | } 102 | } 103 | 104 | for viewName := range eventViews { 105 | if err := g.DeleteView(viewName); err != nil && err != gocui.ErrUnknownView { 106 | return err 107 | } 108 | dv.children.Delete(viewName) 109 | } 110 | 111 | return nil 112 | } 113 | 114 | func (dv *DayView) IsOnEvent(y int) (*EventView, bool) { 115 | y += dv.Y + 1 116 | for pair := dv.children.Newest(); pair != nil; pair = pair.Prev() { 117 | if eventView, ok := pair.Value.(*EventView); ok { 118 | if y >= eventView.Y && y < (eventView.Y+eventView.H) { 119 | return eventView, true 120 | } 121 | } 122 | } 123 | return nil, false 124 | } 125 | -------------------------------------------------------------------------------- /pkg/views/event-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/HubertBel/lazyorg/internal/calendar" 5 | "github.com/jroimartin/gocui" 6 | ) 7 | 8 | type EventView struct { 9 | *BaseView 10 | 11 | Event *calendar.Event 12 | } 13 | 14 | func NewEvenView(name string, e *calendar.Event) *EventView { 15 | return &EventView { 16 | BaseView: NewBaseView(name), 17 | 18 | Event: e, 19 | } 20 | } 21 | 22 | func (ev *EventView) Update(g *gocui.Gui) error { 23 | v, err := g.SetView( 24 | ev.Name, 25 | ev.X, 26 | ev.Y, 27 | ev.X+ev.W, 28 | ev.Y+ev.H, 29 | ) 30 | if err != nil { 31 | if err != gocui.ErrUnknownView { 32 | return err 33 | } 34 | v.Title = ev.Event.Name 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/views/hover-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/HubertBel/lazyorg/internal/calendar" 7 | "github.com/jroimartin/gocui" 8 | ) 9 | 10 | type HoverView struct { 11 | *BaseView 12 | 13 | Calendar *calendar.Calendar 14 | CurrentView View 15 | } 16 | 17 | func NewHoverView(c *calendar.Calendar) *HoverView { 18 | hv := &HoverView{ 19 | BaseView: NewBaseView("hover"), 20 | Calendar: c, 21 | } 22 | 23 | return hv 24 | } 25 | 26 | func (hv *HoverView) Update(g *gocui.Gui) error { 27 | v, err := g.SetView( 28 | hv.Name, 29 | hv.X, 30 | hv.Y, 31 | hv.X+hv.W, 32 | hv.Y+hv.H, 33 | ) 34 | if err != nil { 35 | if err != gocui.ErrUnknownView { 36 | return err 37 | } 38 | v.Wrap = true 39 | } 40 | 41 | hv.updateTitle(v) 42 | hv.updateBody(v) 43 | 44 | return nil 45 | } 46 | 47 | func (hv *HoverView) updateTitle(v *gocui.View) { 48 | if view, ok := hv.CurrentView.(*DayView); ok { 49 | v.Title = " " + view.Day.FormatTimeAndHour() + " " 50 | } else if view, ok := hv.CurrentView.(*EventView); ok { 51 | v.Title = " " + view.Event.Name + " " 52 | } 53 | } 54 | 55 | func (hv *HoverView) updateBody(v *gocui.View) { 56 | v.Clear() 57 | if view, ok := hv.CurrentView.(*DayView); ok { 58 | v.Wrap = false 59 | v.FgColor = gocui.AttrBold | gocui.ColorYellow 60 | fmt.Fprintln(v, view.Day.FormatBody()) 61 | } else if view, ok := hv.CurrentView.(*EventView); ok { 62 | v.Wrap = true 63 | v.FgColor = gocui.AttrBold | gocui.ColorMagenta 64 | fmt.Fprintln(v, view.Event.FormatBody()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pkg/views/keybindings-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | import ( 3 | "fmt" 4 | "github.com/jroimartin/gocui" 5 | ) 6 | type KeybindsView struct { 7 | *BaseView 8 | IsVisible bool 9 | } 10 | func NewKeybindsView() *KeybindsView { 11 | return &KeybindsView{ 12 | BaseView: NewBaseView("keybinds"), 13 | IsVisible: false, 14 | } 15 | } 16 | func (kbv *KeybindsView) Update(g *gocui.Gui) error { 17 | if !kbv.IsVisible { 18 | return nil 19 | } 20 | v, err := g.SetView( 21 | kbv.Name, 22 | kbv.X, 23 | kbv.Y, 24 | kbv.X+kbv.W, 25 | kbv.Y+kbv.H, 26 | ) 27 | if err != nil { 28 | if err != gocui.ErrUnknownView { 29 | return err 30 | } 31 | v.Title = " Keybindings " 32 | } 33 | v.Clear() 34 | fmt.Fprintln(v, "") 35 | fmt.Fprintln(v, "") 36 | fmt.Fprintln(v, " Navigation:") 37 | fmt.Fprintln(v, " h/l or ←/→ - Previous/Next day") 38 | fmt.Fprintln(v, " H/L - Previous/Next week") 39 | fmt.Fprintln(v, " j/k or ↓/↑ - Move time cursor down/up") 40 | fmt.Fprintln(v, " T - Jump to today") 41 | fmt.Fprintln(v, "") 42 | fmt.Fprintln(v, " Events:") 43 | fmt.Fprintln(v, " a - Add new event") 44 | fmt.Fprintln(v, " e - Edit event") 45 | fmt.Fprintln(v, " d - Delete event") 46 | fmt.Fprintln(v, " D - Delete events with same name") 47 | fmt.Fprintln(v, "") 48 | fmt.Fprintln(v, " View Controls:") 49 | fmt.Fprintln(v, " Ctrl+s - Show/Hide side view") 50 | fmt.Fprintln(v, " Ctrl+n - Open/Close notepad") 51 | fmt.Fprintln(v, " Ctrl+r - Clear notepad content") 52 | fmt.Fprintln(v, " ? - Toggle this help") 53 | fmt.Fprintln(v, "") 54 | fmt.Fprintln(v, " Global:") 55 | fmt.Fprintln(v, " q - Quit") 56 | g.SetViewOnTop("keybinds") 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/views/main-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/HubertBel/lazyorg/internal/calendar" 5 | "github.com/HubertBel/lazyorg/internal/utils" 6 | "github.com/jroimartin/gocui" 7 | ) 8 | 9 | type MainView struct { 10 | *BaseView 11 | 12 | Calendar *calendar.Calendar 13 | } 14 | 15 | func NewMainView(c *calendar.Calendar) *MainView { 16 | mv := &MainView{ 17 | BaseView: NewBaseView("main"), 18 | Calendar: c, 19 | } 20 | 21 | tv := NewTimeView() 22 | mv.AddChild("time", tv) 23 | mv.AddChild("week", NewWeekView(c, tv)) 24 | 25 | return mv 26 | } 27 | 28 | func (mv *MainView) Update(g *gocui.Gui) error { 29 | v, err := g.SetView( 30 | mv.Name, 31 | mv.X, 32 | mv.Y, 33 | mv.X+mv.W, 34 | mv.Y+mv.H, 35 | ) 36 | if err != nil { 37 | if err != gocui.ErrUnknownView { 38 | return err 39 | } 40 | v.FgColor = gocui.AttrBold 41 | } 42 | 43 | mv.updateChildViewProperties() 44 | 45 | if err = mv.UpdateChildren(g); err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (mv *MainView) updateChildViewProperties() { 53 | if view, ok := mv.GetChild("time"); ok { 54 | if timeView, ok := view.(*TimeView); ok { 55 | y := utils.TimeToPosition(mv.Calendar.CurrentDay.Date, timeView.Body) 56 | timeView.SetCursor(y) 57 | timeView.SetProperties( 58 | mv.X+1, 59 | mv.Y+1, 60 | TimeViewWidth, 61 | mv.H-2, 62 | ) 63 | } 64 | } 65 | 66 | if weekView, ok := mv.GetChild("week"); ok { 67 | weekView.SetProperties( 68 | mv.X+TimeViewWidth+1, 69 | mv.Y, 70 | mv.W-TimeViewWidth-1, 71 | mv.H, 72 | ) 73 | } 74 | 75 | if titleView, ok := mv.GetChild("title"); ok { 76 | titleView.SetProperties( 77 | mv.X, 78 | mv.Y, 79 | mv.W, 80 | TitleViewHeight, 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/views/notepad-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/HubertBel/lazyorg/internal/calendar" 7 | "github.com/HubertBel/lazyorg/internal/database" 8 | "github.com/jroimartin/gocui" 9 | ) 10 | 11 | type NotepadView struct { 12 | *BaseView 13 | 14 | Database *database.Database 15 | IsActive bool 16 | content string 17 | } 18 | 19 | func NewNotepadView(c *calendar.Calendar, db *database.Database) *NotepadView { 20 | tv := &NotepadView{ 21 | BaseView: NewBaseView("notepad"), 22 | Database: db, 23 | IsActive: false, 24 | } 25 | 26 | content, err := db.GetLatestNote() 27 | if err == nil { 28 | tv.content = content 29 | } 30 | 31 | return tv 32 | } 33 | 34 | func (npv *NotepadView) Update(g *gocui.Gui) error { 35 | v, err := g.SetView( 36 | npv.Name, 37 | npv.X, 38 | npv.Y, 39 | npv.X+npv.W, 40 | npv.Y+npv.H, 41 | ) 42 | if err != nil { 43 | if err != gocui.ErrUnknownView { 44 | return err 45 | } 46 | v.Title = " Notepad " 47 | v.Editable = true 48 | v.FgColor = gocui.AttrBold 49 | v.Wrap = true 50 | v.Clear() 51 | fmt.Fprint(v, npv.content) 52 | } 53 | 54 | npv.SaveContent(g) 55 | 56 | return nil 57 | } 58 | 59 | func (npv *NotepadView) SaveContent(g *gocui.Gui) error { 60 | v, err := g.View(npv.Name) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return npv.Database.SaveNote(v.Buffer()) 66 | } 67 | 68 | func (npv *NotepadView) ClearContent(g *gocui.Gui) error { 69 | v, err := g.View(npv.Name) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | v.Clear() 75 | return npv.SaveContent(g) 76 | } 77 | -------------------------------------------------------------------------------- /pkg/views/popup-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/HubertBel/lazyorg/internal/calendar" 8 | "github.com/HubertBel/lazyorg/internal/database" 9 | "github.com/HubertBel/lazyorg/internal/utils" 10 | component "github.com/j-04/gocui-component" 11 | "github.com/jroimartin/gocui" 12 | ) 13 | 14 | type EventPopupView struct { 15 | *BaseView 16 | Form *component.Form 17 | Calendar *calendar.Calendar 18 | Database *database.Database 19 | 20 | IsVisible bool 21 | } 22 | 23 | func NewEvenPopup(g *gocui.Gui, c *calendar.Calendar, db *database.Database) *EventPopupView { 24 | 25 | epv := &EventPopupView{ 26 | BaseView: NewBaseView("popup"), 27 | Form: nil, 28 | Calendar: c, 29 | Database: db, 30 | IsVisible: false, 31 | } 32 | 33 | return epv 34 | } 35 | 36 | func (epv *EventPopupView) Update(g *gocui.Gui) error { 37 | return nil 38 | } 39 | 40 | func (epv *EventPopupView) NewEventForm(g *gocui.Gui, title, name, time, location, duration, frequency, occurence, description string) *component.Form { 41 | form := component.NewForm(g, title, epv.X, epv.Y, epv.W, epv.H) 42 | 43 | form.AddInputField("Name", LabelWidth, FieldWidth).SetText(name).AddValidate("Invalid name", utils.ValidateName) 44 | form.AddInputField("Time", LabelWidth, FieldWidth).SetText(time).AddValidate("Invalid time", utils.ValidateTime) 45 | form.AddInputField("Location", LabelWidth, FieldWidth).SetText(location) 46 | form.AddInputField("Duration", LabelWidth, FieldWidth).SetText(duration).AddValidate("Invalid duration", utils.ValidateDuration) 47 | form.AddInputField("Frequency", LabelWidth, FieldWidth).SetText(frequency).AddValidate("Invalid frequency", utils.ValidateNumber) 48 | form.AddInputField("Occurence", LabelWidth, FieldWidth).SetText(occurence).AddValidate("Invalid occurence", utils.ValidateNumber) 49 | form.AddInputField("Description", LabelWidth, FieldWidth).SetText(description) 50 | 51 | return form 52 | } 53 | 54 | func (epv *EventPopupView) EditEventForm(g *gocui.Gui, title, name, time, location, duration, description string) *component.Form { 55 | form := component.NewForm(g, title, epv.X, epv.Y, epv.W, epv.H) 56 | 57 | form.AddInputField("Name", LabelWidth, FieldWidth).SetText(name).AddValidate("Invalid name", utils.ValidateName) 58 | form.AddInputField("Time", LabelWidth, FieldWidth).SetText(time).AddValidate("Invalid time", utils.ValidateTime) 59 | form.AddInputField("Location", LabelWidth, FieldWidth).SetText(location) 60 | form.AddInputField("Duration", LabelWidth, FieldWidth).SetText(duration).AddValidate("Invalid duration", utils.ValidateDuration) 61 | form.AddInputField("Description", LabelWidth, FieldWidth).SetText(description) 62 | 63 | return form 64 | } 65 | 66 | func (epv *EventPopupView) ShowNewEventPopup(g *gocui.Gui) error { 67 | if epv.IsVisible { 68 | return nil 69 | } 70 | 71 | epv.Form = epv.NewEventForm(g, "New Event", "", epv.Calendar.CurrentDay.Date.Format(TimeFormat), "", "1.0", "7", "1", "") 72 | 73 | epv.addKeybind(gocui.KeyEsc, epv.Close) 74 | epv.addKeybind(gocui.KeyEnter, epv.AddEvent) 75 | 76 | epv.Form.AddButton("Add", epv.AddEvent) 77 | epv.Form.AddButton("Cancel", epv.Close) 78 | 79 | epv.Form.SetCurrentItem(0) 80 | epv.IsVisible = true 81 | epv.Form.Draw() 82 | 83 | return nil 84 | } 85 | 86 | func (epv *EventPopupView) ShowEditEventPopup(g *gocui.Gui, eventView *EventView) error { 87 | if epv.IsVisible { 88 | return nil 89 | } 90 | 91 | event := eventView.Event 92 | 93 | epv.Form = epv.EditEventForm(g, 94 | "Edit Event", 95 | event.Name, 96 | event.Time.Format(TimeFormat), 97 | event.Location, 98 | strconv.FormatFloat(event.DurationHour, 'f', -1, 64), 99 | event.Description, 100 | ) 101 | 102 | editHandler := func(g *gocui.Gui, v *gocui.View) error { 103 | return epv.EditEvent(g, v, event) 104 | } 105 | epv.addKeybind(gocui.KeyEsc, epv.Close) 106 | epv.addKeybind(gocui.KeyEnter, editHandler) 107 | 108 | epv.Form.AddButton("Edit", editHandler) 109 | epv.Form.AddButton("Cancel", epv.Close) 110 | 111 | epv.Form.SetCurrentItem(0) 112 | epv.IsVisible = true 113 | epv.Form.Draw() 114 | 115 | return nil 116 | } 117 | 118 | func (epv *EventPopupView) CreateEventFromInputs() *calendar.Event { 119 | for _, v := range epv.Form.GetInputs() { 120 | if !v.IsValid() { 121 | return nil 122 | } 123 | } 124 | 125 | name := epv.Form.GetFieldText("Name") 126 | time, _ := time.Parse(TimeFormat, epv.Form.GetFieldText("Time")) 127 | location := epv.Form.GetFieldText("Location") 128 | 129 | duration, _ := strconv.ParseFloat(epv.Form.GetFieldText("Duration"), 64) 130 | frequency, _ := strconv.Atoi(epv.Form.GetFieldText("Frequency")) 131 | occurence, _ := strconv.Atoi(epv.Form.GetFieldText("Occurence")) 132 | 133 | description := epv.Form.GetFieldText("Description") 134 | 135 | return calendar.NewEvent(name, description, location, time, duration, frequency, occurence) 136 | } 137 | 138 | func (epv *EventPopupView) AddEvent(g *gocui.Gui, v *gocui.View) error { 139 | if !epv.IsVisible { 140 | return nil 141 | } 142 | 143 | var newEvent *calendar.Event 144 | if newEvent = epv.CreateEventFromInputs(); newEvent == nil { 145 | return nil 146 | } 147 | events := newEvent.GetReccuringEvents() 148 | 149 | for _, v := range events { 150 | if _, err := epv.Database.AddEvent(v); err != nil { 151 | return err 152 | } 153 | } 154 | 155 | return epv.Close(g, v) 156 | } 157 | 158 | func (epv *EventPopupView) EditEvent(g *gocui.Gui, v *gocui.View, event *calendar.Event) error { 159 | if !epv.IsVisible { 160 | return nil 161 | } 162 | 163 | var newEvent *calendar.Event 164 | if newEvent = epv.CreateEventFromInputs(); newEvent == nil { 165 | return nil 166 | } 167 | newEvent.Id = event.Id 168 | 169 | if err := epv.Database.UpdateEventById(event.Id, newEvent); err != nil { 170 | return err 171 | } 172 | 173 | return epv.Close(g, v) 174 | } 175 | 176 | func (epv *EventPopupView) Close(g *gocui.Gui, v *gocui.View) error { 177 | epv.IsVisible = false 178 | return epv.Form.Close(g, v) 179 | } 180 | 181 | func (epv *EventPopupView) addKeybind(key interface{}, handler func(g *gocui.Gui, v *gocui.View) error) { 182 | for _, item := range epv.Form.GetItems() { 183 | item.AddHandlerOnly(key, handler) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /pkg/views/side-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/HubertBel/lazyorg/internal/calendar" 5 | "github.com/HubertBel/lazyorg/internal/database" 6 | "github.com/jroimartin/gocui" 7 | ) 8 | 9 | type SideView struct { 10 | *BaseView 11 | 12 | Calendar *calendar.Calendar 13 | } 14 | 15 | func NewSideView(c *calendar.Calendar, db *database.Database) *SideView { 16 | sv := &SideView{ 17 | BaseView: NewBaseView("side"), 18 | Calendar: c, 19 | } 20 | 21 | sv.AddChild("hover", NewHoverView(c)) 22 | sv.AddChild("notepad", NewNotepadView(c, db)) 23 | 24 | return sv 25 | } 26 | 27 | func (sv *SideView) Update(g *gocui.Gui) error { 28 | v, err := g.SetView( 29 | sv.Name, 30 | sv.X, 31 | sv.Y, 32 | sv.X+sv.W, 33 | sv.Y+sv.H, 34 | ) 35 | if err != nil { 36 | if err != gocui.ErrUnknownView { 37 | return err 38 | } 39 | v.Frame = false 40 | v.FgColor = gocui.AttrBold 41 | } 42 | 43 | sv.updateChildViewProperties() 44 | 45 | if err = sv.UpdateChildren(g); err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (sv *SideView) updateChildViewProperties() { 53 | heightHover := int(float64(sv.H)*0.5) 54 | heightNotepad := sv.H - heightHover - 2 55 | 56 | if hoverView, ok := sv.GetChild("hover"); ok { 57 | hoverView.SetProperties( 58 | sv.X, 59 | sv.Y, 60 | sv.W, 61 | heightHover, 62 | ) 63 | } 64 | 65 | if notepadView, ok := sv.GetChild("notepad"); ok { 66 | notepadView.SetProperties( 67 | sv.X, 68 | sv.Y+heightHover+1, 69 | sv.W, 70 | heightNotepad, 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pkg/views/time-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/HubertBel/lazyorg/internal/utils" 7 | "github.com/jroimartin/gocui" 8 | ) 9 | 10 | type TimeView struct { 11 | *BaseView 12 | Body string 13 | Cursor int 14 | } 15 | 16 | func NewTimeView() *TimeView { 17 | tv := &TimeView{ 18 | BaseView: NewBaseView("time"), 19 | Cursor: 0, 20 | } 21 | 22 | return tv 23 | } 24 | 25 | func (tv *TimeView) Update(g *gocui.Gui) error { 26 | v, err := g.SetView( 27 | tv.Name, 28 | tv.X, 29 | tv.Y, 30 | tv.X+tv.W, 31 | tv.Y+tv.H, 32 | ) 33 | 34 | if err != nil { 35 | if err != gocui.ErrUnknownView { 36 | return err 37 | } 38 | v.Frame = false 39 | v.FgColor = gocui.ColorGreen 40 | } 41 | 42 | tv.updateBody(v) 43 | 44 | return nil 45 | } 46 | 47 | func (tv *TimeView) updateBody(v *gocui.View) { 48 | initialTime := 12 - tv.H/4 49 | tv.Body = "" 50 | 51 | for i := range tv.H { 52 | var time string 53 | 54 | if i%2 == 0 { 55 | hour := utils.FormatHour(initialTime, 0) 56 | time = fmt.Sprintf(" %s - \n", hour) 57 | } else { 58 | hour := utils.FormatHour(initialTime, 30) 59 | time = fmt.Sprintf(" %s \n", hour) 60 | initialTime++ 61 | } 62 | 63 | if i == tv.Cursor { 64 | runes := []rune(time) 65 | runes[0] = '>' 66 | time = string(runes) 67 | } 68 | 69 | tv.Body += time 70 | } 71 | 72 | v.Clear() 73 | fmt.Fprintln(v, tv.Body) 74 | } 75 | 76 | func (tv *TimeView) SetCursor(y int) { 77 | tv.Cursor = y 78 | } 79 | -------------------------------------------------------------------------------- /pkg/views/title-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/HubertBel/lazyorg/internal/calendar" 8 | "github.com/HubertBel/lazyorg/internal/utils" 9 | "github.com/jroimartin/gocui" 10 | ) 11 | 12 | type TitleView struct { 13 | *BaseView 14 | 15 | Calendar *calendar.Calendar 16 | } 17 | 18 | func NewTitleView(c *calendar.Calendar) *TitleView { 19 | tv := &TitleView{ 20 | BaseView: NewBaseView("title"), 21 | Calendar: c, 22 | } 23 | 24 | return tv 25 | } 26 | 27 | func (tv *TitleView) Update(g *gocui.Gui) error { 28 | v, err := g.SetView( 29 | tv.Name, 30 | tv.X, 31 | tv.Y, 32 | tv.X+tv.W, 33 | tv.Y+tv.H, 34 | ) 35 | if err != nil { 36 | if err != gocui.ErrUnknownView { 37 | return err 38 | } 39 | v.FgColor = gocui.AttrBold | gocui.ColorCyan 40 | v.Wrap = true 41 | } 42 | 43 | tv.updateBody(v) 44 | 45 | return nil 46 | } 47 | 48 | func (tv *TitleView) updateBody(v *gocui.View) { 49 | today := time.Now() 50 | selectedWeek := tv.Calendar.FormatWeekBody() 51 | todayString := fmt.Sprintf("%s %d - %s", today.Month().String(), today.Day(), utils.FormatHourFromTime(today)) 52 | 53 | title := fmt.Sprintf("%s | %s", selectedWeek, todayString) 54 | 55 | v.Clear() 56 | fmt.Fprintln(v, title) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/views/week-view.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/HubertBel/lazyorg/internal/calendar" 5 | "github.com/jroimartin/gocui" 6 | ) 7 | 8 | var WeekdayNames = []string{ 9 | "Sunday", 10 | "Monday", 11 | "Tuesday", 12 | "Wednesday", 13 | "Thursday", 14 | "Friday", 15 | "Saturday", 16 | } 17 | 18 | type WeekView struct { 19 | *BaseView 20 | 21 | Calendar *calendar.Calendar 22 | 23 | TimeView *TimeView 24 | } 25 | 26 | func NewWeekView(c *calendar.Calendar, tv *TimeView) *WeekView { 27 | wv := &WeekView{ 28 | BaseView: NewBaseView("week"), 29 | Calendar: c, 30 | TimeView: tv, 31 | } 32 | 33 | wv.AddChild(WeekdayNames[0], NewDayView(WeekdayNames[0], c.CurrentWeek.Days[0], tv)) 34 | wv.AddChild(WeekdayNames[1], NewDayView(WeekdayNames[1], c.CurrentWeek.Days[1], tv)) 35 | wv.AddChild(WeekdayNames[2], NewDayView(WeekdayNames[2], c.CurrentWeek.Days[2], tv)) 36 | wv.AddChild(WeekdayNames[3], NewDayView(WeekdayNames[3], c.CurrentWeek.Days[3], tv)) 37 | wv.AddChild(WeekdayNames[4], NewDayView(WeekdayNames[4], c.CurrentWeek.Days[4], tv)) 38 | wv.AddChild(WeekdayNames[5], NewDayView(WeekdayNames[5], c.CurrentWeek.Days[5], tv)) 39 | wv.AddChild(WeekdayNames[6], NewDayView(WeekdayNames[6], c.CurrentWeek.Days[6], tv)) 40 | 41 | return wv 42 | } 43 | 44 | func (wv *WeekView) Update(g *gocui.Gui) error { 45 | v, err := g.SetView( 46 | wv.Name, 47 | wv.X, 48 | wv.Y, 49 | wv.X+wv.W, 50 | wv.Y+wv.H, 51 | ) 52 | if err != nil { 53 | if err != gocui.ErrUnknownView { 54 | return err 55 | } 56 | v.Frame = false 57 | } 58 | 59 | wv.updateChildViewProperties() 60 | 61 | if err = wv.UpdateChildren(g); err != nil { 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (wv *WeekView) updateChildViewProperties() { 69 | x := wv.X 70 | w := wv.W/7 - Padding 71 | 72 | for _, weekday := range WeekdayNames { 73 | if dayView, ok := wv.GetChild(weekday); ok { 74 | 75 | dayView.SetProperties( 76 | x, 77 | wv.Y+1, 78 | w, 79 | wv.H-2, 80 | ) 81 | } 82 | 83 | x += w + Padding 84 | } 85 | } 86 | --------------------------------------------------------------------------------