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.
@@ -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
+ }