epoch-tui 0.1.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.
Files changed (147) hide show
  1. package/dist/App.d.ts +3 -0
  2. package/dist/App.js +38 -0
  3. package/dist/components/calendar/CalendarPane.d.ts +2 -0
  4. package/dist/components/calendar/CalendarPane.js +91 -0
  5. package/dist/components/calendar/DayCell.d.ts +7 -0
  6. package/dist/components/calendar/DayCell.js +26 -0
  7. package/dist/components/calendar/MonthView.d.ts +7 -0
  8. package/dist/components/calendar/MonthView.js +10 -0
  9. package/dist/components/common/BorderedBox.d.ts +18 -0
  10. package/dist/components/common/BorderedBox.js +29 -0
  11. package/dist/components/common/ClearTimelineDialog.d.ts +2 -0
  12. package/dist/components/common/ClearTimelineDialog.js +33 -0
  13. package/dist/components/common/FullscreenBackground.d.ts +11 -0
  14. package/dist/components/common/FullscreenBackground.js +12 -0
  15. package/dist/components/common/HelpDialog.d.ts +2 -0
  16. package/dist/components/common/HelpDialog.js +40 -0
  17. package/dist/components/common/Modal.d.ts +10 -0
  18. package/dist/components/common/Modal.js +15 -0
  19. package/dist/components/common/Separator.d.ts +8 -0
  20. package/dist/components/common/Separator.js +10 -0
  21. package/dist/components/common/ThemeDialog.d.ts +2 -0
  22. package/dist/components/common/ThemeDialog.js +117 -0
  23. package/dist/components/common/ThemedScreen.d.ts +22 -0
  24. package/dist/components/common/ThemedScreen.js +36 -0
  25. package/dist/components/common/ThemedText.d.ts +11 -0
  26. package/dist/components/common/ThemedText.js +20 -0
  27. package/dist/components/layout/Pane.d.ts +12 -0
  28. package/dist/components/layout/Pane.js +10 -0
  29. package/dist/components/layout/ThreeColumnLayout.d.ts +13 -0
  30. package/dist/components/layout/ThreeColumnLayout.js +22 -0
  31. package/dist/components/overview/OverviewScreen.d.ts +2 -0
  32. package/dist/components/overview/OverviewScreen.js +138 -0
  33. package/dist/components/tasks/TaskHeader.d.ts +7 -0
  34. package/dist/components/tasks/TaskHeader.js +8 -0
  35. package/dist/components/tasks/TaskItem.d.ts +10 -0
  36. package/dist/components/tasks/TaskItem.js +25 -0
  37. package/dist/components/tasks/TaskList.d.ts +11 -0
  38. package/dist/components/tasks/TaskList.js +11 -0
  39. package/dist/components/tasks/TasksPane.d.ts +2 -0
  40. package/dist/components/tasks/TasksPane.js +410 -0
  41. package/dist/components/timeline/TimelineEntry.d.ts +9 -0
  42. package/dist/components/timeline/TimelineEntry.js +26 -0
  43. package/dist/components/timeline/TimelinePane.d.ts +2 -0
  44. package/dist/components/timeline/TimelinePane.js +78 -0
  45. package/dist/contexts/AppContext.d.ts +47 -0
  46. package/dist/contexts/AppContext.js +104 -0
  47. package/dist/contexts/StorageContext.d.ts +15 -0
  48. package/dist/contexts/StorageContext.js +83 -0
  49. package/dist/contexts/ThemeContext.d.ts +15 -0
  50. package/dist/contexts/ThemeContext.js +44 -0
  51. package/dist/hooks/useKeyboardNav.d.ts +1 -0
  52. package/dist/hooks/useKeyboardNav.js +89 -0
  53. package/dist/hooks/useTerminalSize.d.ts +9 -0
  54. package/dist/hooks/useTerminalSize.js +34 -0
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +8 -0
  57. package/dist/services/calendarService.d.ts +18 -0
  58. package/dist/services/calendarService.js +57 -0
  59. package/dist/services/storage.d.ts +14 -0
  60. package/dist/services/storage.js +130 -0
  61. package/dist/services/taskService.d.ts +17 -0
  62. package/dist/services/taskService.js +106 -0
  63. package/dist/services/timelineService.d.ts +25 -0
  64. package/dist/services/timelineService.js +78 -0
  65. package/dist/themes/amazon.d.ts +2 -0
  66. package/dist/themes/amazon.js +37 -0
  67. package/dist/themes/amazonLight.d.ts +2 -0
  68. package/dist/themes/amazonLight.js +38 -0
  69. package/dist/themes/apple.d.ts +2 -0
  70. package/dist/themes/apple.js +38 -0
  71. package/dist/themes/appleLight.d.ts +2 -0
  72. package/dist/themes/appleLight.js +38 -0
  73. package/dist/themes/atomOneDark.d.ts +2 -0
  74. package/dist/themes/atomOneDark.js +37 -0
  75. package/dist/themes/atomOneLight.d.ts +2 -0
  76. package/dist/themes/atomOneLight.js +38 -0
  77. package/dist/themes/batman.d.ts +2 -0
  78. package/dist/themes/batman.js +37 -0
  79. package/dist/themes/catppuccin.d.ts +2 -0
  80. package/dist/themes/catppuccin.js +37 -0
  81. package/dist/themes/catppuccinLatte.d.ts +2 -0
  82. package/dist/themes/catppuccinLatte.js +38 -0
  83. package/dist/themes/claude.d.ts +2 -0
  84. package/dist/themes/claude.js +48 -0
  85. package/dist/themes/claudeCode.d.ts +2 -0
  86. package/dist/themes/claudeCode.js +47 -0
  87. package/dist/themes/cursor.d.ts +2 -0
  88. package/dist/themes/cursor.js +38 -0
  89. package/dist/themes/cursorLight.d.ts +2 -0
  90. package/dist/themes/cursorLight.js +38 -0
  91. package/dist/themes/dark.d.ts +2 -0
  92. package/dist/themes/dark.js +38 -0
  93. package/dist/themes/githubDark.d.ts +2 -0
  94. package/dist/themes/githubDark.js +37 -0
  95. package/dist/themes/githubLight.d.ts +2 -0
  96. package/dist/themes/githubLight.js +38 -0
  97. package/dist/themes/index.d.ts +9 -0
  98. package/dist/themes/index.js +83 -0
  99. package/dist/themes/instagram.d.ts +2 -0
  100. package/dist/themes/instagram.js +37 -0
  101. package/dist/themes/instagramLight.d.ts +2 -0
  102. package/dist/themes/instagramLight.js +38 -0
  103. package/dist/themes/intellij.d.ts +2 -0
  104. package/dist/themes/intellij.js +37 -0
  105. package/dist/themes/intellijLight.d.ts +2 -0
  106. package/dist/themes/intellijLight.js +38 -0
  107. package/dist/themes/light.d.ts +2 -0
  108. package/dist/themes/light.js +38 -0
  109. package/dist/themes/nord.d.ts +2 -0
  110. package/dist/themes/nord.js +37 -0
  111. package/dist/themes/nordLight.d.ts +2 -0
  112. package/dist/themes/nordLight.js +38 -0
  113. package/dist/themes/postman.d.ts +2 -0
  114. package/dist/themes/postman.js +37 -0
  115. package/dist/themes/postmanLight.d.ts +2 -0
  116. package/dist/themes/postmanLight.js +38 -0
  117. package/dist/themes/spiderman.d.ts +2 -0
  118. package/dist/themes/spiderman.js +37 -0
  119. package/dist/themes/terminal.d.ts +2 -0
  120. package/dist/themes/terminal.js +37 -0
  121. package/dist/themes/ubuntu.d.ts +2 -0
  122. package/dist/themes/ubuntu.js +37 -0
  123. package/dist/themes/ubuntuLight.d.ts +2 -0
  124. package/dist/themes/ubuntuLight.js +38 -0
  125. package/dist/themes/x.d.ts +2 -0
  126. package/dist/themes/x.js +38 -0
  127. package/dist/themes/xLight.d.ts +2 -0
  128. package/dist/themes/xLight.js +38 -0
  129. package/dist/types/calendar.d.ts +19 -0
  130. package/dist/types/calendar.js +1 -0
  131. package/dist/types/storage.d.ts +21 -0
  132. package/dist/types/storage.js +1 -0
  133. package/dist/types/task.d.ts +21 -0
  134. package/dist/types/task.js +1 -0
  135. package/dist/types/theme.d.ts +38 -0
  136. package/dist/types/theme.js +1 -0
  137. package/dist/types/timeline.d.ts +23 -0
  138. package/dist/types/timeline.js +9 -0
  139. package/dist/utils/date.d.ts +7 -0
  140. package/dist/utils/date.js +37 -0
  141. package/dist/utils/logger.d.ts +3 -0
  142. package/dist/utils/logger.js +39 -0
  143. package/dist/utils/tree.d.ts +11 -0
  144. package/dist/utils/tree.js +64 -0
  145. package/dist/utils/validation.d.ts +7 -0
  146. package/dist/utils/validation.js +35 -0
  147. package/package.json +44 -0
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useState, useRef, useCallback, } from "react";
3
+ import { useStorage } from "./StorageContext";
4
+ const AppContext = createContext(undefined);
5
+ export const AppProvider = ({ children }) => {
6
+ const { data, save, saveNow } = useStorage();
7
+ const today = new Date();
8
+ const [selectedDate, setSelectedDate] = useState({
9
+ year: today.getFullYear(),
10
+ month: today.getMonth(),
11
+ day: today.getDate(),
12
+ });
13
+ const [tasks, setTasks] = useState({});
14
+ const [timeline, setTimeline] = useState({});
15
+ const [activePane, setActivePane] = useState("tasks");
16
+ const [showHelp, setShowHelp] = useState(false);
17
+ // Use ref for isInputMode to avoid React state timing issues
18
+ const isInputModeRef = useRef(false);
19
+ const [isInputModeState, setIsInputModeState] = useState(false);
20
+ const [showOverview, setShowOverview] = useState(false);
21
+ // Use ref for immediate updates to avoid React state timing issues with useInput hooks
22
+ const setIsInputMode = useCallback((mode) => {
23
+ isInputModeRef.current = mode;
24
+ setIsInputModeState(mode);
25
+ }, []);
26
+ // Read from ref for immediate access in useInput hooks
27
+ const isInputMode = isInputModeRef.current;
28
+ const [overviewMonth, setOverviewMonth] = useState({
29
+ year: today.getFullYear(),
30
+ month: today.getMonth(),
31
+ });
32
+ const [exitConfirmation, setExitConfirmation] = useState(false);
33
+ const [showThemeDialog, setShowThemeDialog] = useState(false);
34
+ const [showClearTimelineDialog, setShowClearTimelineDialog] = useState(false);
35
+ // Function to clear timeline for a specific date
36
+ const clearTimelineForDate = useCallback((dateStr) => {
37
+ setTimeline((prev) => {
38
+ const newTimeline = { ...prev };
39
+ delete newTimeline[dateStr];
40
+ return newTimeline;
41
+ });
42
+ }, []);
43
+ // Track if initial data has been loaded to prevent save loop
44
+ const initialLoadDone = useRef(false);
45
+ const dataRef = useRef(data);
46
+ const saveRef = useRef(save);
47
+ // Keep refs updated
48
+ useEffect(() => {
49
+ dataRef.current = data;
50
+ saveRef.current = save;
51
+ }, [data, save]);
52
+ // Load data from storage (only once)
53
+ useEffect(() => {
54
+ if (data && !initialLoadDone.current) {
55
+ setTasks(data.tasks);
56
+ setTimeline(data.timeline);
57
+ initialLoadDone.current = true;
58
+ }
59
+ }, [data]);
60
+ // Save when tasks or timeline change (only after initial load)
61
+ useEffect(() => {
62
+ if (initialLoadDone.current && dataRef.current) {
63
+ saveRef.current({
64
+ ...dataRef.current,
65
+ tasks,
66
+ timeline,
67
+ });
68
+ }
69
+ }, [tasks, timeline]);
70
+ return (_jsx(AppContext.Provider, { value: {
71
+ selectedDate,
72
+ setSelectedDate,
73
+ tasks,
74
+ setTasks,
75
+ timeline,
76
+ setTimeline,
77
+ activePane,
78
+ setActivePane,
79
+ showHelp,
80
+ setShowHelp,
81
+ isInputMode,
82
+ setIsInputMode,
83
+ showOverview,
84
+ setShowOverview,
85
+ overviewMonth,
86
+ setOverviewMonth,
87
+ exitConfirmation,
88
+ setExitConfirmation,
89
+ showThemeDialog,
90
+ setShowThemeDialog,
91
+ showClearTimelineDialog,
92
+ setShowClearTimelineDialog,
93
+ clearTimelineForDate,
94
+ isModalOpen: showHelp || showThemeDialog || showOverview || showClearTimelineDialog,
95
+ saveNow,
96
+ }, children: children }));
97
+ };
98
+ export const useApp = () => {
99
+ const context = useContext(AppContext);
100
+ if (!context) {
101
+ throw new Error("useApp must be used within AppProvider");
102
+ }
103
+ return context;
104
+ };
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import type { StorageSchema } from '../types/storage';
3
+ interface StorageContextType {
4
+ data: StorageSchema | null;
5
+ isLoading: boolean;
6
+ error: Error | null;
7
+ save: (data: StorageSchema) => Promise<void>;
8
+ saveNow: () => Promise<void>;
9
+ }
10
+ interface StorageProviderProps {
11
+ children: React.ReactNode;
12
+ }
13
+ export declare const StorageProvider: React.FC<StorageProviderProps>;
14
+ export declare const useStorage: () => StorageContextType;
15
+ export {};
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useRef, useState, useCallback, } from 'react';
3
+ import { storageService } from '../services/storage';
4
+ const StorageContext = createContext(undefined);
5
+ export const StorageProvider = ({ children, }) => {
6
+ const [data, setData] = useState(null);
7
+ const [isLoading, setIsLoading] = useState(true);
8
+ const [error, setError] = useState(null);
9
+ const saveTimeoutRef = useRef(undefined);
10
+ // Keep a ref to always have the latest data for sync saves
11
+ const latestDataRef = useRef(null);
12
+ // Load data on mount
13
+ useEffect(() => {
14
+ const loadData = async () => {
15
+ try {
16
+ setIsLoading(true);
17
+ const loadedData = await storageService.load();
18
+ setData(loadedData);
19
+ latestDataRef.current = loadedData;
20
+ setError(null);
21
+ }
22
+ catch (err) {
23
+ setError(err instanceof Error ? err : new Error('Unknown error'));
24
+ }
25
+ finally {
26
+ setIsLoading(false);
27
+ }
28
+ };
29
+ loadData();
30
+ }, []);
31
+ // Debounced save
32
+ const save = useCallback(async (newData) => {
33
+ setData(newData);
34
+ latestDataRef.current = newData;
35
+ if (saveTimeoutRef.current) {
36
+ clearTimeout(saveTimeoutRef.current);
37
+ }
38
+ saveTimeoutRef.current = setTimeout(async () => {
39
+ try {
40
+ await storageService.save(newData);
41
+ }
42
+ catch (err) {
43
+ setError(err instanceof Error ? err : new Error('Failed to save'));
44
+ }
45
+ }, 500);
46
+ }, []);
47
+ // Immediate save (for exit)
48
+ const saveNow = useCallback(async () => {
49
+ if (saveTimeoutRef.current) {
50
+ clearTimeout(saveTimeoutRef.current);
51
+ }
52
+ if (latestDataRef.current) {
53
+ try {
54
+ await storageService.save(latestDataRef.current);
55
+ }
56
+ catch (err) {
57
+ console.error('Failed to save:', err);
58
+ }
59
+ }
60
+ }, []);
61
+ // Save on unmount using the ref (always has latest data)
62
+ useEffect(() => {
63
+ return () => {
64
+ if (saveTimeoutRef.current) {
65
+ clearTimeout(saveTimeoutRef.current);
66
+ }
67
+ if (latestDataRef.current) {
68
+ // Use sync write for unmount to ensure data is saved
69
+ storageService.save(latestDataRef.current).catch(err => {
70
+ console.error('Failed to save on unmount:', err);
71
+ });
72
+ }
73
+ };
74
+ }, []);
75
+ return (_jsx(StorageContext.Provider, { value: { data, isLoading, error, save, saveNow }, children: children }));
76
+ };
77
+ export const useStorage = () => {
78
+ const context = useContext(StorageContext);
79
+ if (!context) {
80
+ throw new Error('useStorage must be used within StorageProvider');
81
+ }
82
+ return context;
83
+ };
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import type { Theme } from "../types/theme";
3
+ interface ThemeContextType {
4
+ theme: Theme;
5
+ themeName: string;
6
+ setTheme: (name: string) => void;
7
+ availableThemes: string[];
8
+ }
9
+ interface ThemeProviderProps {
10
+ children: React.ReactNode;
11
+ initialTheme?: string;
12
+ }
13
+ export declare const ThemeProvider: React.FC<ThemeProviderProps>;
14
+ export declare const useTheme: () => ThemeContextType;
15
+ export {};
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState, useEffect } from "react";
3
+ import { getTheme, getThemeNames } from "../themes";
4
+ import { useStorage } from "./StorageContext";
5
+ const ThemeContext = createContext(undefined);
6
+ export const ThemeProvider = ({ children, initialTheme = "dark", }) => {
7
+ const { data, save } = useStorage();
8
+ const [themeName, setThemeName] = useState(initialTheme);
9
+ const theme = getTheme(themeName);
10
+ // Load theme from storage when data is available
11
+ useEffect(() => {
12
+ if (data?.settings?.theme) {
13
+ setThemeName(data.settings.theme);
14
+ }
15
+ }, [data?.settings?.theme]);
16
+ const handleSetTheme = (name) => {
17
+ if (getThemeNames().includes(name)) {
18
+ setThemeName(name);
19
+ // Save theme to storage
20
+ if (data) {
21
+ save({
22
+ ...data,
23
+ settings: {
24
+ ...data.settings,
25
+ theme: name,
26
+ },
27
+ });
28
+ }
29
+ }
30
+ };
31
+ return (_jsx(ThemeContext.Provider, { value: {
32
+ theme,
33
+ themeName,
34
+ setTheme: handleSetTheme,
35
+ availableThemes: getThemeNames(),
36
+ }, children: children }));
37
+ };
38
+ export const useTheme = () => {
39
+ const context = useContext(ThemeContext);
40
+ if (!context) {
41
+ throw new Error("useTheme must be used within ThemeProvider");
42
+ }
43
+ return context;
44
+ };
@@ -0,0 +1 @@
1
+ export declare const useKeyboardNav: () => void;
@@ -0,0 +1,89 @@
1
+ import { useInput } from 'ink';
2
+ import { useApp } from '../contexts/AppContext';
3
+ import { useEffect } from 'react';
4
+ export const useKeyboardNav = () => {
5
+ const { showHelp, setShowHelp, activePane, setActivePane, isInputMode, showOverview, setShowOverview, overviewMonth, setOverviewMonth, exitConfirmation, setExitConfirmation, showThemeDialog, setShowThemeDialog, showClearTimelineDialog, setShowClearTimelineDialog, saveNow } = useApp();
6
+ // Auto-reset exit confirmation after 3 seconds
7
+ useEffect(() => {
8
+ if (exitConfirmation) {
9
+ const timer = setTimeout(() => {
10
+ setExitConfirmation(false);
11
+ }, 3000);
12
+ return () => clearTimeout(timer);
13
+ }
14
+ }, [exitConfirmation, setExitConfirmation]);
15
+ // Global input handler - inactive when in input mode or dialogs open
16
+ const isActive = !isInputMode && !showThemeDialog && !showClearTimelineDialog;
17
+ useInput((input, key) => {
18
+ // Handle Ctrl+C with confirmation (always active for exit)
19
+ if (key.ctrl && input === 'c') {
20
+ if (exitConfirmation) {
21
+ // Save data before exiting
22
+ saveNow().then(() => {
23
+ // Unmount the Ink app first
24
+ const inkApp = global.__inkApp;
25
+ if (inkApp) {
26
+ inkApp.unmount();
27
+ }
28
+ // Clear the terminal completely
29
+ setTimeout(() => {
30
+ console.clear();
31
+ process.stdout.write('\x1Bc'); // Reset terminal
32
+ process.exit(0);
33
+ }, 100); // Give Ink time to unmount
34
+ }).catch(() => {
35
+ // Unmount the Ink app first even on error
36
+ const inkApp = global.__inkApp;
37
+ if (inkApp) {
38
+ inkApp.unmount();
39
+ }
40
+ // Clear the terminal completely
41
+ setTimeout(() => {
42
+ console.clear();
43
+ process.stdout.write('\x1Bc'); // Reset terminal
44
+ process.exit(0);
45
+ }, 100); // Give Ink time to unmount
46
+ });
47
+ return;
48
+ }
49
+ else {
50
+ setExitConfirmation(true);
51
+ return;
52
+ }
53
+ }
54
+ // Shift+; (colon) to toggle overview
55
+ if (input === ':') {
56
+ setShowOverview(!showOverview);
57
+ return;
58
+ }
59
+ if (input === '?') {
60
+ setShowHelp(!showHelp);
61
+ return;
62
+ }
63
+ if (key.ctrl && input === 't') {
64
+ setShowThemeDialog(true);
65
+ return;
66
+ }
67
+ // 'C' (shift+c) to clear timeline (when timeline pane is focused)
68
+ if (input === 'C' && activePane === 'timeline') {
69
+ setShowClearTimelineDialog(true);
70
+ return;
71
+ }
72
+ // Pane switching
73
+ if (input === '1') {
74
+ setActivePane('calendar');
75
+ return;
76
+ }
77
+ if (input === '2' || (key.tab && !key.shift)) {
78
+ setActivePane('tasks');
79
+ return;
80
+ }
81
+ if (input === '3' || (key.tab && key.shift)) {
82
+ setActivePane('timeline');
83
+ return;
84
+ }
85
+ }, {
86
+ // Disable this hook when in input mode or dialogs are open to prevent interference with TextInput
87
+ isActive: isActive
88
+ });
89
+ };
@@ -0,0 +1,9 @@
1
+ interface TerminalSize {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ /**
6
+ * Hook to track terminal dimensions and automatically update on resize
7
+ */
8
+ export declare const useTerminalSize: () => TerminalSize;
9
+ export {};
@@ -0,0 +1,34 @@
1
+ import { useState, useEffect } from "react";
2
+ import { useStdout } from "ink";
3
+ /**
4
+ * Hook to track terminal dimensions and automatically update on resize
5
+ */
6
+ export const useTerminalSize = () => {
7
+ const { stdout } = useStdout();
8
+ const [size, setSize] = useState({
9
+ width: stdout?.columns || 100,
10
+ height: stdout?.rows || 30,
11
+ });
12
+ useEffect(() => {
13
+ if (!stdout)
14
+ return;
15
+ // Update size immediately in case it changed
16
+ setSize({
17
+ width: stdout.columns || 100,
18
+ height: stdout.rows || 30,
19
+ });
20
+ // Listen for resize events
21
+ const handleResize = () => {
22
+ setSize({
23
+ width: stdout.columns || 100,
24
+ height: stdout.rows || 30,
25
+ });
26
+ };
27
+ stdout.on("resize", handleResize);
28
+ // Cleanup listener on unmount
29
+ return () => {
30
+ stdout.off("resize", handleResize);
31
+ };
32
+ }, [stdout]);
33
+ return size;
34
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { render } from "ink";
4
+ import App from "./App";
5
+ console.clear();
6
+ const app = render(_jsx(App, {}), { exitOnCtrlC: false });
7
+ // Make the app instance globally accessible for clean exit
8
+ global.__inkApp = app;
@@ -0,0 +1,18 @@
1
+ import type { CalendarView, CalendarDate } from '../types/calendar';
2
+ import type { TaskTree } from '../types/task';
3
+ export declare class CalendarService {
4
+ generateMonthView(year: number, month: number, selectedDate: CalendarDate, tasks: TaskTree): CalendarView;
5
+ private createCalendarDay;
6
+ private isSameDay;
7
+ getNextMonth(year: number, month: number): {
8
+ year: number;
9
+ month: number;
10
+ };
11
+ getPreviousMonth(year: number, month: number): {
12
+ year: number;
13
+ month: number;
14
+ };
15
+ getDayOfWeek(date: Date): number;
16
+ getMonthName(month: number): string;
17
+ }
18
+ export declare const calendarService: CalendarService;
@@ -0,0 +1,57 @@
1
+ import { generateMonthCalendar, getDateString, isToday } from '../utils/date';
2
+ export class CalendarService {
3
+ generateMonthView(year, month, selectedDate, tasks) {
4
+ const weeks = generateMonthCalendar(year, month);
5
+ const calendarDays = weeks.map(week => week.map(date => this.createCalendarDay(date, selectedDate, tasks, month)));
6
+ return {
7
+ year,
8
+ month,
9
+ weeks: calendarDays,
10
+ };
11
+ }
12
+ createCalendarDay(date, selectedDate, tasks, currentMonth) {
13
+ const dateString = getDateString(date);
14
+ const dayTasks = tasks[dateString] || [];
15
+ return {
16
+ date: {
17
+ year: date.getFullYear(),
18
+ month: date.getMonth(),
19
+ day: date.getDate(),
20
+ },
21
+ dateString,
22
+ isToday: isToday(date),
23
+ isSelected: this.isSameDay(date, selectedDate),
24
+ isCurrentMonth: date.getMonth() === currentMonth,
25
+ hasTasks: dayTasks.length > 0,
26
+ taskCount: dayTasks.length,
27
+ };
28
+ }
29
+ isSameDay(date, calendarDate) {
30
+ return (date.getDate() === calendarDate.day &&
31
+ date.getMonth() === calendarDate.month &&
32
+ date.getFullYear() === calendarDate.year);
33
+ }
34
+ getNextMonth(year, month) {
35
+ if (month === 11) {
36
+ return { year: year + 1, month: 0 };
37
+ }
38
+ return { year, month: month + 1 };
39
+ }
40
+ getPreviousMonth(year, month) {
41
+ if (month === 0) {
42
+ return { year: year - 1, month: 11 };
43
+ }
44
+ return { year, month: month - 1 };
45
+ }
46
+ getDayOfWeek(date) {
47
+ return date.getDay();
48
+ }
49
+ getMonthName(month) {
50
+ const months = [
51
+ 'January', 'February', 'March', 'April', 'May', 'June',
52
+ 'July', 'August', 'September', 'October', 'November', 'December',
53
+ ];
54
+ return months[month];
55
+ }
56
+ }
57
+ export const calendarService = new CalendarService();
@@ -0,0 +1,14 @@
1
+ import type { StorageSchema } from '../types/storage';
2
+ export declare class StorageService {
3
+ private filePath;
4
+ constructor(filePath?: string);
5
+ load(): Promise<StorageSchema>;
6
+ save(data: StorageSchema): Promise<void>;
7
+ backup(): Promise<void>;
8
+ getStoragePath(): string;
9
+ private hydrateDates;
10
+ private hydrateTask;
11
+ private serializeDates;
12
+ private serializeTask;
13
+ }
14
+ export declare const storageService: StorageService;
@@ -0,0 +1,130 @@
1
+ import { promises as fs } from 'fs';
2
+ import { dirname } from 'path';
3
+ import { homedir } from 'os';
4
+ const getStoragePath = () => {
5
+ const home = homedir();
6
+ const platform = process.platform;
7
+ if (platform === 'darwin') {
8
+ return `${home}/Library/Application Support/epoch/data.json`;
9
+ }
10
+ else if (platform === 'linux') {
11
+ return `${home}/.config/epoch/data.json`;
12
+ }
13
+ else if (platform === 'win32') {
14
+ const appData = process.env.APPDATA || `${home}\\AppData\\Roaming`;
15
+ return `${appData}\\epoch\\data.json`;
16
+ }
17
+ return `${home}/.epoch/data.json`;
18
+ };
19
+ const getDefaultSchema = () => ({
20
+ version: '1.0.0',
21
+ tasks: {},
22
+ timeline: {},
23
+ settings: {
24
+ theme: 'dark',
25
+ defaultStartTime: 'now',
26
+ dateFormat: 'MMMM do, yyyy',
27
+ timeFormat: '12h',
28
+ },
29
+ });
30
+ export class StorageService {
31
+ constructor(filePath) {
32
+ this.filePath = filePath || getStoragePath();
33
+ }
34
+ async load() {
35
+ try {
36
+ const data = await fs.readFile(this.filePath, 'utf-8');
37
+ const parsed = JSON.parse(data);
38
+ // Convert date strings back to Date objects
39
+ return this.hydrateDates(parsed);
40
+ }
41
+ catch (error) {
42
+ if (error.code === 'ENOENT') {
43
+ return getDefaultSchema();
44
+ }
45
+ console.error('Failed to load storage:', error);
46
+ return getDefaultSchema();
47
+ }
48
+ }
49
+ async save(data) {
50
+ try {
51
+ const dir = dirname(this.filePath);
52
+ await fs.mkdir(dir, { recursive: true });
53
+ // Convert Date objects to ISO strings for JSON serialization
54
+ const serialized = this.serializeDates(data);
55
+ await fs.writeFile(this.filePath, JSON.stringify(serialized, null, 2), 'utf-8');
56
+ }
57
+ catch (error) {
58
+ console.error('Failed to save storage:', error);
59
+ }
60
+ }
61
+ async backup() {
62
+ try {
63
+ const timestamp = new Date().toISOString().replace(/:/g, '-');
64
+ const backupPath = `${this.filePath}.backup-${timestamp}`;
65
+ const data = await fs.readFile(this.filePath, 'utf-8');
66
+ await fs.writeFile(backupPath, data, 'utf-8');
67
+ }
68
+ catch (error) {
69
+ console.error('Failed to backup storage:', error);
70
+ }
71
+ }
72
+ getStoragePath() {
73
+ return this.filePath;
74
+ }
75
+ hydrateDates(data) {
76
+ if (data.tasks) {
77
+ Object.keys(data.tasks).forEach(date => {
78
+ data.tasks[date] = data.tasks[date].map((task) => this.hydrateTask(task));
79
+ });
80
+ }
81
+ if (data.timeline) {
82
+ Object.keys(data.timeline).forEach(date => {
83
+ data.timeline[date] = data.timeline[date].map((event) => ({
84
+ ...event,
85
+ timestamp: new Date(event.timestamp),
86
+ previousState: event.previousState,
87
+ newState: event.newState,
88
+ }));
89
+ });
90
+ }
91
+ return data;
92
+ }
93
+ hydrateTask(task) {
94
+ return {
95
+ ...task,
96
+ createdAt: new Date(task.createdAt),
97
+ updatedAt: new Date(task.updatedAt),
98
+ startTime: task.startTime ? new Date(task.startTime) : undefined,
99
+ endTime: task.endTime ? new Date(task.endTime) : undefined,
100
+ children: task.children ? task.children.map((child) => this.hydrateTask(child)) : [],
101
+ };
102
+ }
103
+ serializeDates(data) {
104
+ return {
105
+ ...data,
106
+ tasks: Object.keys(data.tasks).reduce((acc, date) => {
107
+ acc[date] = data.tasks[date].map(task => this.serializeTask(task));
108
+ return acc;
109
+ }, {}),
110
+ timeline: Object.keys(data.timeline).reduce((acc, date) => {
111
+ acc[date] = data.timeline[date].map(event => ({
112
+ ...event,
113
+ timestamp: event.timestamp.toISOString(),
114
+ }));
115
+ return acc;
116
+ }, {}),
117
+ };
118
+ }
119
+ serializeTask(task) {
120
+ return {
121
+ ...task,
122
+ createdAt: task.createdAt.toISOString(),
123
+ updatedAt: task.updatedAt.toISOString(),
124
+ startTime: task.startTime ? task.startTime.toISOString() : undefined,
125
+ endTime: task.endTime ? task.endTime.toISOString() : undefined,
126
+ children: task.children ? task.children.map((child) => this.serializeTask(child)) : [],
127
+ };
128
+ }
129
+ }
130
+ export const storageService = new StorageService();
@@ -0,0 +1,17 @@
1
+ import type { Task, TaskState, TaskTree } from '../types/task';
2
+ export declare class TaskService {
3
+ createTask(title: string, date: string, state?: TaskState): Task;
4
+ updateTask(tasks: TaskTree, taskId: string, updates: Partial<Task>): TaskTree;
5
+ deleteTask(tasks: TaskTree, taskId: string): TaskTree;
6
+ addSubtask(tasks: TaskTree, parentId: string, title: string): TaskTree;
7
+ changeTaskState(tasks: TaskTree, taskId: string, newState: TaskState): TaskTree;
8
+ startTask(tasks: TaskTree, taskId: string, startTime?: Date): TaskTree;
9
+ getTasksForDate(tasks: TaskTree, date: string): Task[];
10
+ getAllTasks(tasks: TaskTree): Task[];
11
+ getTaskStats(tasks: TaskTree, date: string): {
12
+ total: number;
13
+ completed: number;
14
+ percentage: number;
15
+ };
16
+ }
17
+ export declare const taskService: TaskService;