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.
- package/dist/App.d.ts +3 -0
- package/dist/App.js +38 -0
- package/dist/components/calendar/CalendarPane.d.ts +2 -0
- package/dist/components/calendar/CalendarPane.js +91 -0
- package/dist/components/calendar/DayCell.d.ts +7 -0
- package/dist/components/calendar/DayCell.js +26 -0
- package/dist/components/calendar/MonthView.d.ts +7 -0
- package/dist/components/calendar/MonthView.js +10 -0
- package/dist/components/common/BorderedBox.d.ts +18 -0
- package/dist/components/common/BorderedBox.js +29 -0
- package/dist/components/common/ClearTimelineDialog.d.ts +2 -0
- package/dist/components/common/ClearTimelineDialog.js +33 -0
- package/dist/components/common/FullscreenBackground.d.ts +11 -0
- package/dist/components/common/FullscreenBackground.js +12 -0
- package/dist/components/common/HelpDialog.d.ts +2 -0
- package/dist/components/common/HelpDialog.js +40 -0
- package/dist/components/common/Modal.d.ts +10 -0
- package/dist/components/common/Modal.js +15 -0
- package/dist/components/common/Separator.d.ts +8 -0
- package/dist/components/common/Separator.js +10 -0
- package/dist/components/common/ThemeDialog.d.ts +2 -0
- package/dist/components/common/ThemeDialog.js +117 -0
- package/dist/components/common/ThemedScreen.d.ts +22 -0
- package/dist/components/common/ThemedScreen.js +36 -0
- package/dist/components/common/ThemedText.d.ts +11 -0
- package/dist/components/common/ThemedText.js +20 -0
- package/dist/components/layout/Pane.d.ts +12 -0
- package/dist/components/layout/Pane.js +10 -0
- package/dist/components/layout/ThreeColumnLayout.d.ts +13 -0
- package/dist/components/layout/ThreeColumnLayout.js +22 -0
- package/dist/components/overview/OverviewScreen.d.ts +2 -0
- package/dist/components/overview/OverviewScreen.js +138 -0
- package/dist/components/tasks/TaskHeader.d.ts +7 -0
- package/dist/components/tasks/TaskHeader.js +8 -0
- package/dist/components/tasks/TaskItem.d.ts +10 -0
- package/dist/components/tasks/TaskItem.js +25 -0
- package/dist/components/tasks/TaskList.d.ts +11 -0
- package/dist/components/tasks/TaskList.js +11 -0
- package/dist/components/tasks/TasksPane.d.ts +2 -0
- package/dist/components/tasks/TasksPane.js +410 -0
- package/dist/components/timeline/TimelineEntry.d.ts +9 -0
- package/dist/components/timeline/TimelineEntry.js +26 -0
- package/dist/components/timeline/TimelinePane.d.ts +2 -0
- package/dist/components/timeline/TimelinePane.js +78 -0
- package/dist/contexts/AppContext.d.ts +47 -0
- package/dist/contexts/AppContext.js +104 -0
- package/dist/contexts/StorageContext.d.ts +15 -0
- package/dist/contexts/StorageContext.js +83 -0
- package/dist/contexts/ThemeContext.d.ts +15 -0
- package/dist/contexts/ThemeContext.js +44 -0
- package/dist/hooks/useKeyboardNav.d.ts +1 -0
- package/dist/hooks/useKeyboardNav.js +89 -0
- package/dist/hooks/useTerminalSize.d.ts +9 -0
- package/dist/hooks/useTerminalSize.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/dist/services/calendarService.d.ts +18 -0
- package/dist/services/calendarService.js +57 -0
- package/dist/services/storage.d.ts +14 -0
- package/dist/services/storage.js +130 -0
- package/dist/services/taskService.d.ts +17 -0
- package/dist/services/taskService.js +106 -0
- package/dist/services/timelineService.d.ts +25 -0
- package/dist/services/timelineService.js +78 -0
- package/dist/themes/amazon.d.ts +2 -0
- package/dist/themes/amazon.js +37 -0
- package/dist/themes/amazonLight.d.ts +2 -0
- package/dist/themes/amazonLight.js +38 -0
- package/dist/themes/apple.d.ts +2 -0
- package/dist/themes/apple.js +38 -0
- package/dist/themes/appleLight.d.ts +2 -0
- package/dist/themes/appleLight.js +38 -0
- package/dist/themes/atomOneDark.d.ts +2 -0
- package/dist/themes/atomOneDark.js +37 -0
- package/dist/themes/atomOneLight.d.ts +2 -0
- package/dist/themes/atomOneLight.js +38 -0
- package/dist/themes/batman.d.ts +2 -0
- package/dist/themes/batman.js +37 -0
- package/dist/themes/catppuccin.d.ts +2 -0
- package/dist/themes/catppuccin.js +37 -0
- package/dist/themes/catppuccinLatte.d.ts +2 -0
- package/dist/themes/catppuccinLatte.js +38 -0
- package/dist/themes/claude.d.ts +2 -0
- package/dist/themes/claude.js +48 -0
- package/dist/themes/claudeCode.d.ts +2 -0
- package/dist/themes/claudeCode.js +47 -0
- package/dist/themes/cursor.d.ts +2 -0
- package/dist/themes/cursor.js +38 -0
- package/dist/themes/cursorLight.d.ts +2 -0
- package/dist/themes/cursorLight.js +38 -0
- package/dist/themes/dark.d.ts +2 -0
- package/dist/themes/dark.js +38 -0
- package/dist/themes/githubDark.d.ts +2 -0
- package/dist/themes/githubDark.js +37 -0
- package/dist/themes/githubLight.d.ts +2 -0
- package/dist/themes/githubLight.js +38 -0
- package/dist/themes/index.d.ts +9 -0
- package/dist/themes/index.js +83 -0
- package/dist/themes/instagram.d.ts +2 -0
- package/dist/themes/instagram.js +37 -0
- package/dist/themes/instagramLight.d.ts +2 -0
- package/dist/themes/instagramLight.js +38 -0
- package/dist/themes/intellij.d.ts +2 -0
- package/dist/themes/intellij.js +37 -0
- package/dist/themes/intellijLight.d.ts +2 -0
- package/dist/themes/intellijLight.js +38 -0
- package/dist/themes/light.d.ts +2 -0
- package/dist/themes/light.js +38 -0
- package/dist/themes/nord.d.ts +2 -0
- package/dist/themes/nord.js +37 -0
- package/dist/themes/nordLight.d.ts +2 -0
- package/dist/themes/nordLight.js +38 -0
- package/dist/themes/postman.d.ts +2 -0
- package/dist/themes/postman.js +37 -0
- package/dist/themes/postmanLight.d.ts +2 -0
- package/dist/themes/postmanLight.js +38 -0
- package/dist/themes/spiderman.d.ts +2 -0
- package/dist/themes/spiderman.js +37 -0
- package/dist/themes/terminal.d.ts +2 -0
- package/dist/themes/terminal.js +37 -0
- package/dist/themes/ubuntu.d.ts +2 -0
- package/dist/themes/ubuntu.js +37 -0
- package/dist/themes/ubuntuLight.d.ts +2 -0
- package/dist/themes/ubuntuLight.js +38 -0
- package/dist/themes/x.d.ts +2 -0
- package/dist/themes/x.js +38 -0
- package/dist/themes/xLight.d.ts +2 -0
- package/dist/themes/xLight.js +38 -0
- package/dist/types/calendar.d.ts +19 -0
- package/dist/types/calendar.js +1 -0
- package/dist/types/storage.d.ts +21 -0
- package/dist/types/storage.js +1 -0
- package/dist/types/task.d.ts +21 -0
- package/dist/types/task.js +1 -0
- package/dist/types/theme.d.ts +38 -0
- package/dist/types/theme.js +1 -0
- package/dist/types/timeline.d.ts +23 -0
- package/dist/types/timeline.js +9 -0
- package/dist/utils/date.d.ts +7 -0
- package/dist/utils/date.js +37 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.js +39 -0
- package/dist/utils/tree.d.ts +11 -0
- package/dist/utils/tree.js +64 -0
- package/dist/utils/validation.d.ts +7 -0
- package/dist/utils/validation.js +35 -0
- 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,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
|
+
};
|
package/dist/index.d.ts
ADDED
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;
|