foam-habits 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -0
- package/dist/app.d.ts +5 -0
- package/dist/app.js +39 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +33 -0
- package/dist/components/Cell.d.ts +7 -0
- package/dist/components/Cell.js +19 -0
- package/dist/components/HabitRow.d.ts +9 -0
- package/dist/components/HabitRow.js +31 -0
- package/dist/components/Header.d.ts +9 -0
- package/dist/components/Header.js +65 -0
- package/dist/components/Heatmap.d.ts +10 -0
- package/dist/components/Heatmap.js +14 -0
- package/dist/components/Warnings.d.ts +6 -0
- package/dist/components/Warnings.js +10 -0
- package/dist/hooks/useHabitData.d.ts +9 -0
- package/dist/hooks/useHabitData.js +33 -0
- package/dist/lib/__tests__/parser.test.d.ts +1 -0
- package/dist/lib/__tests__/parser.test.js +171 -0
- package/dist/lib/__tests__/schemas.test.d.ts +1 -0
- package/dist/lib/__tests__/schemas.test.js +49 -0
- package/dist/lib/__tests__/tracker.test.d.ts +1 -0
- package/dist/lib/__tests__/tracker.test.js +54 -0
- package/dist/lib/config.d.ts +16 -0
- package/dist/lib/config.js +71 -0
- package/dist/lib/palette.d.ts +7 -0
- package/dist/lib/palette.js +8 -0
- package/dist/lib/parser.d.ts +34 -0
- package/dist/lib/parser.js +165 -0
- package/dist/lib/schemas.d.ts +43 -0
- package/dist/lib/schemas.js +47 -0
- package/dist/lib/string-utils.d.ts +16 -0
- package/dist/lib/string-utils.js +43 -0
- package/dist/lib/tracker.d.ts +14 -0
- package/dist/lib/tracker.js +120 -0
- package/package.json +82 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { format, subDays, startOfMonth, endOfMonth, eachDayOfInterval, parseISO, isAfter, } from 'date-fns';
|
|
2
|
+
import { parseGoal, } from './schemas.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generate date range based on view args
|
|
5
|
+
*/
|
|
6
|
+
export function getDateRange(args) {
|
|
7
|
+
const today = new Date();
|
|
8
|
+
let startDate;
|
|
9
|
+
let endDate = today;
|
|
10
|
+
if (args.currentMonth) {
|
|
11
|
+
startDate = startOfMonth(today);
|
|
12
|
+
endDate = endOfMonth(today);
|
|
13
|
+
// Don't show future dates
|
|
14
|
+
if (isAfter(endDate, today)) {
|
|
15
|
+
endDate = today;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// Last N weeks (N * 7 days)
|
|
20
|
+
startDate = subDays(today, args.weeks * 7 - 1);
|
|
21
|
+
}
|
|
22
|
+
return eachDayOfInterval({ start: startDate, end: endDate }).map(date => format(date, 'yyyy-MM-dd'));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Calculate current streak for a habit
|
|
26
|
+
* Streak = consecutive days with completion, going backwards from today
|
|
27
|
+
*/
|
|
28
|
+
function calculateStreak(entries, goal, threshold) {
|
|
29
|
+
const today = new Date();
|
|
30
|
+
let streak = 0;
|
|
31
|
+
let currentDate = today;
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
33
|
+
while (true) {
|
|
34
|
+
const dateStr = format(currentDate, 'yyyy-MM-dd');
|
|
35
|
+
const value = entries[dateStr];
|
|
36
|
+
// Check if habit was completed on this day
|
|
37
|
+
const isComplete = value !== undefined && (goal === undefined || value >= goal * threshold);
|
|
38
|
+
if (!isComplete) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
streak++;
|
|
42
|
+
currentDate = subDays(currentDate, 1);
|
|
43
|
+
// Safety limit
|
|
44
|
+
if (streak > 365)
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
return streak;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Aggregate habit entries into HabitData for rendering
|
|
51
|
+
*/
|
|
52
|
+
export function aggregateHabits(entries, config, dateRange) {
|
|
53
|
+
const habits = [];
|
|
54
|
+
const today = new Date();
|
|
55
|
+
// Process each habit from config (maintains order)
|
|
56
|
+
for (const [habitKey, habitConfig] of Object.entries(config.habits)) {
|
|
57
|
+
const habitEntries = {};
|
|
58
|
+
// Find all entries for this habit
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.name === habitKey.toLowerCase()) {
|
|
61
|
+
const entryDate = parseISO(entry.date);
|
|
62
|
+
// Skip future dates
|
|
63
|
+
if (isAfter(entryDate, today))
|
|
64
|
+
continue;
|
|
65
|
+
// Only include if in date range or needed for streak calculation
|
|
66
|
+
habitEntries[entry.date] = entry.value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Filter entries to only those in the display range
|
|
70
|
+
const displayEntries = {};
|
|
71
|
+
for (const date of dateRange) {
|
|
72
|
+
displayEntries[date] = habitEntries[date];
|
|
73
|
+
}
|
|
74
|
+
const threshold = habitConfig.threshold ?? 1.0;
|
|
75
|
+
// Parse goal string (e.g., "4L" -> {value: 4, unit: "L"})
|
|
76
|
+
let goalValue;
|
|
77
|
+
let goalUnit;
|
|
78
|
+
if (habitConfig.goal) {
|
|
79
|
+
const parsed = parseGoal(habitConfig.goal);
|
|
80
|
+
goalValue = parsed.value;
|
|
81
|
+
goalUnit = parsed.unit;
|
|
82
|
+
}
|
|
83
|
+
habits.push({
|
|
84
|
+
name: habitKey,
|
|
85
|
+
emoji: habitConfig.emoji,
|
|
86
|
+
goal: goalValue,
|
|
87
|
+
unit: goalUnit,
|
|
88
|
+
threshold,
|
|
89
|
+
entries: displayEntries,
|
|
90
|
+
streak: calculateStreak(habitEntries, goalValue, threshold),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return habits;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get completion level for a single cell (0-3)
|
|
97
|
+
* 0 = not done (░), 1 = low (▒), 2 = partial (▓), 3 = complete (█)
|
|
98
|
+
*/
|
|
99
|
+
export function getCompletionLevel(value, goal, threshold = 1.0) {
|
|
100
|
+
if (value === undefined) {
|
|
101
|
+
return 0; // Not done
|
|
102
|
+
}
|
|
103
|
+
if (goal === undefined) {
|
|
104
|
+
// Boolean habit - present = complete
|
|
105
|
+
return 3;
|
|
106
|
+
}
|
|
107
|
+
const percentage = value / goal;
|
|
108
|
+
if (percentage >= threshold) {
|
|
109
|
+
return 3; // Complete (meets threshold)
|
|
110
|
+
}
|
|
111
|
+
// Scale partial levels relative to threshold
|
|
112
|
+
const partialThreshold = threshold * 0.625; // ~50% of the way to threshold
|
|
113
|
+
if (percentage >= partialThreshold) {
|
|
114
|
+
return 2; // Partial
|
|
115
|
+
}
|
|
116
|
+
if (percentage > 0) {
|
|
117
|
+
return 1; // Low
|
|
118
|
+
}
|
|
119
|
+
return 0; // Not done
|
|
120
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "foam-habits",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Terminal habit tracker for Foam daily notes with GitHub-style heatmap",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Olavo Carvalho",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/olavocarvalho/foam-habits.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/olavocarvalho/foam-habits#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/olavocarvalho/foam-habits/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"foam",
|
|
17
|
+
"habits",
|
|
18
|
+
"tracker",
|
|
19
|
+
"cli",
|
|
20
|
+
"terminal",
|
|
21
|
+
"heatmap",
|
|
22
|
+
"ink",
|
|
23
|
+
"daily-notes"
|
|
24
|
+
],
|
|
25
|
+
"bin": "dist/cli.js",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=16"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"dev": "tsc --watch",
|
|
33
|
+
"test": "tsc && ava",
|
|
34
|
+
"lint": "prettier --check . && xo",
|
|
35
|
+
"prepublishOnly": "npm run build && npm test"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"registry": "https://registry.npmjs.org/"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"date-fns": "^4.1.0",
|
|
45
|
+
"ink": "^4.1.0",
|
|
46
|
+
"ink-spinner": "^5.0.0",
|
|
47
|
+
"js-yaml": "^4.1.1",
|
|
48
|
+
"meow": "^11.0.0",
|
|
49
|
+
"react": "^18.2.0",
|
|
50
|
+
"string-width": "^8.1.0",
|
|
51
|
+
"zod": "^4.3.4"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@sindresorhus/tsconfig": "^3.0.1",
|
|
55
|
+
"@types/js-yaml": "^4.0.9",
|
|
56
|
+
"@types/react": "^18.0.32",
|
|
57
|
+
"@vdemedes/prettier-config": "^2.0.1",
|
|
58
|
+
"ava": "^5.2.0",
|
|
59
|
+
"chalk": "^5.2.0",
|
|
60
|
+
"eslint-config-xo-react": "^0.27.0",
|
|
61
|
+
"eslint-plugin-react": "^7.32.2",
|
|
62
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
63
|
+
"ink-testing-library": "^3.0.0",
|
|
64
|
+
"prettier": "^2.8.7",
|
|
65
|
+
"ts-node": "^10.9.1",
|
|
66
|
+
"typescript": "^5.0.3",
|
|
67
|
+
"xo": "^0.53.1"
|
|
68
|
+
},
|
|
69
|
+
"ava": {
|
|
70
|
+
"files": [
|
|
71
|
+
"dist/lib/__tests__/*.test.js"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"xo": {
|
|
75
|
+
"extends": "xo-react",
|
|
76
|
+
"prettier": true,
|
|
77
|
+
"rules": {
|
|
78
|
+
"react/prop-types": "off"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"prettier": "@vdemedes/prettier-config"
|
|
82
|
+
}
|