cron-human 1.1.2 → 1.1.3

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/ui/app.js CHANGED
@@ -5,6 +5,7 @@ import { InputSection } from './components/InputSection.js';
5
5
  import { PreviewSection } from './components/PreviewSection.js';
6
6
  import { OptionsPanel } from './components/OptionsPanel.js';
7
7
  import { HistoryPanel } from './components/HistoryPanel.js';
8
+ import { PresetsModal } from './components/PresetsModal.js';
8
9
  import clipboardy from 'clipboardy';
9
10
  var FocusArea;
10
11
  (function (FocusArea) {
@@ -15,7 +16,10 @@ var FocusArea;
15
16
  export const App = () => {
16
17
  const { exit } = useApp();
17
18
  const [expression, setExpression] = useState('');
18
- const [timezone] = useState(undefined);
19
+ const [timezone, setTimezone] = useState(undefined);
20
+ const [timezoneInput, setTimezoneInput] = useState('');
21
+ const [inputMode, setInputMode] = useState('cron');
22
+ const [showPresets, setShowPresets] = useState(false);
19
23
  const [allowSeconds, setAllowSeconds] = useState(false);
20
24
  const [focus, setFocus] = useState(FocusArea.Input);
21
25
  const [history, setHistory] = useState([]);
@@ -32,6 +36,55 @@ export const App = () => {
32
36
  exit();
33
37
  return;
34
38
  }
39
+ // Global Shortcuts (Paste, Reset, Toggles)
40
+ if (key.ctrl && input === 'v') {
41
+ if (focus === FocusArea.Input && !showPresets) {
42
+ clipboardy.read().then(text => {
43
+ if (text) {
44
+ if (inputMode === 'timezone') {
45
+ setTimezoneInput(text.trim());
46
+ }
47
+ else {
48
+ setExpression(text.trim());
49
+ }
50
+ setNotification('Pasted from clipboard!');
51
+ }
52
+ }).catch(err => {
53
+ setNotification(`Paste failed: ${err.message}`);
54
+ });
55
+ return;
56
+ }
57
+ }
58
+ if (key.ctrl && input === 'r') {
59
+ if (inputMode === 'timezone') {
60
+ setTimezoneInput('');
61
+ }
62
+ else {
63
+ setExpression('');
64
+ }
65
+ setFocus(FocusArea.Input);
66
+ return;
67
+ }
68
+ if (key.ctrl && input === 't') {
69
+ setInputMode('timezone');
70
+ setTimezoneInput(timezone || '');
71
+ setFocus(FocusArea.Input);
72
+ return;
73
+ }
74
+ if (key.ctrl && input === 'p') {
75
+ setShowPresets(prev => !prev);
76
+ return;
77
+ }
78
+ // Mode Specific Handling
79
+ if (inputMode === 'timezone') {
80
+ if (key.escape) {
81
+ setInputMode('cron');
82
+ setFocus(FocusArea.Input);
83
+ return;
84
+ }
85
+ return; // Let TextInput handle typing
86
+ }
87
+ // Cron Mode Handling
35
88
  if (input === 'q' && !key.ctrl && focus !== FocusArea.Input) {
36
89
  exit();
37
90
  return;
@@ -44,23 +97,12 @@ export const App = () => {
44
97
  return FocusArea.History;
45
98
  return FocusArea.Input;
46
99
  });
100
+ return;
47
101
  }
48
- if (key.ctrl && input === 'r') {
49
- setExpression('');
50
- setFocus(FocusArea.Input);
51
- }
52
- if (key.ctrl && input === 'v') {
53
- if (focus === FocusArea.Input) {
54
- clipboardy.read().then(text => {
55
- if (text) {
56
- setExpression(text.trim());
57
- setNotification('Pasted from clipboard!');
58
- }
59
- }).catch(err => {
60
- setNotification(`Paste failed: ${err.message}`);
61
- });
62
- }
102
+ if (key.ctrl && input === 's') { // Save? Or just implicit.
103
+ // Placeholder if needed
63
104
  }
105
+ // Navigation etc handled by components or below
64
106
  if (focus === FocusArea.Options) {
65
107
  if (input === ' ') {
66
108
  setAllowSeconds(prev => !prev);
@@ -92,6 +134,11 @@ export const App = () => {
92
134
  }
93
135
  });
94
136
  const handleInputSubmit = (val) => {
137
+ if (inputMode === 'timezone') {
138
+ setTimezone(val.trim() || undefined);
139
+ setInputMode('cron');
140
+ return;
141
+ }
95
142
  if (val.trim()) {
96
143
  const last = history[0];
97
144
  if (!last || last.expression !== val) {
@@ -99,5 +146,9 @@ export const App = () => {
99
146
  }
100
147
  }
101
148
  };
102
- return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", height: "100%", children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "magenta", children: "Cron Human TUI" }), _jsx(Text, { children: " | " }), _jsx(Text, { dimColor: true, children: "Ctrl+C to Quit" })] }), notification && _jsx(Text, { color: "yellow", bold: true, children: notification })] }), _jsx(InputSection, { value: expression, onChange: setExpression, onSubmit: handleInputSubmit, isFocused: focus === FocusArea.Input }), _jsx(Box, { marginY: 1, children: _jsx(OptionsPanel, { isFocused: focus === FocusArea.Options, timezone: timezone || "Local", allowSeconds: allowSeconds }) }), _jsx(PreviewSection, { expression: expression, timezone: timezone, allowSeconds: allowSeconds }), _jsxs(Box, { marginTop: 1, children: [_jsx(HistoryPanel, { isFocused: focus === FocusArea.History, items: history.slice(0, 5), selectedIndex: historyIndex }), history.length > 5 && _jsxs(Text, { dimColor: true, children: ["... ", history.length - 5, " more"] })] })] }));
149
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", height: "100%", children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "magenta", children: "Cron Human TUI" }), _jsx(Text, { children: " | " }), _jsx(Text, { dimColor: true, children: "Ctrl+C to Quit" })] }), notification && _jsx(Text, { color: "yellow", bold: true, children: notification })] }), _jsx(InputSection, { value: inputMode === 'timezone' ? timezoneInput : expression, onChange: inputMode === 'timezone' ? setTimezoneInput : setExpression, onSubmit: handleInputSubmit, isFocused: focus === FocusArea.Input && !showPresets, label: inputMode === 'timezone' ? "Enter Timezone (e.g. UTC, America/New_York) [Esc to cancel]:" : "Cron Expression:" }), _jsx(Box, { marginY: 1, children: _jsx(OptionsPanel, { isFocused: focus === FocusArea.Options, timezone: timezone || "Local", allowSeconds: allowSeconds }) }), _jsx(PreviewSection, { expression: expression, timezone: timezone, allowSeconds: allowSeconds }), _jsxs(Box, { marginTop: 1, children: [_jsx(HistoryPanel, { isFocused: focus === FocusArea.History, items: history.slice(Math.max(0, historyIndex - 2), Math.max(0, historyIndex - 2) + 5), selectedIndex: historyIndex, startIndex: Math.max(0, historyIndex - 2) }), history.length > 5 && _jsxs(Text, { dimColor: true, children: ["... ", history.length - 5, " more"] })] }), showPresets && (_jsx(Box, { position: "absolute", minHeight: 10, minWidth: 50, marginTop: 2, marginLeft: 5, children: _jsx(PresetsModal, { onSelect: (val) => {
150
+ setExpression(val);
151
+ setShowPresets(false);
152
+ setFocus(FocusArea.Input);
153
+ }, onCancel: () => setShowPresets(false) }) }))] }));
103
154
  };
