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
32 changes: 6 additions & 26 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.5] - 2024-07-26

### Added

- **Delete Command**: Remove a session from your log with `flow delete`. Useful for cleaning up mistakes or test sessions.

## [1.1.4] - 2025-07-19

- **Paused Session Working Time**: The `flow status` command now shows how much time you've actually worked when a session is paused, excluding pause time for accurate productivity tracking.
Expand Down Expand Up @@ -43,32 +49,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Addressed a potential supply chain vulnerability by replacing an archived dependency.

## [Unreleased]

### Added

- Initial release of Flow
- Focus timer with countdown display
- Task tagging with `--tag` flag
- Session logging to `~/.flowlog`
- Unix philosophy compliance - no built-in integrations
- Shell composition examples for Zenta integration
- Version information with `--version` flag
- Cross-platform build support
- CI/CD with GitHub Actions
- One-liner installation script (`install.sh`)
- Automated platform detection and binary installation
- Automatic creation of installation directory if it doesn't exist

### Changed

- Upgraded to Go 1.23.0
- Removed direct Zenta integration in favor of Unix composition

## Philosophy

Flow follows semantic versioning and Unix philosophy. Breaking changes will only be introduced in major versions, and we strive to maintain backward compatibility.

## [0.1.0] - 2025-07-01

### Added
Expand Down
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ Flow is built on a few core principles that guide its design and purpose.

---

## Quick Start

1. **Install Flow**:
```bash
curl -sSL https://raw.githubusercontent.com/e6a5/flow/main/install.sh | bash
```

2. **Start your first session**:
```bash
flow start --tag "My first deep work session"
```

3. **Check your progress**:
```bash
flow status
```

4. **End when complete**:
```bash
flow end
```

---

## A Typical Workflow

Flow is designed to be intuitive. Here's how a typical session works:
Expand All @@ -49,11 +73,18 @@ Flow is designed to be intuitive. Here's how a typical session works:
> ✨ Session complete: Writing the first draft
> Total focus time: 2h 5m
```

4. **Review your day** and find your patterns.
```bash
flow recent
flow insights
```
```bash
flow recent
flow insights
```

5. **Clean up if needed** - Remove any test sessions or mistakes.
```bash
flow delete
```

---

## Installation
Expand All @@ -77,8 +108,11 @@ For other installation methods (Go, manual), see the [Installation Guide](docs/I
| `pause` | Pause the active session. |
| `resume` | Resume a paused session. |
| `end` | Complete the session and log it. |
| `delete` | Interactively delete a session from your log. |
| `watch` | Run a watcher to get gentle, timely reminders. |

> **💡 Tip**: After ending a session, if you made a mistake, you can immediately run `flow delete` to remove it!

### Data & Analysis Commands

| Command | Description |
Expand Down
80 changes: 80 additions & 0 deletions cmd/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package cmd

import (
"bufio"
"fmt"
"os"
"strconv"
"strings"

"github.com/e6a5/flow/core"
"github.com/spf13/cobra"
)

var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Deletes a session",
Long: `Deletes a session from the log.

This command interactively lists your recent sessions and allows you to select one to delete.
You will be asked to confirm before the session is permanently removed.

Example:
flow delete`,
Run: func(cmd *cobra.Command, args []string) {
sessions, err := core.GetRecentSessions(10)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting recent sessions: %v\n", err)
os.Exit(1)
}

if len(sessions) == 0 {
fmt.Println("No sessions to delete.")
return
}

fmt.Println("Select a session to delete:")
for i, session := range sessions {
fmt.Printf("%d: %s - %s (%s)\n", i+1, session.StartTime.Format("2006-01-02 15:04"), session.Tag, session.Duration)
}

fmt.Print("Enter the number of the session to delete (or 0 to cancel): ")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
input := scanner.Text()

choice, err := strconv.Atoi(input)
if err != nil || choice < 0 || choice > len(sessions) {
fmt.Println("Invalid selection.")
return
}

if choice == 0 {
fmt.Println("Operation cancelled.")
return
}

sessionToDelete := sessions[choice-1]

fmt.Printf("\nYou have selected to delete the following session:\n")
fmt.Printf("%s - %s (%s)\n", sessionToDelete.StartTime.Format("2006-01-02 15:04"), sessionToDelete.Tag, sessionToDelete.Duration)
fmt.Print("Are you sure you want to delete this session? (y/N) ")

scanner.Scan()
confirmation := scanner.Text()

if strings.ToLower(confirmation) == "y" {
if err := core.DeleteLogEntry(sessionToDelete); err != nil {
fmt.Fprintf(os.Stderr, "Error deleting session: %v\n", err)
os.Exit(1)
}
fmt.Println("Session deleted.")
} else {
fmt.Println("Operation cancelled.")
}
},
}

func init() {
rootCmd.AddCommand(deleteCmd)
}
6 changes: 5 additions & 1 deletion core/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ func createTestConfigFile(t *testing.T, content string) (string, func()) {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write temp config file: %v", err)
}
return path, func() { _ = os.Remove(path) }
return path, func() {
if removeErr := os.Remove(path); removeErr != nil {
t.Errorf("Failed to remove test config file: %v", removeErr)
}
}
}

func TestLoadConfig_Defaults(t *testing.T) {
Expand Down
87 changes: 87 additions & 0 deletions core/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package core

import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
)

// DeleteLogEntry removes a specific log entry from the log files.
func DeleteLogEntry(entryToDelete LogEntry) error {
logPath, err := GetLogPath(entryToDelete.EndTime)
if err != nil {
return err
}

file, err := os.Open(logPath)
if err != nil {
return err
}
defer func() {
if closeErr := file.Close(); closeErr != nil {
// Log the error but don't return it as it's in a defer
fmt.Fprintf(os.Stderr, "Warning: failed to close file: %v\n", closeErr)
}
}()

tempFile, err := os.CreateTemp(filepath.Dir(logPath), "temp_log_")
if err != nil {
return err
}
defer func() {
if removeErr := os.Remove(tempFile.Name()); removeErr != nil {
// Log the error but don't return it as it's in a defer
fmt.Fprintf(os.Stderr, "Warning: failed to remove temp file: %v\n", removeErr)
}
}()

scanner := bufio.NewScanner(file)
writer := bufio.NewWriter(tempFile)
found := false

for scanner.Scan() {
line := scanner.Text()
var entry LogEntry
if err := json.Unmarshal([]byte(line), &entry); err != nil {
// Skip malformed lines
continue
}

if entry.StartTime.Equal(entryToDelete.StartTime) && entry.Tag == entryToDelete.Tag {
found = true
} else {
if _, writeErr := fmt.Fprintln(writer, line); writeErr != nil {
if closeErr := tempFile.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close temp file: %v\n", closeErr)
}
return writeErr
}
}
}

if err := scanner.Err(); err != nil {
if closeErr := tempFile.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close temp file: %v\n", closeErr)
}
return err
}

if err := writer.Flush(); err != nil {
if closeErr := tempFile.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close temp file: %v\n", closeErr)
}
return err
}

if closeErr := tempFile.Close(); closeErr != nil {
return closeErr
}

if found {
return os.Rename(tempFile.Name(), logPath)
}

return fmt.Errorf("log entry not found")
}
Loading
Loading