Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 19 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,37 @@ Keep track of what you're doing when you do it and forget trying to write summar

![demo](./demo.gif)

## Usage
## Installation

To write or edit today's file, run `daylog` and today's log will be opened in `$EDITOR`.
### Homebrew
```
brew tap notnmeyer/daylog-cli
brew install daylog
```

To view today's file, run `daylog show`.
### Releases
Grab a release directly from the [releases page]()

To interact with a past or future log supply a date (`daylog show -- 2023/01/07`), or a more casual realtive reference, "tomorrow", "yesterday", "1 day ago", etc.
### From source
`go build -o ~/bin/daylog main.go`, substituting `~/bin/daylog` for a different path if you prefer.

### Log storage
## Usage

Logs are stored in `$XDG_DATA_HOME/daylog`. Use `daylog info` to print the exact directory.
To write or edit today's log, run `daylog` and today's log will be opened in `$EDITOR`.

## Installation
To view today's log, run `daylog show`.

### Install a prebuilt binary
To interact with a past or future log supply a date (`daylog show -- 2023/01/07`), or a more casual realtive reference, "tomorrow", "yesterday", "1 day ago", etc.

Via Homebrew,
You can pipe updates as well, `echo "- ate a burrito" | daylog`.

```
brew tap notnmeyer/daylog-cli
brew install daylog
```
For other commands and options see, `daylog --help`.

Or grab a release directly from the [releases page]()
### Log storage

### From source
Logs are stored in `$XDG_DATA_HOME/daylog`. Use `daylog info` to print the exact directory.

1. Build the project with, `go build -o ~/bin/daylog main.go`, substituting `~/bin/daylog` for a different path if you prefer.
---

[^1]: DayLog ah ahh ahhhhhh, fighter of the night log ah ahh ahhhhh.

Expand Down
55 changes: 50 additions & 5 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package cmd

import (
"context"
"io"
"log"
"os"
"path/filepath"
"strings"

"github.com/charmbracelet/fang"
"github.com/notnmeyer/daylog-cli/internal/daylog"
Expand Down Expand Up @@ -32,17 +34,25 @@ var rootCmd = &cobra.Command{
log.Fatal(err)
}

showPrevious, err := cmd.PersistentFlags().GetBool("prev")
if err := applyPrevFlag(cmd, dl); err != nil {
log.Fatal(err)
}

piped, err := stdinIsPiped()
if err != nil {
log.Fatalf("%s", err.Error())
log.Fatal(err)
}

if showPrevious {
prev, err := file.PreviousLog(dl.ProjectPath, file.LogProvider{})
if piped {
content, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
}
dl.Path = filepath.Join(dl.ProjectPath, prev, "log.md")
formatted := formatStdinContent(string(content))
if err := dl.Append(formatted); err != nil {
log.Fatal(err)
}
return
}

if err := dl.Edit(); err != nil {
Expand Down Expand Up @@ -71,3 +81,38 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&config.Project, "project", "p", "default", "The daylog project to use")
rootCmd.PersistentFlags().Bool("prev", false, "Operate on the most recent log that isn't today's")
}

func applyPrevFlag(cmd *cobra.Command, dl *daylog.DayLog) error {
showPrevious, err := cmd.PersistentFlags().GetBool("prev")
if err != nil {
return err
}
if showPrevious {
prev, err := file.PreviousLog(dl.ProjectPath, file.LogProvider{})
if err != nil {
return err
}
dl.Path = filepath.Join(dl.ProjectPath, prev, "log.md")
}
return nil
}

func formatStdinContent(content string) string {
// if a string like "hello\nworld" is piped, the markdown formatter (the default) will smash
// it together like "helloworld". so if there are \n's just make it a code block so the line
// breaks render correctly.
content = strings.TrimRight(content, "\n")
if strings.Contains(content, "\n") {
return "```\n" + content + "\n```"
}
return content
}

func stdinIsPiped() (bool, error) {
stat, err := os.Stdin.Stat()
if err != nil {
return false, err
}
// terminals set ModeCharDevice. pipes don't. so if the bit is zero, we have piped input
return (stat.Mode() & os.ModeCharDevice) == 0, nil
}
46 changes: 46 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cmd

import "testing"