@@ -7,6 +7,7 @@ interface HistoryPanelProps {
7
7
  isFocused: boolean;
8
8
  items: HistoryItem[];
9
9
  selectedIndex: number;
10
+ startIndex: number;
10
11
  }
11
12
  export declare const HistoryPanel: React.FC<HistoryPanelProps>;
12
13
  export {};
@@ -1,8 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- export const HistoryPanel = ({ isFocused, items, selectedIndex }) => {
3
+ export const HistoryPanel = ({ isFocused, items, selectedIndex, startIndex }) => {
4
4
  if (items.length === 0) {
5
5
  return (_jsxs(Box, { borderStyle: "round", borderColor: isFocused ? "green" : "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "History:" }), _jsx(Text, { dimColor: true, children: "No history yet. Press Enter to save successful runs." })] }));
6
6
  }
7
- return (_jsxs(Box, { borderStyle: "round", borderColor: isFocused ? "green" : "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "History (Up/Down to navigate, Enter to load, 'c' to copy):" }), items.map((item, index) => (_jsx(Box, { children: _jsxs(Text, { color: index === selectedIndex ? "cyan" : "white", children: [index === selectedIndex ? "> " : " ", item.expression] }) }, index)))] }));
7
+ return (_jsxs(Box, { borderStyle: "round", borderColor: isFocused ? "green" : "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "History (Up/Down to navigate, Enter to load, 'c' to copy):" }), items.map((item, index) => {
8
+ const globalIndex = startIndex + index;
9
+ const isSelected = globalIndex === selectedIndex;
10
+ return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? "cyan" : "white", children: [isSelected ? "> " : " ", item.expression] }) }, index));
11
+ })] }));
8
12
  };
