momer 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,57 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { renderImage, isImagePath } from '../imageRenderer.js';
4
+
5
+ export default function ImageMessage({ path, maxWidth = 25 }) {
6
+ const [imageLines, setImageLines] = useState(null);
7
+ const [loading, setLoading] = useState(true);
8
+ const [error, setError] = useState(false);
9
+
10
+ useEffect(() => {
11
+ let mounted = true;
12
+
13
+ async function loadImage() {
14
+ if (!path || !isImagePath(path)) {
15
+ setLoading(false);
16
+ setError(true);
17
+ return;
18
+ }
19
+
20
+ try {
21
+ const result = await renderImage(path, maxWidth, 10);
22
+ if (mounted) {
23
+ if (result) {
24
+ setImageLines(result.lines);
25
+ } else {
26
+ setError(true);
27
+ }
28
+ setLoading(false);
29
+ }
30
+ } catch {
31
+ if (mounted) {
32
+ setError(true);
33
+ setLoading(false);
34
+ }
35
+ }
36
+ }
37
+
38
+ loadImage();
39
+ return () => { mounted = false; };
40
+ }, [path, maxWidth]);
41
+
42
+ if (loading) {
43
+ return <Text dimColor>loading image...</Text>;
44
+ }
45
+
46
+ if (error || !imageLines) {
47
+ return <Text dimColor>πŸ“· [image]</Text>;
48
+ }
49
+
50
+ return (
51
+ <Box flexDirection="column">
52
+ {imageLines.map((line, i) => (
53
+ <Text key={i}>{line}</Text>
54
+ ))}
55
+ </Box>
56
+ );
57
+ }
@@ -0,0 +1,137 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { sendMessage } from '../sender.js';
5
+ import { findChat } from '../db.js';
6
+ import { getTheme, isCuteMode } from '../config.js';
7
+
8
+ export default function NewMessage({ onBack, onSent }) {
9
+ const [step, setStep] = useState('contact');
10
+ const [contact, setContact] = useState('');
11
+ const [message, setMessage] = useState('');
12
+ const [sending, setSending] = useState(false);
13
+ const [error, setError] = useState(null);
14
+
15
+ const theme = getTheme();
16
+ const cute = isCuteMode();
17
+
18
+ useInput((input, key) => {
19
+ if (key.escape) {
20
+ if (step === 'message') {
21
+ setStep('contact');
22
+ setError(null);
23
+ } else {
24
+ onBack();
25
+ }
26
+ }
27
+ });
28
+
29
+ const handleContactSubmit = (value) => {
30
+ if (!value.trim()) return;
31
+ setContact(value.trim());
32
+ setStep('message');
33
+ setError(null);
34
+ };
35
+
36
+ const handleMessageSubmit = async (value) => {
37
+ if (!value.trim() || sending) return;
38
+
39
+ setSending(true);
40
+ setError(null);
41
+
42
+ const result = sendMessage(contact, value.trim());
43
+
44
+ if (result.success) {
45
+ const existingChat = findChat(contact);
46
+ const contactObj = existingChat || {
47
+ chatId: null,
48
+ identifier: contact,
49
+ displayName: contact,
50
+ contactId: contact
51
+ };
52
+ onSent(contactObj);
53
+ } else {
54
+ setError(result.error);
55
+ setSending(false);
56
+ }
57
+ };
58
+
59
+ return (
60
+ <Box flexDirection="column">
61
+ {/* Decorative top */}
62
+ {cute && theme.decorTop && (
63
+ <Box justifyContent="center">
64
+ <Text color={theme.primary}>{theme.decorTop}</Text>
65
+ </Box>
66
+ )}
67
+
68
+ {/* Header */}
69
+ <Box borderStyle="round" borderColor={theme.headerBorder} paddingX={1}>
70
+ <Text bold color={theme.primary}>
71
+ {cute ? 'πŸ’Œ new message ✨' : 'New Message'}
72
+ </Text>
73
+ </Box>
74
+
75
+ {/* Content */}
76
+ <Box
77
+ flexDirection="column"
78
+ borderStyle="round"
79
+ borderColor={theme.border}
80
+ paddingX={1}
81
+ >
82
+ {step === 'contact' ? (
83
+ <Box flexDirection="column">
84
+ <Text>{cute ? 'πŸ’­ who do you wanna text?' : 'Enter phone number or email:'}</Text>
85
+ <Box>
86
+ <Text color={theme.secondary}>{cute ? 'πŸ’• To: ' : 'To: '}</Text>
87
+ <TextInput
88
+ value={contact}
89
+ onChange={setContact}
90
+ onSubmit={handleContactSubmit}
91
+ placeholder={cute ? '+1234567890 or bestie@email.com' : '+1234567890 or email@example.com'}
92
+ />
93
+ </Box>
94
+ <Text dimColor>
95
+ {cute
96
+ ? '✨ tip: use full phone number with country code!'
97
+ : 'Tip: Use full phone number with country code (e.g., +1 for US)'
98
+ }
99
+ </Text>
100
+ </Box>
101
+ ) : (
102
+ <Box flexDirection="column">
103
+ <Box>
104
+ <Text dimColor>{cute ? 'πŸ’• To: ' : 'To: '}</Text>
105
+ <Text bold color={theme.secondary}>{contact}</Text>
106
+ </Box>
107
+ <Box>
108
+ <Text color={theme.secondary}>
109
+ {sending ? (cute ? 'πŸ’« ' : '⟳ ') : (cute ? `${theme.inputPrefix} ` : 'β–Έ ')}
110
+ </Text>
111
+ <TextInput
112
+ value={message}
113
+ onChange={setMessage}
114
+ onSubmit={handleMessageSubmit}
115
+ placeholder={sending ? theme.sending : (cute ? 'say something sweet...' : 'Type your message...')}
116
+ />
117
+ </Box>
118
+ </Box>
119
+ )}
120
+
121
+ {/* Error display */}
122
+ {error && (
123
+ <Text color="red">{cute ? 'πŸ’” ' : 'βœ— '}{error}</Text>
124
+ )}
125
+ </Box>
126
+
127
+ {/* Status Bar */}
128
+ <Box borderStyle="round" borderColor={theme.border} paddingX={1}>
129
+ {cute && <Text color={theme.primary}>{theme.decorBottom} </Text>}
130
+ <Text dimColor>
131
+ <Text color={theme.secondary}>{theme.back}</Text> {step === 'contact' ? 'cancel' : 'back'}
132
+ <Text color={theme.secondary}> {theme.send}</Text> {step === 'contact' ? 'next' : 'send'}
133
+ </Text>
134
+ </Box>
135
+ </Box>
136
+ );
137
+ }
@@ -0,0 +1,185 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { saveConfig, completeOnboarding } from '../config.js';
4
+ import { checkDatabaseAccess } from '../db.js';
5
+
6
+ export default function Onboarding({ onComplete }) {
7
+ const [step, setStep] = useState('checking'); // 'checking', 'no_access', 0, 1, 2, 3
8
+ const [theme, setTheme] = useState('cute');
9
+ const [viewMode, setViewMode] = useState('compact');
10
+ const [accessError, setAccessError] = useState(null);
11
+
12
+ useEffect(() => {
13
+ const result = checkDatabaseAccess();
14
+ if (result.success) {
15
+ setStep(0);
16
+ } else {
17
+ setAccessError(result);
18
+ setStep('no_access');
19
+ }
20
+ }, []);
21
+
22
+ useInput((input, key) => {
23
+ if (step === 'no_access') {
24
+ // Retry check on enter
25
+ if (key.return) {
26
+ setStep('checking');
27
+ const result = checkDatabaseAccess();
28
+ if (result.success) {
29
+ setStep(0);
30
+ } else {
31
+ setAccessError(result);
32
+ setStep('no_access');
33
+ }
34
+ }
35
+ } else if (step === 0) {
36
+ // Welcome screen
37
+ if (key.return) {
38
+ setStep(1);
39
+ }
40
+ } else if (step === 1) {
41
+ // Theme selection
42
+ if (key.leftArrow || input === '1') {
43
+ setTheme('cute');
44
+ } else if (key.rightArrow || input === '2') {
45
+ setTheme('minimal');
46
+ } else if (key.return) {
47
+ setStep(2);
48
+ }
49
+ } else if (step === 2) {
50
+ // View mode selection
51
+ if (key.leftArrow || input === '1') {
52
+ setViewMode('compact');
53
+ } else if (key.rightArrow || input === '2') {
54
+ setViewMode('spaced');
55
+ } else if (key.return) {
56
+ setStep(3);
57
+ }
58
+ } else if (step === 3) {
59
+ // Final screen
60
+ if (key.return) {
61
+ saveConfig({ theme, viewMode });
62
+ completeOnboarding();
63
+ onComplete();
64
+ }
65
+ }
66
+ });
67
+
68
+ return (
69
+ <Box flexDirection="column" alignItems="center" justifyContent="center">
70
+ <Box marginBottom={1}>
71
+ <Text color="magenta">qο½₯:*:ο½₯οΎŸβ˜…,qο½₯:*:ο½₯οΎŸβ˜†</Text>
72
+ </Box>
73
+
74
+ {step === 'checking' && (
75
+ <Box flexDirection="column" alignItems="center">
76
+ <Text color="magenta">checking permissions...</Text>
77
+ </Box>
78
+ )}
79
+
80
+ {step === 'no_access' && (
81
+ <Box flexDirection="column" alignItems="center">
82
+ <Text bold color="red">πŸ’” can't access messages</Text>
83
+ <Text> </Text>
84
+ {accessError?.error === 'no_permission' ? (
85
+ <>
86
+ <Text>momer needs Full Disk Access to read your messages</Text>
87
+ <Text> </Text>
88
+ <Text bold color="cyan">how to fix:</Text>
89
+ <Text> </Text>
90
+ <Text>1. Open <Text bold>System Settings</Text></Text>
91
+ <Text>2. Go to <Text bold>Privacy & Security</Text></Text>
92
+ <Text>3. Click <Text bold>Full Disk Access</Text></Text>
93
+ <Text>4. Enable your terminal app:</Text>
94
+ <Text dimColor> (Terminal, iTerm2, Warp, etc.)</Text>
95
+ <Text>5. Restart your terminal</Text>
96
+ </>
97
+ ) : (
98
+ <>
99
+ <Text>{accessError?.message}</Text>
100
+ </>
101
+ )}
102
+ <Text> </Text>
103
+ <Text dimColor>press enter to retry</Text>
104
+ </Box>
105
+ )}
106
+
107
+ {step === 0 && (
108
+ <Box flexDirection="column" alignItems="center">
109
+ <Text bold color="magenta">✨ welcome to momer ✨</Text>
110
+ <Text> </Text>
111
+ <Text>your cute iMessage terminal client</Text>
112
+ <Text> </Text>
113
+ <Text dimColor>press enter to set up</Text>
114
+ </Box>
115
+ )}
116
+
117
+ {step === 1 && (
118
+ <Box flexDirection="column" alignItems="center">
119
+ <Text bold color="magenta">pick your vibe</Text>
120
+ <Text> </Text>
121
+ <Box>
122
+ <Box
123
+ borderStyle={theme === 'cute' ? 'double' : 'single'}
124
+ borderColor={theme === 'cute' ? 'magenta' : 'gray'}
125
+ paddingX={2}
126
+ marginRight={2}
127
+ >
128
+ <Text color={theme === 'cute' ? 'magenta' : 'white'}>✨ cute ✨</Text>
129
+ </Box>
130
+ <Box
131
+ borderStyle={theme === 'minimal' ? 'double' : 'single'}
132
+ borderColor={theme === 'minimal' ? 'blue' : 'gray'}
133
+ paddingX={2}
134
+ >
135
+ <Text color={theme === 'minimal' ? 'blue' : 'white'}>minimal</Text>
136
+ </Box>
137
+ </Box>
138
+ <Text> </Text>
139
+ <Text dimColor>← β†’ to choose, enter to continue</Text>
140
+ </Box>
141
+ )}
142
+
143
+ {step === 2 && (
144
+ <Box flexDirection="column" alignItems="center">
145
+ <Text bold color="magenta">message style</Text>
146
+ <Text> </Text>
147
+ <Box>
148
+ <Box
149
+ borderStyle={viewMode === 'compact' ? 'double' : 'single'}
150
+ borderColor={viewMode === 'compact' ? 'magenta' : 'gray'}
151
+ paddingX={2}
152
+ marginRight={2}
153
+ >
154
+ <Text color={viewMode === 'compact' ? 'magenta' : 'white'}>compact</Text>
155
+ </Box>
156
+ <Box
157
+ borderStyle={viewMode === 'spaced' ? 'double' : 'single'}
158
+ borderColor={viewMode === 'spaced' ? 'magenta' : 'gray'}
159
+ paddingX={2}
160
+ >
161
+ <Text color={viewMode === 'spaced' ? 'magenta' : 'white'}>spaced</Text>
162
+ </Box>
163
+ </Box>
164
+ <Text> </Text>
165
+ <Text dimColor>← β†’ to choose, enter to continue</Text>
166
+ </Box>
167
+ )}
168
+
169
+ {step === 3 && (
170
+ <Box flexDirection="column" alignItems="center">
171
+ <Text bold color="magenta">you're all set! πŸ’•</Text>
172
+ <Text> </Text>
173
+ <Text>theme: <Text color="cyan">{theme}</Text></Text>
174
+ <Text>view: <Text color="cyan">{viewMode}</Text></Text>
175
+ <Text> </Text>
176
+ <Text dimColor>press enter to start</Text>
177
+ </Box>
178
+ )}
179
+
180
+ <Box marginTop={1}>
181
+ <Text color="magenta">β˜†οΎŸο½₯:*:ο½₯q momer β˜…οΎŸο½₯:*:ο½₯q</Text>
182
+ </Box>
183
+ </Box>
184
+ );
185
+ }
package/src/config.js ADDED
@@ -0,0 +1,147 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
4
+
5
+ const CONFIG_DIR = join(homedir(), '.config', 'momer');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULT_CONFIG = {
9
+ theme: 'cute', // 'cute' or 'minimal'
10
+ viewMode: 'compact', // 'compact' or 'spaced'
11
+ showEmojis: true,
12
+ sparkles: true,
13
+ refreshInterval: 2000
14
+ };
15
+
16
+ let config = null;
17
+
18
+ export function getConfig() {
19
+ if (config) return config;
20
+
21
+ try {
22
+ if (existsSync(CONFIG_FILE)) {
23
+ const data = readFileSync(CONFIG_FILE, 'utf-8');
24
+ config = { ...DEFAULT_CONFIG, ...JSON.parse(data) };
25
+ } else {
26
+ config = { ...DEFAULT_CONFIG };
27
+ }
28
+ } catch {
29
+ config = { ...DEFAULT_CONFIG };
30
+ }
31
+
32
+ return config;
33
+ }
34
+
35
+ export function isFirstRun() {
36
+ return !existsSync(CONFIG_FILE);
37
+ }
38
+
39
+ export function completeOnboarding() {
40
+ // Just save the config to mark onboarding as complete
41
+ saveConfig(getConfig());
42
+ }
43
+
44
+ export function saveConfig(newConfig) {
45
+ config = { ...getConfig(), ...newConfig };
46
+
47
+ try {
48
+ if (!existsSync(CONFIG_DIR)) {
49
+ mkdirSync(CONFIG_DIR, { recursive: true });
50
+ }
51
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ export function isCuteMode() {
59
+ const cfg = getConfig();
60
+ return cfg.theme === 'cute';
61
+ }
62
+
63
+ export function isCompactMode() {
64
+ const cfg = getConfig();
65
+ return cfg.viewMode === 'compact';
66
+ }
67
+
68
+ // Theme colors and decorations
69
+ export const themes = {
70
+ cute: {
71
+ primary: 'magenta',
72
+ secondary: 'cyan',
73
+ accent: '#ff69b4', // hot pink
74
+ sent: 'magenta',
75
+ received: 'cyan',
76
+ border: 'magenta',
77
+ headerBorder: 'magenta',
78
+ title: '✨ momer ✨',
79
+ subtitle: 'πŸ’•',
80
+ msgSent: 'πŸ’—',
81
+ msgReceived: 'πŸ’¬',
82
+ group: 'πŸ‘―β€β™€οΈ',
83
+ newMsg: 'n',
84
+ back: 'esc',
85
+ send: '⏎',
86
+ typing: '>',
87
+ refresh: 'r',
88
+ quit: 'q',
89
+ divider: 'ο½₯゚✧',
90
+ sparkle: '✨',
91
+ heart: 'β™‘',
92
+ star: 'β˜†',
93
+ flower: '✿',
94
+ bullet: 'β™‘',
95
+ arrow: 'β₯',
96
+ time: '',
97
+ you: 'πŸ’– You',
98
+ inputPrefix: 'πŸ’­',
99
+ welcome: 'hey cutie! πŸ’•',
100
+ noMessages: 'no messages yet... be the first! πŸ’Œ',
101
+ sending: 'sending with love...',
102
+ sent: 'sent! πŸ’•',
103
+ decorTop: 'qο½₯:*:ο½₯οΎŸβ˜…,qο½₯:*:ο½₯οΎŸβ˜†',
104
+ decorBottom: 'β˜† momer β˜…'
105
+ },
106
+ minimal: {
107
+ primary: 'blue',
108
+ secondary: 'gray',
109
+ accent: 'cyan',
110
+ sent: 'blue',
111
+ received: 'white',
112
+ border: 'gray',
113
+ headerBorder: 'blue',
114
+ title: 'momer',
115
+ subtitle: '',
116
+ msgSent: '',
117
+ msgReceived: '',
118
+ group: 'πŸ‘₯',
119
+ newMsg: 'n',
120
+ back: 'esc',
121
+ send: '⏎',
122
+ typing: '>',
123
+ refresh: 'r',
124
+ quit: 'q',
125
+ divider: '─',
126
+ sparkle: '',
127
+ heart: '',
128
+ star: '',
129
+ flower: '',
130
+ bullet: 'β€’',
131
+ arrow: '>',
132
+ time: '',
133
+ you: 'You',
134
+ inputPrefix: '>',
135
+ welcome: '',
136
+ noMessages: 'No messages yet',
137
+ sending: 'Sending...',
138
+ sent: 'Sent',
139
+ decorTop: '',
140
+ decorBottom: ''
141
+ }
142
+ };
143
+
144
+ export function getTheme() {
145
+ const cfg = getConfig();
146
+ return themes[cfg.theme] || themes.cute;
147
+ }