├── test └── data │ ├── journal_with_entry │ ├── 2022-01-01.md │ └── 2022-just-name.md │ └── journal_with_various_entries │ ├── 1984-01-25.md │ ├── 1999-jan-01.txt │ └── random.md ├── demo.png ├── .gitignore ├── obsidian-daily-notes.png ├── staticcheck.conf ├── cmd ├── edit_test.go ├── about.go ├── edit.go ├── list.go └── quote.go ├── .github └── workflows │ ├── test.yml │ ├── golangci-lint.yml │ └── release.yml ├── internal ├── entry.go ├── entry_test.go ├── context_test.go └── context.go ├── .golangci.yml ├── LICENSE ├── main.go ├── INSTALL.md ├── CHANGELOG.md ├── go.mod ├── .goreleaser.yaml ├── README.md └── go.sum /test/data/journal_with_entry/2022-01-01.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/data/journal_with_entry/2022-just-name.md: -------------------------------------------------------------------------------- 1 | random text 2 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skatkov/stoic/HEAD/demo.png -------------------------------------------------------------------------------- /test/data/journal_with_various_entries/1984-01-25.md: -------------------------------------------------------------------------------- 1 | random text 2 | -------------------------------------------------------------------------------- /test/data/journal_with_various_entries/1999-jan-01.txt: -------------------------------------------------------------------------------- 1 | random text -------------------------------------------------------------------------------- /test/data/journal_with_various_entries/random.md: -------------------------------------------------------------------------------- 1 | random text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | .vscode 3 | stoic 4 | 5 | dist/ 6 | 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /obsidian-daily-notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skatkov/stoic/HEAD/obsidian-daily-notes.png -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = [ 2 | "inherit", 3 | "-ST1001", # Allow dot imports 4 | "-ST1005", # Allow capitalized error strings 5 | ] -------------------------------------------------------------------------------- /cmd/edit_test.go: -------------------------------------------------------------------------------- 1 | package stoic 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | s "github.com/skatkov/stoic/internal" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNewEditCommand(t *testing.T) { 12 | ctx := s.NewContext("", "", "", "") 13 | command := NewEditCommand(ctx, "yesterday") 14 | y, m, d := time.Now().AddDate(0, 0, -1).Date() 15 | 16 | assert.Equal(t, time.Date(y, m, d, 0, 0, 0, 0, time.Now().Location()), command.Date()) 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: actions/setup-go@v5 9 | with: 10 | go-version-file: 'go.mod' 11 | cache: true 12 | - name: Build 13 | run: go build -v ./... 14 | 15 | - name: Go Vet 16 | run: go vet ./... 17 | 18 | - name: Test 19 | run: go test ./... 20 | -------------------------------------------------------------------------------- /internal/entry.go: -------------------------------------------------------------------------------- 1 | package stoic 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | const FILE_TEMPLATE = "2006-01-02" 10 | 11 | type Entry interface { 12 | Filepath() string 13 | } 14 | type entry struct { 15 | filepath string 16 | } 17 | 18 | func NewEntry(ctx Context, time time.Time) Entry { 19 | return &entry{ 20 | filepath: ctx.Directory() + strings.ToLower(fmt.Sprintf("%s.%s", time.Format(FILE_TEMPLATE), ctx.FileExtension())), 21 | } 22 | } 23 | 24 | func (e *entry) Filepath() string { return e.filepath } 25 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | 2 | name: golangci-lint 3 | on: 4 | push: 5 | pull_request: 6 | permissions: 7 | contents: read 8 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 9 | # pull-requests: read 10 | jobs: 11 | golangci: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version-file: 'go.mod' 19 | cache: true 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v7 22 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - errcheck 6 | - errname 7 | - errorlint 8 | - goconst 9 | - gocritic 10 | - govet 11 | - ineffassign 12 | - staticcheck 13 | - unconvert 14 | - unparam 15 | - unused 16 | - usestdlibvars 17 | exclusions: 18 | generated: lax 19 | presets: 20 | - comments 21 | - common-false-positives 22 | - legacy 23 | - std-error-handling 24 | paths: 25 | - third_party$ 26 | - builtin$ 27 | - examples$ 28 | formatters: 29 | exclusions: 30 | generated: lax 31 | paths: 32 | - third_party$ 33 | - builtin$ 34 | - examples$ 35 | -------------------------------------------------------------------------------- /cmd/about.go: -------------------------------------------------------------------------------- 1 | package stoic 2 | 3 | import "fmt" 4 | 5 | type AboutCommand interface { 6 | Run() 7 | } 8 | 9 | type aboutCommand struct { 10 | version string 11 | commitHash string 12 | date string 13 | } 14 | 15 | func NewAboutCommand(version, commitHash, date string) AboutCommand { 16 | return &aboutCommand{ 17 | version: version, 18 | commitHash: commitHash, 19 | date: date, 20 | } 21 | } 22 | 23 | func (a *aboutCommand) Run() { 24 | about_message := fmt.Sprintf("Version: %s", a.version) + "\n" 25 | about_message += fmt.Sprintf("Commit Hash: %s", a.commitHash) + "\n" 26 | about_message += fmt.Sprintf("Release Date: %s", a.date) + "\n" 27 | 28 | fmt.Println(about_message) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/edit.go: -------------------------------------------------------------------------------- 1 | package stoic 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | stoic "github.com/skatkov/stoic/internal" 9 | naturaldate "github.com/tj/go-naturaldate" 10 | ) 11 | 12 | type EditCommand interface { 13 | Run() 14 | Date() time.Time 15 | } 16 | 17 | type editCommand struct { 18 | ctx stoic.Context 19 | value string 20 | date time.Time 21 | } 22 | 23 | func NewEditCommand(ctx stoic.Context, value string) EditCommand { 24 | date, err := naturaldate.Parse(value, time.Now()) 25 | if err != nil { 26 | fmt.Println("Error parsing date:", err) 27 | os.Exit(1) 28 | } 29 | 30 | return &editCommand{ 31 | ctx: ctx, 32 | value: value, 33 | date: date, 34 | } 35 | } 36 | 37 | func (e *editCommand) Date() time.Time { 38 | return e.date 39 | } 40 | 41 | func (e *editCommand) Run() { 42 | entry := stoic.NewEntry(e.ctx, e.date) 43 | err := e.ctx.OpenInEditor(entry) 44 | if err != nil { 45 | fmt.Println("Error running program:", err) 46 | os.Exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/entry_test.go: -------------------------------------------------------------------------------- 1 | package stoic 2 | 3 | import ( 4 | "os/user" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNewEntry(t *testing.T) { 12 | tm, _ := time.Parse("2006-Jan-02", "2020-Jan-01") 13 | ctx := NewContext("", "", "", "") 14 | e := NewEntry(ctx, tm) 15 | 16 | homeDir, _ := user.Current() 17 | 18 | assert.Equal(t, homeDir.HomeDir+"/Journal/2020-01-01.md", e.Filepath()) 19 | } 20 | 21 | func TestNewEntryWithExtension(t *testing.T) { 22 | tm, _ := time.Parse("2006-Jan-02", "2020-Jan-01") 23 | ctx := NewContext("", "txt", "", "") 24 | e := NewEntry(ctx, tm) 25 | 26 | homeDir, _ := user.Current() 27 | 28 | assert.Equal(t, homeDir.HomeDir+"/Journal/2020-01-01.txt", e.Filepath()) 29 | } 30 | 31 | func TestNewEntryWithDirectory(t *testing.T) { 32 | tm, _ := time.Parse("2006-Jan-02", "2020-Jan-01") 33 | ctx := NewContext("~/Journal/test", "", "", "") 34 | e := NewEntry(ctx, tm) 35 | 36 | homeDir, _ := user.Current() 37 | 38 | assert.Equal(t, homeDir.HomeDir+"/Journal/test/2020-01-01.md", e.Filepath()) 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Stanislav Katkov 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | name: goreleaser 3 | 4 | on: 5 | push: 6 | # run only against tags 7 | tags: 8 | - "*" 9 | 10 | permissions: 11 | contents: write 12 | # packages: write 13 | # issues: write 14 | 15 | jobs: 16 | goreleaser: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - run: git fetch --force --tags 23 | - uses: actions/setup-go@v5 24 | with: 25 | go-version-file: 'go.mod' 26 | cache: true 27 | # More assembly might be required: Docker logins, GPG, etc. 28 | # It all depends on your needs. 29 | - uses: goreleaser/goreleaser-action@v6 30 | with: 31 | # either 'goreleaser' (default) or 'goreleaser-pro': 32 | distribution: goreleaser 33 | version: latest 34 | args: release --clean 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.STOIC_SECRET }} 37 | # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' 38 | # distribution: 39 | # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} 40 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | cmd "github.com/skatkov/stoic/cmd" 10 | stoic "github.com/skatkov/stoic/internal" 11 | ) 12 | 13 | var ( 14 | version = "dev" 15 | commit = "none" 16 | date = "unknown" 17 | ) 18 | 19 | func main() { 20 | ctx := stoic.NewContext( 21 | os.Getenv("STOIC_DIR"), 22 | os.Getenv("STOIC_EXT"), 23 | os.Getenv("EDITOR"), 24 | os.Getenv("STOIC_TEMPLATE"), 25 | ) 26 | 27 | aboutFlag := flag.Bool("about", false, "display about info") 28 | listFlag := flag.Bool("list", false, "list journal entries") 29 | quoteFlag := flag.Bool("quote", false, "random quote to inspire ongoing journaling habit") 30 | editFlag := flag.String("edit", "", "edit a journal entry") 31 | flag.Parse() 32 | 33 | switch { 34 | case *aboutFlag: 35 | cmd.NewAboutCommand(version, commit, date).Run() 36 | case *listFlag: 37 | cmd.NewListCommand(ctx).Run() 38 | case *editFlag != "": 39 | cmd.NewEditCommand(ctx, *editFlag).Run() 40 | case *quoteFlag: 41 | cmd.NewQuoteCommand().Run() 42 | default: 43 | err := ctx.OpenInEditor(stoic.NewEntry(ctx, time.Now())) 44 | if err != nil { 45 | fmt.Println(err) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Install stoic 2 | 3 | In order to not miss any updates you can subscribe to the release 4 | notifications on [Github](https://github.com/skatkov/stoic) (at the top right: 5 | “Watch”→“Custom”→“Releases”). 6 | 7 | ## MacOS 8 | 9 | 1. Download the latest version and unzip 10 | - [**Download for Intel**](https://github.com/skatkov/stoic/releases/latest/download/stoic_Darwin_x86_64.tar.gz) 11 | - [**Download for M1 (ARM)**](https://github.com/skatkov/stoic/releases/latest/download/stoic_Darwin_arm64.tar.gz) 12 | 2. Right-click on the binary and select “Open“ 13 | (due to [Gatekeeper](https://support.apple.com/en-us/HT202491)) 14 | 3. Copy to path, e.g. `mv stoic /usr/local/bin/stoic` (might require `sudo`) 15 | 16 | ## Linux 17 | 18 | 1. Download the latest version and unzip 19 | - [**Download for Intel (x86_64)**](https://github.com/skatkov/stoic/releases/latest/download/stoic_Linux_x86_64.tar.gz) 20 | - [**Download for Intel (i386)**](https://github.com/skatkov/stoic/releases/latest/download/stoic_Linux_i386.tar.gz) 21 | - [**Download for ARM**](https://github.com/skatkov/stoic/releases/latest/download/stoic_Linux_arm64.tar.gz) 22 | 2. Copy to path, e.g. `mv stoic /usr/local/bin/stoic` (might require `sudo`) 23 | 24 | ## Windows 25 | 26 | Is not supported 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.5] - 2022-08-23 11 | 12 | ### Refactoring 13 | 14 | - Remove use of 'ioutil' deprecated class 15 | - Refactor main and list classes to use switch statement 16 | - Upgrade target for golang to 1.19 17 | - Update dependencies 18 | 19 | ### Added 20 | 21 | - Styling `-quote` command with lipgloss 22 | 23 | ## [0.4] - 2022-06-18 24 | 25 | ### Added 26 | 27 | - add `-quote` command to pick a random encouragement to journal 28 | 29 | ## [0.3.2] - 2022-06-12 30 | 31 | ### Added 32 | 33 | - add ability to edit entries through `-edit` command 34 | 35 | ## [0.3.1] - 2022-06-12 36 | 37 | ### Changed 38 | 39 | - proper exit status for edit files in list 40 | 41 | ## [0.3.0] - 2022-06-12 42 | 43 | ### Added 44 | 45 | - Add a basic `-list` command. 46 | 47 | ## [0.2.1] 48 | 49 | ### Changed 50 | 51 | - Code refactoring, simplification 52 | 53 | ## [0.2] 54 | 55 | ### Added 56 | 57 | - `-about` command was added 58 | 59 | ## [0.1] 60 | 61 | ### Added 62 | 63 | - First version that opens a current journal entry 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skatkov/stoic 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/charmbracelet/lipgloss v1.1.0 9 | github.com/mitchellh/go-homedir v1.1.0 10 | github.com/stretchr/testify v1.8.0 11 | github.com/tj/go-naturaldate v1.3.0 12 | ) 13 | 14 | require ( 15 | github.com/atotto/clipboard v0.1.4 // indirect 16 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 17 | github.com/charmbracelet/colorprofile v0.3.0 // indirect 18 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 19 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 20 | github.com/charmbracelet/x/term v0.2.1 // indirect 21 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 22 | github.com/kr/pretty v0.3.0 // indirect 23 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 24 | github.com/mattn/go-isatty v0.0.20 // indirect 25 | github.com/mattn/go-localereader v0.0.1 // indirect 26 | github.com/mattn/go-runewidth v0.0.16 // indirect 27 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 28 | github.com/muesli/cancelreader v0.2.2 // indirect 29 | github.com/muesli/termenv v0.16.0 // indirect 30 | github.com/rivo/uniseg v0.4.7 // indirect 31 | github.com/sahilm/fuzzy v0.1.1 // indirect 32 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 33 | golang.org/x/sync v0.12.0 // indirect 34 | golang.org/x/sys v0.31.0 // indirect 35 | golang.org/x/text v0.23.0 // indirect 36 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | 40 | require ( 41 | github.com/charmbracelet/bubbles v0.20.0 42 | github.com/charmbracelet/bubbletea v1.3.4 43 | github.com/davecgh/go-spew v1.1.1 // indirect 44 | github.com/pmezard/go-difflib v1.0.0 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /.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 | 4 | # The lines bellow are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | before: 10 | hooks: 11 | # You may remove this if you don't use go modules. 12 | - go mod tidy 13 | # you may remove this if you don't need go generate 14 | - go generate ./... 15 | 16 | builds: 17 | - env: 18 | - CGO_ENABLED=0 19 | ldflags: ["-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }}"] 20 | goos: 21 | - linux 22 | - darwin 23 | 24 | archives: 25 | - format: tar.gz 26 | # this name template makes the OS and Arch compatible with the results of `uname`. 27 | name_template: >- 28 | {{ .ProjectName }}_ 29 | {{- title .Os }}_ 30 | {{- if eq .Arch "amd64" }}x86_64 31 | {{- else if eq .Arch "386" }}i386 32 | {{- else }}{{ .Arch }}{{ end }} 33 | {{- if .Arm }}v{{ .Arm }}{{ end }} 34 | # use zip for windows archives 35 | # format_overrides: 36 | # - goos: windows 37 | # format: zip 38 | 39 | source: 40 | enabled: true 41 | 42 | release: 43 | github: 44 | owner: skatkov 45 | name: stoic 46 | 47 | changelog: 48 | sort: asc 49 | filters: 50 | exclude: 51 | - "^docs:" 52 | - "^test:" 53 | 54 | brews: 55 | - homepage: "https://github.com/skatkov/stoic" 56 | description: "Command-line application for daily journaling with plain-text files" 57 | name: stoic 58 | repository: 59 | owner: skatkov 60 | name: homebrew-tap -------------------------------------------------------------------------------- /internal/context_test.go: -------------------------------------------------------------------------------- 1 | package stoic 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestFiles(t *testing.T) { 13 | current_dir, _ := os.Getwd() 14 | test_folder := strings.TrimSuffix(current_dir, "/internal") + "/test/data" 15 | 16 | ctx := NewContext(test_folder+"/journal_with_entry", "", "", "") 17 | assert.Equal(t, []string{test_folder + "/journal_with_entry/" + "2022-01-01.md"}, ctx.Files()) 18 | 19 | ctx = NewContext(test_folder+"/journal_with_entry", "txt", "", "") 20 | assert.Empty(t, ctx.Files()) 21 | 22 | ctx = NewContext(test_folder+"/journal_with_various_entries", "md", "", "") 23 | assert.Equal(t, []string{test_folder + "/journal_with_various_entries/" + "1984-01-25.md"}, ctx.Files()) 24 | 25 | ctx = NewContext(test_folder+"/journal_zero", "", "", "") 26 | assert.Empty(t, ctx.Files()) 27 | } 28 | 29 | func TestNewContext(t *testing.T) { 30 | ctx := NewContext("", "", "", "") 31 | homeDir, _ := user.Current() 32 | 33 | assert.Equal(t, homeDir.HomeDir+"/Journal/", ctx.Directory()) 34 | assert.Equal(t, "md", ctx.FileExtension()) 35 | assert.Equal(t, "nano", ctx.Editor()) 36 | assert.Equal(t, "", ctx.Template()) 37 | } 38 | 39 | func TestNewContextWithEditor(t *testing.T) { 40 | assert.Equal(t, "vim", NewContext("", "", "vim", "").Editor()) 41 | } 42 | 43 | func TestNewContextWithExtension(t *testing.T) { 44 | assert.Equal(t, "txt", NewContext("", "txt", "", "").FileExtension()) 45 | } 46 | 47 | func TestNewContextWithDirectory(t *testing.T) { 48 | homeDir, _ := user.Current() 49 | ctx := NewContext("~/Journal/test", "", "", "") 50 | 51 | assert.Equal(t, homeDir.HomeDir+"/Journal/test/", ctx.Directory()) 52 | } 53 | 54 | func TestNewContextWithTemplate(t *testing.T) { 55 | homeDir, _ := user.Current() 56 | ctx := NewContext("", "", "", "~/Journal/template.md") 57 | 58 | assert.Equal(t, homeDir.HomeDir+"/Journal/template.md", ctx.Template()) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package stoic 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/charmbracelet/bubbles/list" 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/charmbracelet/lipgloss" 11 | stoic "github.com/skatkov/stoic/internal" 12 | ) 13 | 14 | type ListCommand interface { 15 | Run() 16 | } 17 | 18 | type listCommand struct { 19 | ctx stoic.Context 20 | } 21 | 22 | func NewListCommand(ctx stoic.Context) ListCommand { 23 | return &listCommand{ 24 | ctx: ctx, 25 | } 26 | } 27 | 28 | func (lc listCommand) Run() { 29 | var items []list.Item 30 | files := lc.ctx.Files() 31 | 32 | for _, file := range files { 33 | fileInfo, _ := os.Lstat(file) 34 | status := fmt.Sprintf("%s %s %s", 35 | fileInfo.ModTime().Format("2006-01-02 15:04:05"), 36 | fileInfo.Mode().String(), 37 | ConvertBytesToSizeString(fileInfo.Size())) 38 | 39 | items = append(items, item{ 40 | title: file, 41 | desc: status, 42 | }) 43 | } 44 | 45 | m := model{ 46 | list: list.New(items, list.NewDefaultDelegate(), 0, 0), 47 | context: lc.ctx, 48 | } 49 | m.list.Title = "Journal Entries" 50 | 51 | p := tea.NewProgram(m, tea.WithAltScreen()) 52 | 53 | if _, err := p.Run(); err != nil { 54 | fmt.Println("Error running program:", err) 55 | os.Exit(1) 56 | } 57 | } 58 | 59 | var docStyle = lipgloss.NewStyle().Margin(1, 2) 60 | 61 | type item struct { 62 | title, desc string 63 | } 64 | 65 | func (i item) Title() string { return i.title } 66 | func (i item) Description() string { return i.desc } 67 | func (i item) FilterValue() string { return i.title } 68 | 69 | type model struct { 70 | list list.Model 71 | context stoic.Context 72 | } 73 | 74 | func (m model) Init() tea.Cmd { 75 | return nil 76 | } 77 | 78 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 79 | switch msg := msg.(type) { 80 | case tea.KeyMsg: 81 | switch { 82 | case msg.String() == "ctrl+c": 83 | return m, tea.Quit 84 | case msg.String() == "enter": 85 | selectedItem, _ := m.list.SelectedItem().(item) 86 | 87 | _ = OpenFileInEditor(selectedItem.title, m.context) 88 | os.Exit(0) 89 | case msg.String() == " ": 90 | selectedItem, _ := m.list.SelectedItem().(item) 91 | 92 | _ = OpenFileInEditor(selectedItem.title, m.context) 93 | os.Exit(0) 94 | } 95 | case tea.WindowSizeMsg: 96 | h, v := docStyle.GetFrameSize() 97 | m.list.SetSize(msg.Width-h, msg.Height-v) 98 | } 99 | 100 | var cmd tea.Cmd 101 | m.list, cmd = m.list.Update(msg) 102 | return m, cmd 103 | } 104 | 105 | func (m model) View() string { 106 | return docStyle.Render(m.list.View()) 107 | } 108 | 109 | func OpenFileInEditor(filepath string, ctx stoic.Context) error { 110 | cmd := exec.Command(ctx.Editor(), filepath) 111 | cmd.Stdin = os.Stdin 112 | cmd.Stdout = os.Stdout 113 | cmd.Stderr = os.Stderr 114 | return cmd.Run() 115 | } 116 | 117 | const ( 118 | thousand = 1000 119 | ten = 10 120 | fivePercent = 0.0499 121 | ) 122 | 123 | // ConvertBytesToSizeString converts a byte count to a human readable string. 124 | func ConvertBytesToSizeString(size int64) string { 125 | if size < thousand { 126 | return fmt.Sprintf("%dB", size) 127 | } 128 | 129 | suffix := []string{ 130 | "K", // kilo 131 | "M", // mega 132 | "G", // giga 133 | "T", // tera 134 | "P", // peta 135 | "E", // exa 136 | "Z", // zeta 137 | "Y", // yotta 138 | } 139 | 140 | curr := float64(size) / thousand 141 | for _, s := range suffix { 142 | if curr < ten { 143 | return fmt.Sprintf("%.1f%s", curr-fivePercent, s) 144 | } else if curr < thousand { 145 | return fmt.Sprintf("%d%s", int(curr), s) 146 | } 147 | curr /= thousand 148 | } 149 | 150 | return "" 151 | } 152 | -------------------------------------------------------------------------------- /internal/context.go: -------------------------------------------------------------------------------- 1 | package stoic 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | "time" 10 | 11 | homedir "github.com/mitchellh/go-homedir" 12 | ) 13 | 14 | const ( 15 | DEFAULT_EDITOR = "nano" 16 | DEFAULT_DIRECTORY = "~/Journal/" 17 | DEFAULT_EXTENSION = "md" 18 | ) 19 | 20 | type Context interface { 21 | Directory() string 22 | FileExtension() string 23 | Template() string 24 | Editor() string 25 | OpenInEditor(entry Entry) error 26 | Files() []string 27 | } 28 | 29 | type context struct { 30 | directory string 31 | fileExtension string 32 | editor string 33 | template string 34 | } 35 | 36 | func NewContext(homeDir, fileExtension, editor, template string) Context { 37 | directory := expandDir(homeDir) 38 | 39 | if fileExtension == "" { 40 | fileExtension = DEFAULT_EXTENSION 41 | } 42 | 43 | template, _ = homedir.Expand(template) 44 | 45 | if editor == "" { 46 | editor = DEFAULT_EDITOR 47 | } 48 | 49 | return &context{ 50 | directory: directory, 51 | fileExtension: fileExtension, 52 | editor: editor, 53 | template: template, 54 | } 55 | } 56 | 57 | func (ctx *context) Directory() string { return ctx.directory } 58 | func (ctx *context) FileExtension() string { return ctx.fileExtension } 59 | func (ctx *context) Editor() string { return ctx.editor } 60 | func (ctx *context) Template() string { return ctx.template } 61 | 62 | func (ctx *context) OpenInEditor(entry Entry) error { 63 | err := createDirectoryIfMissing(ctx.directory) 64 | if err != nil { 65 | fmt.Println(err) 66 | return err 67 | } 68 | 69 | if ctx.Template() != "" && !fileExists(entry.Filepath()) { 70 | _ = createFileFromTemplate(entry.Filepath(), ctx.Template()) 71 | } 72 | 73 | cmd := exec.Command(ctx.editor, entry.Filepath()) 74 | cmd.Stdin = os.Stdin 75 | cmd.Stdout = os.Stdout 76 | cmd.Stderr = os.Stderr 77 | return cmd.Run() 78 | } 79 | 80 | func createFileFromTemplate(filename, template_path string) error { 81 | file, err := os.Create(filename) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | defer file.Close() 87 | 88 | template, _ := readFile(template_path) 89 | 90 | _, err = file.WriteString(template) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | 98 | func fileExists(filepath string) bool { 99 | if _, err := os.Stat(filepath); os.IsNotExist(err) { 100 | return false 101 | } 102 | 103 | return true 104 | } 105 | 106 | func readFile(filename string) (string, error) { 107 | file, err := os.Open(filename) 108 | if err != nil { 109 | return "", err 110 | } 111 | 112 | defer file.Close() 113 | 114 | var lines []string 115 | scanner := bufio.NewScanner(file) 116 | for scanner.Scan() { 117 | lines = append(lines, scanner.Text()) 118 | } 119 | 120 | return strings.Join(lines, "\n"), nil 121 | } 122 | 123 | func expandDir(directory string) string { 124 | if directory == "" { 125 | directory, _ = homedir.Expand(DEFAULT_DIRECTORY) 126 | } else { 127 | directory, _ = homedir.Expand(directory) 128 | } 129 | 130 | return directory + "/" 131 | } 132 | 133 | func (ctx context) Files() []string { 134 | files, _ := os.ReadDir(ctx.directory) 135 | 136 | var filenames []string 137 | for _, file := range files { 138 | if strings.HasSuffix(file.Name(), ctx.fileExtension) { 139 | filename := strings.TrimSuffix(file.Name(), "."+ctx.fileExtension) 140 | 141 | _, err := time.Parse(FILE_TEMPLATE, filename) 142 | if err == nil { 143 | filenames = append(filenames, ctx.directory+file.Name()) 144 | } 145 | } 146 | } 147 | 148 | return filenames 149 | } 150 | 151 | func createDirectoryIfMissing(dir string) error { 152 | if _, err := os.Stat(dir); os.IsNotExist(err) { 153 | err := os.MkdirAll(dir, 0o755) 154 | if err != nil { 155 | return err 156 | } 157 | } 158 | 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stoic 2 | 3 | ![stoic](./demo.png) 4 | 5 | Stoic is a command-line application for daily journaling with plain-text files. It helps maintain day-to-day journaling habit by allowing you to instantly edit current day's entry with console editor of choice. 6 | 7 | ## Installation 8 | 9 | Installation is possible through [Homebrew](https://brew.sh/) on Linux or MacOS. 10 | 11 | `brew install skatkov/tap/stoic` 12 | 13 | You can also download the executable directly. See the 📥 [Installation](INSTALL.md) instructions. 14 | 15 | ## Usage 16 | 17 | In a terminal: 18 | 19 | - `stoic`: open today's journal entry in the editor 20 | - `stoic -list` (beta): list existing entries and pick one for editing 21 | - `stoic -edit ""`: open or create the previous day's file using natural dates like "2 days ago" and "yesterday" 22 | - `stoic -quote`: shows a random quote encouraging the journaling habit 23 | - `stoic -about`: shows information about the application 24 | 25 | ## Configuration 26 | 27 | - The editor could be changed by setting the `$EDITOR` global variable. (default: `nano`) 28 | - The directory for a journal could be changed by setting the `$STOIC_DIR` global variable. (default: `~/Journal/`) 29 | - Provide file template through `$STOIC_TEMPLATE` global variable. 30 | - Provide new extension format through `$STOIC_EXT` global variables. (default: `md`) 31 | 32 | ```shell 33 | export EDITOR="neovim" 34 | export STOIC_DIR="~/MEGAsync/journal/" 35 | export STOIC_TEMPLATE="~/MEGAsync/journal/template.md" 36 | export STOIC_EXT="md" 37 | ``` 38 | 39 | ## Motivation 40 | 41 | There is a recurring theme in biographies of great people. They had journaling as a hobby. 42 | 43 | I've been battling my inner demons with different methods with varying success. But journaling has helped me keep these demons permanently at bay. My sleep will be peaceful if it follows after careful and honest self-examination in my journal. 44 | 45 | Existing software for journaling and note-taking is slow to load and filled with features I don't need. Plain-text files stored locally and edited through Nano are enough for journaling. 46 | 47 | However, some recurring manual work is still required with such a simple setup; creating a new daily file and modifying it according to a template. This command line utility completely removes that manual work. 48 | 49 | Epictetus, the great Stoic philosopher and slave, once told his students that "philosophy is something one should write down day by day". Hence, the name of this tool is a reference to this great human and a hat tip to the practical philosophy called Stoicism. 50 | 51 | ## Obsidian integration 52 | 53 | I wrote this tool out of frustration with existing note-taking apps (everything has to be in the cloud these days), but other apps followed similar design choices as Stoic did. 54 | 55 | I'm now using [Obsidian](https://obsidian.md/) as a fully featured writing app on my laptop and mobile phone. Obsidian comes with core plugins that you can enable. One is "[Daily Notes](https://help.obsidian.md/Plugins/Daily+notes)", which does something similar to Stoic. Nonetheless, I still keep using Stoic as a companion CLI application to Obsidian; with light configuration changes, they play perfectly together. 56 | 57 | With the 0.6 version of Stoic, many settings align with those Obsidian defaults. In my case, only two settings are tweaked: 58 | 59 | ```shell 60 | export STOIC_DIR="~/Obsidian/journal" 61 | export STOIC_TEMPLATE="~/Obsidian/journal/template.md" 62 | ``` 63 | 64 | This closely corresponds to settings I have in obsidian daily notes: 65 | ![obsidian daily notes config](./obsidian-daily-notes.png) 66 | 67 | The only thing that Stoic doesn't allow you to tweak is the date format. So, if you have that customized in obsidian, lousy luck. 68 | 69 | PRs are welcome. Everything is fixable. 70 | 71 | ## Development 72 | 73 | As a prerequisite, you need to have the [Go compiler](https://golang.org/doc/install). 74 | Please check the [`go.mod`](go.mod) file to see what Go version Stoic requires. 75 | 76 | Fetch the sources: 77 | 78 | ```shell 79 | git clone https://github.com/skatkov/stoic.git 80 | cd stoic 81 | ``` 82 | 83 | To build the project, run: 84 | 85 | ``` 86 | goreleaser build 87 | ``` 88 | 89 | This automatically resolves the dependencies and compiles the source code into an executable for your platform. 90 | 91 | ## Releasing 92 | Publish to homebrew tap at "skatkov/homebrew-tap" and publish a release in current github repository. 93 | 94 | ``` 95 | goreleaser release 96 | ``` 97 | 98 | ## Contributions 99 | 100 | This project is my little Go playground. It would be awesome to learn about any appropriate improvements for this codebase. 101 | 102 | Everyone is welcome to contribute. 103 | 104 | ## TODO's 105 | 106 | I've been brainstorming for possible improvements, and here is a rough list of ideas in no particular order: 107 | 108 | - `stoic -cal`: perhaps with the `bubbletea` TUI framework and the `cal` utility. A calendar view for existing records showing dates marked with green dots if there is a record for that day. 109 | - Improve error handling 110 | - Use a CLI framework like [cobra](https://github.com/spf13/cobra) 111 | - Add Windows support 112 | - Add the ability to store configuration in dotfiles (not just ENV variables), including the ability to add a custom editor, not one defined in `$EDITOR` 113 | - `stoic -stats`: statistics about journaling, % of days journaled, average journal length, etc. 114 | 115 | ## Feedback 116 | 117 | Got some feedback or suggestions? Please open an issue or drop me a note! 118 | 119 | - [Twitter](https://twitter.com/5katkov) 120 | - [Personal Website](https://skatkov.com) 121 | -------------------------------------------------------------------------------- /cmd/quote.go: -------------------------------------------------------------------------------- 1 | package stoic 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | 8 | "github.com/charmbracelet/lipgloss" 9 | ) 10 | 11 | type quote struct { 12 | content string 13 | author string 14 | } 15 | 16 | var quotes = []quote{} 17 | 18 | // Style definitions 19 | 20 | var ( 21 | foreground = lipgloss.AdaptiveColor{Light: "#969B86", Dark: "#696969"} 22 | pageStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2) 23 | boxStyle = lipgloss.NewStyle(). 24 | Border(lipgloss.RoundedBorder()). 25 | BorderForeground(lipgloss.Color("#874BFD")). 26 | Padding(1, 0). 27 | BorderTop(true). 28 | BorderLeft(true). 29 | BorderRight(true). 30 | BorderBottom(true) 31 | 32 | authoredBy = lipgloss.NewStyle().SetString("—"). 33 | PaddingRight(1). 34 | Foreground(foreground). 35 | String() 36 | 37 | author = func(s string) string { 38 | return "\n" + authoredBy + lipgloss.NewStyle(). 39 | Foreground(foreground). 40 | Render(s) + "\n" 41 | } 42 | ) 43 | 44 | type QuoteCommand interface { 45 | Run() 46 | } 47 | 48 | type quoteCommand struct { 49 | quotes []quote 50 | } 51 | 52 | func NewQuoteCommand() QuoteCommand { 53 | quotes = append(quotes, 54 | quote{ 55 | content: "Few care now about the marches and countermarches of the Roman commanders. What the centuries have clung to is a notebook of thoughts by a man whose real life was largely unknown who put down in the midnight dimness not the events of the day or the plans of the morrow, but something of far more permanent interest, the ideals and aspirations that a rare spirit lived by.", 56 | author: "Brand Blanshard", 57 | }, 58 | quote{ 59 | content: "Five hundred years later, Leonardo’s notebooks are around to astonish and inspire us. Fifty years from now, our own notebooks, if we work up the initiative to start them, will be around to astonish and inspire our grandchildren, unlike our tweets and Facebook posts.", 60 | author: "Isaacs Newton", 61 | }, 62 | quote{ 63 | content: "You know those people whose lives are transformed by meditation or yoga or something like that? For me, it’s writing in my diary and journals. It’s made all the difference in the world for my learning, reflecting, and peace of mind.", 64 | author: "Derek Sivers", 65 | }, 66 | quote{ 67 | content: "When the light has been removed and my wife has fallen silent, aware of this habit that’s now mine, I examine my entire day and go back over what I’ve done and said, hiding nothing from myself, passing nothing by.", 68 | author: "Seneca", 69 | }, 70 | quote{ 71 | content: "Every night, I try myself by Court Martial to see if I have done anything effective during the day. I don’t mean just pawing the ground, anyone can go through the motions, but something really effective.", 72 | author: "Winston Churchill", 73 | }, 74 | quote{ 75 | content: "My journaling system is based around studying complexity. Reducing the complexity down to what is the most important question. Sleeping on it, and then waking up in the morning first thing and pre-input brainstorming on it. So I’m feeding my unconscious material to work on, releasing it completely, and then opening my mind and riffing on it.", 76 | author: "Reid Hoffman", 77 | }, 78 | quote{ 79 | content: "Reflection is…a key factor in expert learning and refers to the extent to which individuals are able to appraise what they have learned and to integrate these experiences into future actions, thereby maximizing performance improvements.", 80 | author: "Marije Elferink-Gemser", 81 | }, 82 | quote{ 83 | content: "When Beethoven was enjoying a beer, he might suddenly pull out his notebook and write something in it. ‘Something just occurred to me,’ he would say, sticking it back into his pocket. The ideas that he tossed off separately, with only a few lines and points and without barlines, are hieroglyphics that no one can decipher. Thus in these tiny notebooks he concealed a treasure of ideas.", 84 | author: "Wilhelm Von Lenz", 85 | }, 86 | quote{ 87 | content: "The recognition that I needed to train and discipline my character. Not to be sidetracked by my interest in rhetoric. Not to write treatises on abstract questions, or deliver moralizing little sermons, or compose imaginary descriptions of The Simple Life or The Man Who Lives Only for Others. To steer clear of oratory, poetry and belles lettres. Not to dress up just to stroll around the house, or things like that. To write straightforward.", 88 | author: "Marcus Aurelius", 89 | }, 90 | quote{ 91 | content: "Is not the poet bound to write his own biography? Is there any other work for him but a good journal? We do not wish to know how his imaginary hero, but how he, the actual hero, lived from day to day.", 92 | author: "Henry David Thoreau", 93 | }, 94 | quote{ 95 | content: "Keeping a journal is the veriest pastime in the world, and the pleasantest…Only those rare natures that are made up of pluck, endurance, devotion to duty for duty’s sake, and invincible determination, may hope to venture upon so tremendous an enterprise as the keeping of a journal.", 96 | author: "Mark Twain", 97 | }, 98 | quote{ 99 | content: "I believe I could never exhaust the supply of material lying within me. The deeper I plunge, the more I discover. There is no bottom to my heart and no limit to the acrobatic feats of my imagination.", 100 | author: "Anaïs Nin", 101 | }, 102 | quote{ 103 | content: "People look for retreats for themselves, in the country, by the coast, or in the hills. There is nowhere that a person can find a more peaceful and trouble-free retreat than in his own mind. . . . So constantly give yourself this retreat, and renew yourself.", 104 | author: "Marcus Aurelius", 105 | }, 106 | ) 107 | 108 | return "eCommand{ 109 | quotes: quotes, 110 | } 111 | } 112 | 113 | func (c quoteCommand) Run() { 114 | quote := c.quotes[rand.Intn(len(c.quotes))] 115 | doc := strings.Builder{} 116 | 117 | ui := lipgloss.NewStyle(). 118 | Width(80). 119 | PaddingBottom(1). 120 | PaddingTop(1). 121 | PaddingLeft(3). 122 | PaddingRight(3). 123 | Align(lipgloss.Center). 124 | Render(quote.content) 125 | 126 | doc.WriteString(boxStyle.Render(ui) + "\n") 127 | doc.WriteString(author(quote.author)) 128 | 129 | fmt.Println(pageStyle.Render(doc.String())) 130 | } 131 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= 6 | github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= 7 | github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= 8 | github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= 9 | github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ= 10 | github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0= 11 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 12 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 13 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 14 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 15 | github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= 16 | github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 17 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 18 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 19 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 24 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 25 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 26 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 27 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 28 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 29 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 30 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 31 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 32 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 33 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 34 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 35 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 36 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 37 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 38 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 39 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 40 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 41 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 42 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 43 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 44 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 45 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 46 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 47 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 48 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 49 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 53 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 54 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 55 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 56 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 57 | github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= 58 | github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 59 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 60 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 61 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 62 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 63 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 64 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 65 | github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0PhZE7qpvbZl5ljd8r6U0bI= 66 | github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= 67 | github.com/tj/go-naturaldate v1.3.0 h1:OgJIPkR/Jk4bFMBLbxZ8w+QUxwjqSvzd9x+yXocY4RI= 68 | github.com/tj/go-naturaldate v1.3.0/go.mod h1:rpUbjivDKiS1BlfMGc2qUKNZ/yxgthOfmytQs8d8hKk= 69 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 70 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 71 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 72 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 73 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 74 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 75 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 78 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 79 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 80 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 83 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 85 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 86 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 87 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 88 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 89 | --------------------------------------------------------------------------------