@@ -4,6 +4,7 @@ interface InputSectionProps {
4
4
  onChange: (value: string) => void;
5
5
  onSubmit: (value: string) => void;
6
6
  isFocused: boolean;
7
+ label?: string;
7
8
  }
8
9
  export declare const InputSection: React.FC<InputSectionProps>;
9
10
  export {};
@@ -1,6 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
2
+ import { useRef } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
3
4
  import TextInput from 'ink-text-input';
4
- export const InputSection = ({ value, onChange, onSubmit, isFocused }) => {
5
- return (_jsxs(Box, { borderStyle: "round", borderColor: isFocused ? "green" : "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: isFocused ? "green" : "white", children: "Cron Expression:" }), _jsx(Box, { children: _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, focus: isFocused, placeholder: "* * * * *" }) })] }));
5
+ export const InputSection = ({ value, onChange, onSubmit, isFocused, label = "Cron Expression:" }) => {
6
+ // FIX: ink-text-input adds characters even if Ctrl is held (except Ctrl+C).
7
+ // We safeguard against this by tracking the Ctrl key state.
8
+ const ctrlHeld = useRef(false);
9
+ useInput((_input, key) => {
10
+ ctrlHeld.current = key.ctrl;
11
+ }, { isActive: true }); // Always track, or at least when focused
12
+ // To attempt "Context" feature simply:
13
+ const parts = value.trim().split(/\s+/);
14
+ let helpText = "";
15
+ if (parts.length > 5)
16
+ helpText = "Fields: Sec Min Hour Day Month Weekday";
17
+ else
18
+ helpText = "Fields: Min Hour Day Month Weekday";
19
+ return (_jsxs(Box, { borderStyle: "round", borderColor: isFocused ? "green" : "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: isFocused ? "green" : "white", children: label }), _jsx(Box, { children: _jsx(TextInput, { value: value, onChange: (val) => {
20
+ // FIX: Defer the update to allow useInput (below/parent) to update ctrlHeld state.
21
+ // ink events bubble child->parent. TextInput (child) fires onChange.
22
+ // Then InputSection (parent) useInput fires.
23
+ // By deferring, we check ctrlHeld *after* useInput has run for this event.
24
+ setTimeout(() => {
25
+ if (!ctrlHeld.current) {
26
+ onChange(val);
27
+ }
28
+ }, 0);
29
+ }, onSubmit: onSubmit, focus: isFocused, placeholder: "* * * * *" }) }), isFocused && label.includes("Cron") && (_jsx(Box, { marginTop: 0, children: _jsx(Text, { dimColor: true, children: helpText }) }))] }));
6
30
  };
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface PresetsModalProps {
3
+ onSelect: (value: string) => void;
4
+ onCancel: () => void;
5
+ }
6
+ export declare const PresetsModal: React.FC<PresetsModalProps>;
7
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ const presets = [
5
+ { label: 'Every Minute (* * * * *)', value: '* * * * *' },
6
+ { label: 'Every Hour (0 * * * *)', value: '0 * * * *' },
7
+ { label: 'Every Day (0 0 * * *)', value: '0 0 * * *' },
8
+ { label: 'Every Weekday (0 0 * * 1-5)', value: '0 0 * * 1-5' },
9
+ { label: 'Every Weekend (0 0 * * 0,6)', value: '0 0 * * 0,6' },
10
+ { label: 'First of Month (0 0 1 * *)', value: '0 0 1 * *' },
11
+ { label: 'Start of Year (0 0 1 1 *)', value: '0 0 1 1 *' },
12
+ ];
13
+ export const PresetsModal = ({ onSelect, onCancel }) => {
14
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "cyan", padding: 1, width: 50, children: [_jsx(Text, { bold: true, children: "Select a Preset (Esc to cancel):" }), _jsx(Box, { marginY: 1, children: _jsx(SelectInput, { items: presets, onSelect: (item) => onSelect(item.value), isFocused: true }) }), _jsx(Text, { dimColor: true, children: "Use Arrow keys to select, Enter to apply" })] }));
15
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cron-human",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "A CLI that converts cron expressions to human-readable English and prints next run times",
5
5
  "main": "dist/lib.js",
6
6
  "types": "dist/lib.d.ts",