func TestFormatStdinContent(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "single line",
input: "hello world",
expected: "hello world",
},
{
name: "single line with trailing newline",
input: "hello world\n",
expected: "hello world",
},
{
name: "multi-line becomes code block",
input: "hello\nworld",
expected: "```\nhello\nworld\n```",
},
{
name: "multi-line with trailing newline becomes code block",
input: "hello\nworld\n",
expected: "```\nhello\nworld\n```",
},
{
name: "empty string",
input: "",
expected: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatStdinContent(tt.input)
if result != tt.expected {
t.Errorf("formatStdinContent(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
34 changes: 33 additions & 1 deletion internal/daylog/daylog.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,38 @@ func New(args []string, project string) (*DayLog, error) {
}, nil
}

// append content to the log for the specified date
func (d *DayLog) Append(content string) error {
if err := createIfMissing(d); err != nil {
return err
}

existing, err := os.ReadFile(d.Path)
if err != nil {
return err
}

if len(existing) > 0 && existing[len(existing)-1] != '\n' {
existing = append(existing, '\n')
}

content = strings.TrimRight(content, "\n") + "\n"

if err := os.WriteFile(d.Path, append(existing, []byte(content)...), 0644); err != nil {
return err
}

if d.gitEnabled() {
msg := fmt.Sprintf("update log for %d/%d/%d\n", d.Date.Year(), int(d.Date.Month()), d.Date.Day())
output, err := git.AddAndCommit(d.ProjectPath, d.Path, msg)
if err != nil {
return fmt.Errorf("%s: %s", err, output.Stderr.String())
}
}

return nil
}

// edit the log for the specified date
func (d *DayLog) Edit() error {
if err := createIfMissing(d); err != nil {
Expand Down Expand Up @@ -195,7 +227,7 @@ func createIfMissing(d *DayLog) error {
}

year, month, day := d.Date.Year(), int(d.Date.Month()), d.Date.Day()
header := fmt.Sprintf("# %d/%02d/%02d", year, month, day)
header := fmt.Sprintf("# %d/%02d/%02d\n\n", year, month, day)
_, err = file.WriteString(header)
if err != nil {
return err
Expand Down
91 changes: 91 additions & 0 deletions internal/daylog/daylog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package daylog

import (
"os"
"path/filepath"
"testing"
"time"
)

func testDayLog(t *testing.T) *DayLog {
t.Helper()
dir := t.TempDir()
date := time.Date(2025, 12, 2, 0, 0, 0, 0, time.UTC)
return &DayLog{
Path: filepath.Join(dir, "log.md"),
ProjectPath: dir,
Date: &date,
}
}

func readFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("reading file: %v", err)
}
return string(b)
}

func TestAppend(t *testing.T) {
tests := []struct {
name string
existing *string // nil means file doesn't exist yet
content string
expectedFinal string
}{
{
name: "new file gets header then content",
existing: nil,
content: "hello world",
expectedFinal: "# 2025/12/02\n\nhello world\n",
},
{
name: "existing file with trailing newline",
existing: strPtr("# 2025/12/02\n\nexisting entry\n"),
content: "new entry",
expectedFinal: "# 2025/12/02\n\nexisting entry\nnew entry\n",
},
{
name: "existing file without trailing newline",
existing: strPtr("# 2025/12/02\n\nexisting entry"),
content: "new entry",
expectedFinal: "# 2025/12/02\n\nexisting entry\nnew entry\n",
},
{
name: "trailing newlines in content are normalized to one",
existing: strPtr("# 2025/12/02\n\n"),
content: "hello\n\n",
expectedFinal: "# 2025/12/02\n\nhello\n",
},
{
name: "multi-line content (code block) appended intact",
existing: strPtr("# 2025/12/02\n\n"),
content: "```\nline one\nline two\n```",
expectedFinal: "# 2025/12/02\n\n```\nline one\nline two\n```\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dl := testDayLog(t)

if tt.existing != nil {
if err := os.WriteFile(dl.Path, []byte(*tt.existing), 0644); err != nil {
t.Fatalf("writing existing file: %v", err)
}
}

if err := dl.Append(tt.content); err != nil {
t.Fatalf("Append() error = %v", err)
}

got := readFile(t, dl.Path)
if got != tt.expectedFinal {
t.Errorf("file contents =\n%q\nwant\n%q", got, tt.expectedFinal)
}
})
}
}

func strPtr(s string) *string { return &s }
Loading