Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ appear in the log for the day it ends.*

![Usage](https://tools.dhruvs.space/images/hours/log-1.png)

Use `--no-truncate` to show full task names, comments, and other log text.

Logs can also be viewed via an interactive interface using the
`--interactive`/`-i` flag.

Expand Down
4 changes: 3 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ func NewRootCommand() (*cobra.Command, error) {
reportAgg bool
recordsInteractive bool
recordsOutputPlain bool
recordsNoTruncate bool
taskStatusStr string
activeTemplate string
genNumDays uint8
Expand Down Expand Up @@ -424,7 +425,7 @@ appear in the log for the day it ends.
return err
}

return ui.RenderTaskLog(db, style, os.Stdout, recordsOutputPlain, dateRange, period, taskStatus, recordsInteractive)
return ui.RenderTaskLog(db, style, os.Stdout, recordsOutputPlain, dateRange, period, taskStatus, recordsInteractive, recordsNoTruncate)
},
}

Expand Down Expand Up @@ -640,6 +641,7 @@ eg. hours active -t ' {{task}} ({{time}}) '

logCmd.Flags().BoolVarP(&recordsOutputPlain, "plain", "p", false, "whether to output logs without any formatting")
logCmd.Flags().BoolVarP(&recordsInteractive, "interactive", "i", false, "whether to view logs interactively")
logCmd.Flags().BoolVar(&recordsNoTruncate, "no-truncate", false, "whether to output logs without truncating any text")
logCmd.Flags().StringVarP(&dbPath, "dbpath", "d", defaultDBPath, "location of hours' database file")
logCmd.Flags().StringVarP(&taskStatusStr, "task-status", "s", "any", fmt.Sprintf("only show data for tasks with this status [possible values: %q]", types.ValidTaskStatusValues))
logCmd.Flags().StringVarP(&themeName, "theme", "t", defaultThemeName, `UI theme to use (run "hours themes list" for allowed values)`)
Expand Down
3 changes: 2 additions & 1 deletion internal/ui/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ func getRecordsData(
dateRange types.DateRange,
taskStatus types.TaskStatus,
plain bool,
noTruncate bool,
) tea.Cmd {
return func() tea.Msg {
var data string
Expand All @@ -198,7 +199,7 @@ func getRecordsData(
case reportAggRecords:
data, err = getReportAgg(db, style, dateRange.Start, dateRange.NumDays, taskStatus, plain)
case reportLogs:
data, err = getTaskLog(db, style, dateRange.Start, dateRange.End, taskStatus, 20, plain)
data, err = getTaskLog(db, style, dateRange.Start, dateRange.End, taskStatus, 20, plain, noTruncate)
case reportStats:
data, err = getStats(db, style, &dateRange, taskStatus, plain)
}
Expand Down
11 changes: 8 additions & 3 deletions internal/ui/initial.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,22 @@ This can be used to record details about your work on this task.`
style: style,
timeProvider: timeProvider,
activeTasksList: list.New(activeTaskItems,
newItemDelegate(style.listItemTitleColor,
newItemDelegate(
style.listItemTitleColor,
style.listItemDescColor,
lipgloss.Color(style.theme.ActiveTasks),
), listWidth, 0),
inactiveTasksList: list.New(inactiveTaskItems,
newItemDelegate(style.listItemTitleColor,
newItemDelegate(
style.listItemTitleColor,
style.listItemDescColor,
lipgloss.Color(style.theme.InactiveTasks),
), listWidth, 0),
taskMap: make(map[int]*types.Task),
taskIndexMap: make(map[int]int),
taskLogList: list.New(tasklogListItems,
newItemDelegate(style.listItemTitleColor,
newItemDelegate(
style.listItemTitleColor,
style.listItemDescColor,
lipgloss.Color(style.theme.TaskLogList),
), listWidth, 0),
Expand Down Expand Up @@ -127,6 +130,7 @@ func initialRecordsModel(
period string,
taskStatus types.TaskStatus,
plain bool,
noTruncate bool,
initialData string,
) recordsModel {
return recordsModel{
Expand All @@ -138,6 +142,7 @@ func initialRecordsModel(
period: period,
taskStatus: taskStatus,
plain: plain,
noTruncate: noTruncate,
report: initialData,
}
}
51 changes: 33 additions & 18 deletions internal/ui/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import (
)

const (
logTaskCharsBudget = 20
logCommentCharsBudget = 40
logDurationCharsBudget = 39
logTimeCharsBudget = 6
interactiveLogDayLimit = 1
logLimit = 10000
Expand All @@ -34,12 +37,13 @@ func RenderTaskLog(db *sql.DB,
period string,
taskStatus types.TaskStatus,
interactive bool,
noTruncate bool,
) error {
if interactive && dateRange.NumDays > interactiveLogDayLimit {
return fmt.Errorf("%w (limited to %d day); use non-interactive mode to see logs for a larger time period", errInteractiveModeNotApplicable, interactiveLogDayLimit)
}

log, err := getTaskLog(db, style, dateRange.Start, dateRange.End, taskStatus, logLimit, plain)
log, err := getTaskLog(db, style, dateRange.Start, dateRange.End, taskStatus, logLimit, plain, noTruncate)
if err != nil {
return fmt.Errorf("%w: %s", errCouldntGenerateLogs, err.Error())
}
Expand All @@ -54,6 +58,7 @@ func RenderTaskLog(db *sql.DB,
period,
taskStatus,
plain,
noTruncate,
log,
))
_, err := p.Run()
Expand All @@ -72,7 +77,8 @@ func getTaskLog(db *sql.DB,
end time.Time,
taskStatus types.TaskStatus,
limit int,
plain bool) (string,
plain bool,
noTruncate bool) (string,
error,
) {
entries, err := pers.FetchTLEntriesBetweenTS(db, start, end, taskStatus, limit)
Expand All @@ -91,11 +97,14 @@ func getTaskLog(db *sql.DB,
data := make([][]string, numEntriesInTable)

if len(entries) == 0 {
data[0] = []string{
utils.RightPadTrim("", 20, false),
utils.RightPadTrim("", 40, false),
utils.RightPadTrim("", 39, false),
utils.RightPadTrim("", logTimeCharsBudget, false),
data[0] = []string{"", "", "", ""}
if !noTruncate {
data[0] = []string{
utils.RightPadTrim("", logTaskCharsBudget, false),
utils.RightPadTrim("", logCommentCharsBudget, false),
utils.RightPadTrim("", logDurationCharsBudget, false),
utils.RightPadTrim("", logTimeCharsBudget, false),
}
}
}

Expand All @@ -107,24 +116,29 @@ func getTaskLog(db *sql.DB,
for i, entry := range entries {
timeSpentStr = types.HumanizeDuration(entry.SecsSpent)

taskSummary := entry.TaskSummary
comment := entry.GetComment()
duration := fmt.Sprintf("%s ... %s", entry.BeginTS.Format(timeFormat), entry.EndTS.Format(timeFormat))

if !noTruncate {
taskSummary = utils.RightPadTrim(taskSummary, logTaskCharsBudget, false)
comment = utils.RightPadTrimWithMoreLinesIndicator(comment, logCommentCharsBudget)
timeSpentStr = utils.RightPadTrim(timeSpentStr, logTimeCharsBudget, false)
}

if plain {
data[i] = []string{
utils.RightPadTrim(entry.TaskSummary, 20, false),
utils.RightPadTrimWithMoreLinesIndicator(entry.GetComment(), 40),
fmt.Sprintf("%s ... %s", entry.BeginTS.Format(timeFormat), entry.EndTS.Format(timeFormat)),
utils.RightPadTrim(timeSpentStr, logTimeCharsBudget, false),
}
data[i] = []string{taskSummary, comment, duration, timeSpentStr}
} else {
rowStyle, ok := styleCache[entry.TaskSummary]
if !ok {
rowStyle = style.getDynamicStyle(entry.TaskSummary)
styleCache[entry.TaskSummary] = rowStyle
}
data[i] = []string{
rowStyle.Render(utils.RightPadTrim(entry.TaskSummary, 20, false)),
rowStyle.Render(utils.RightPadTrimWithMoreLinesIndicator(entry.GetComment(), 40)),
rowStyle.Render(fmt.Sprintf("%s ... %s", entry.BeginTS.Format(timeFormat), entry.EndTS.Format(timeFormat))),
rowStyle.Render(utils.RightPadTrim(timeSpentStr, logTimeCharsBudget, false)),
rowStyle.Render(taskSummary),
rowStyle.Render(comment),
rowStyle.Render(duration),
rowStyle.Render(timeSpentStr),
}
}
}
Expand All @@ -136,7 +150,8 @@ func getTaskLog(db *sql.DB,
}

b := bytes.Buffer{}
table := tablewriter.NewTable(&b,
table := tablewriter.NewTable(
&b,
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
Expand Down
51 changes: 51 additions & 0 deletions internal/ui/log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package ui

import (
"database/sql"
"testing"
"time"

pers "github.com/dhth/hours/internal/persistence"
"github.com/dhth/hours/internal/types"
"github.com/dhth/hours/internal/ui/theme"
"github.com/stretchr/testify/require"
_ "modernc.org/sqlite" // sqlite driver
)

func TestGetTaskLogNoTruncate(t *testing.T) {
// GIVEN
db, err := sql.Open("sqlite", ":memory:")
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, db.Close())
})

require.NoError(t, pers.InitDB(db))

longTaskSummary := "this-is-a-very-long-task-summary"
longComment := "this is a very long comment that should not be truncated"
taskID, err := pers.InsertTask(db, longTaskSummary)
require.NoError(t, err)

beginTS := time.Date(2026, time.January, 2, 10, 0, 0, 0, time.UTC)
endTS := beginTS.Add(90 * time.Minute)
_, err = pers.InsertManualTL(db, taskID, beginTS, endTS, &longComment)
require.NoError(t, err)

style := NewStyle(theme.Default())
start := time.Date(2026, time.January, 2, 0, 0, 0, 0, time.UTC)
end := start.AddDate(0, 0, 1)

// WHEN
truncatedLog, err := getTaskLog(db, style, start, end, types.TaskStatusAny, logLimit, true, false)
require.NoError(t, err)
untruncatedLog, err := getTaskLog(db, style, start, end, types.TaskStatusAny, logLimit, true, true)
require.NoError(t, err)

// THEN
require.NotContains(t, truncatedLog, longTaskSummary)
require.NotContains(t, truncatedLog, longComment)
require.Contains(t, truncatedLog, longTaskSummary[:logTaskCharsBudget])
require.Contains(t, untruncatedLog, longTaskSummary)
require.Contains(t, untruncatedLog, longComment)
}
1 change: 1 addition & 0 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ type recordsModel struct {
period string
plain bool
taskStatus types.TaskStatus
noTruncate bool
report string
quitting bool
busy bool
Expand Down
25 changes: 17 additions & 8 deletions internal/ui/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func RenderReport(db *sql.DB,
period,
taskStatus,
plain,
false,
report,
))
_, err := p.Run()
Expand Down Expand Up @@ -127,7 +128,8 @@ func getReport(db *sql.DB, style Style, start time.Time, numDays int, taskStatus
row := make([]string, numDays)
for colIndex := range numDays {
if rowIndex >= len(reportData[colIndex]) {
row[colIndex] = fmt.Sprintf("%s %s",
row[colIndex] = fmt.Sprintf(
"%s %s",
utils.RightPadTrim("", summaryBudget, false),
utils.RightPadTrim("", reportTimeCharsBudget, false),
)
Expand All @@ -138,7 +140,8 @@ func getReport(db *sql.DB, style Style, start time.Time, numDays int, taskStatus
timeSpentStr := types.HumanizeDuration(tr.SecsSpent)

if plain {
row[colIndex] = fmt.Sprintf("%s %s",
row[colIndex] = fmt.Sprintf(
"%s %s",
utils.RightPadTrim(tr.TaskSummary, summaryBudget, false),
utils.RightPadTrim(timeSpentStr, reportTimeCharsBudget, false),
)
Expand All @@ -150,7 +153,8 @@ func getReport(db *sql.DB, style Style, start time.Time, numDays int, taskStatus
styleCache[tr.TaskSummary] = rowStyle
}

row[colIndex] = fmt.Sprintf("%s %s",
row[colIndex] = fmt.Sprintf(
"%s %s",
rowStyle.Render(utils.RightPadTrim(tr.TaskSummary, summaryBudget, false)),
rowStyle.Render(utils.RightPadTrim(timeSpentStr, reportTimeCharsBudget, false)),
)
Expand Down Expand Up @@ -187,7 +191,8 @@ func getReport(db *sql.DB, style Style, start time.Time, numDays int, taskStatus
}

b := bytes.Buffer{}
table := tablewriter.NewTable(&b,
table := tablewriter.NewTable(
&b,
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
Expand Down Expand Up @@ -288,7 +293,8 @@ func getReportAgg(db *sql.DB,
row := make([]string, numDays)
for colIndex := range numDays {
if rowIndex >= len(reportData[colIndex]) {
row[colIndex] = fmt.Sprintf("%s %s",
row[colIndex] = fmt.Sprintf(
"%s %s",
utils.RightPadTrim("", summaryBudget, false),
utils.RightPadTrim("", reportTimeCharsBudget, false),
)
Expand All @@ -299,7 +305,8 @@ func getReportAgg(db *sql.DB,
timeSpentStr := types.HumanizeDuration(tr.SecsSpent)

if plain {
row[colIndex] = fmt.Sprintf("%s %s",
row[colIndex] = fmt.Sprintf(
"%s %s",
utils.RightPadTrim(tr.TaskSummary, summaryBudget, false),
utils.RightPadTrim(timeSpentStr, reportTimeCharsBudget, false),
)
Expand All @@ -310,7 +317,8 @@ func getReportAgg(db *sql.DB,
styleCache[tr.TaskSummary] = rowStyle
}

row[colIndex] = fmt.Sprintf("%s %s",
row[colIndex] = fmt.Sprintf(
"%s %s",
rowStyle.Render(utils.RightPadTrim(tr.TaskSummary, summaryBudget, false)),
rowStyle.Render(utils.RightPadTrim(timeSpentStr, reportTimeCharsBudget, false)),
)
Expand Down Expand Up @@ -346,7 +354,8 @@ func getReportAgg(db *sql.DB,
}

b := bytes.Buffer{}
table := tablewriter.NewTable(&b,
table := tablewriter.NewTable(
&b,
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
Expand Down
4 changes: 3 additions & 1 deletion internal/ui/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func RenderStats(db *sql.DB,
period,
taskStatus,
plain,
false,
stats,
))
_, err := p.Run()
Expand Down Expand Up @@ -147,7 +148,8 @@ func getStats(db *sql.DB,
headers[i] = rs.headerStyle.Render(h)
}
b := bytes.Buffer{}
table := tablewriter.NewTable(&b,
table := tablewriter.NewTable(
&b,
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
Expand Down
Loading
Loading