conzo 0.0.8 → 0.0.10

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 (45) hide show
  1. package/Component.d.ts +7 -0
  2. package/Component.js +7 -0
  3. package/ConfirmStatus.d.ts +4 -0
  4. package/ConfirmStatus.js +19 -0
  5. package/Conzo.d.ts +4 -0
  6. package/Conzo.js +34 -0
  7. package/Error.d.ts +6 -0
  8. package/Error.js +20 -0
  9. package/Footer.d.ts +2 -0
  10. package/Footer.js +4 -0
  11. package/GoogleChrome.d.ts +10 -0
  12. package/GoogleChrome.js +39 -0
  13. package/Notes.d.ts +2 -0
  14. package/Notes.js +4 -0
  15. package/Search.d.ts +8 -0
  16. package/Search.js +123 -0
  17. package/SelectBox.d.ts +8 -0
  18. package/SelectBox.js +48 -0
  19. package/ViewContext.d.ts +13 -0
  20. package/ViewContext.js +17 -0
  21. package/app.d.ts +2 -0
  22. package/app.js +53 -0
  23. package/apps/ApplicationsCacheContext.d.ts +8 -0
  24. package/apps/ApplicationsCacheContext.js +14 -0
  25. package/apps/RefreshApps.d.ts +1 -0
  26. package/apps/RefreshApps.js +13 -0
  27. package/config/ConfigContext.d.ts +11 -0
  28. package/config/ConfigContext.js +12 -0
  29. package/config/parseAndValidate.d.ts +2 -0
  30. package/config/parseAndValidate.js +51 -0
  31. package/config/types.d.ts +26 -0
  32. package/config/types.js +1 -0
  33. package/debugging/useExitAppRightAway.d.ts +1 -0
  34. package/debugging/useExitAppRightAway.js +9 -0
  35. package/helpers/EscapeBackToSearch.d.ts +7 -0
  36. package/helpers/EscapeBackToSearch.js +20 -0
  37. package/helpers/spawnProcess.d.ts +1 -0
  38. package/helpers/spawnProcess.js +20 -0
  39. package/helpers/tryCatch.d.ts +9 -0
  40. package/helpers/tryCatch.js +11 -0
  41. package/items/items.d.ts +6 -0
  42. package/items/items.js +107 -0
  43. package/items/types.d.ts +23 -0
  44. package/items/types.js +1 -0
  45. package/package.json +6 -31
package/Component.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { ReactItem } from './items/types.js';
3
+ type ComponentProps = {
4
+ item: ReactItem;
5
+ };
6
+ export declare const Component: ({ item }: ComponentProps) => React.JSX.Element;
7
+ export {};
package/Component.js ADDED
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { EscapeBackToSearch } from './helpers/EscapeBackToSearch.js';
3
+ export const Component = ({ item }) => {
4
+ const { action: Component, name } = item;
5
+ return (React.createElement(EscapeBackToSearch, { title: name },
6
+ React.createElement(Component, null)));
7
+ };
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export declare const ConfirmStatus: ({ command }: {
3
+ command: string;
4
+ }) => React.JSX.Element;
@@ -0,0 +1,19 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import childProcess from 'child_process';
4
+ import { tryCatch } from './helpers/tryCatch.js';
5
+ export const ConfirmStatus = ({ command }) => {
6
+ const [status, setStatus] = useState('Loading...');
7
+ useEffect(() => {
8
+ const { error } = tryCatch(() => childProcess.execSync(command).toString());
9
+ if (error) {
10
+ setStatus({ type: 'Failed', error: error.message });
11
+ }
12
+ else {
13
+ setStatus('Done!');
14
+ }
15
+ }, [command]);
16
+ return (React.createElement(Box, null,
17
+ React.createElement(Text, null, typeof status === 'string' ? status : status.type),
18
+ typeof status === 'object' && React.createElement(Text, { color: "red" }, status.error)));
19
+ };
package/Conzo.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { UserConfig } from './config/types.js';
2
+ export declare const createApp: (userConfig?: UserConfig) => {
3
+ start: () => import("ink").Instance;
4
+ };
package/Conzo.js ADDED
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import meow from 'meow';
3
+ import App from './app.js';
4
+ import { ViewProvider } from './ViewContext.js';
5
+ import { ApplicationsCacheProvider } from './apps/ApplicationsCacheContext.js';
6
+ import { ConfigProvider } from './config/ConfigContext.js';
7
+ import { parseAndValidate } from './config/parseAndValidate.js';
8
+ import { tryCatch } from './helpers/tryCatch.js';
9
+ import { Error, Fallback } from './Error.js';
10
+ import { ErrorBoundary } from 'react-error-boundary';
11
+ import { render } from 'ink';
12
+ meow('conzo', {
13
+ importMeta: import.meta,
14
+ flags: {
15
+ name: {
16
+ type: 'string',
17
+ },
18
+ },
19
+ });
20
+ export const createApp = (userConfig) => {
21
+ const { value: config, error } = tryCatch(() => parseAndValidate(userConfig));
22
+ const Conzo = config
23
+ ? () => (React.createElement(ErrorBoundary, { FallbackComponent: Fallback },
24
+ React.createElement(ViewProvider, null,
25
+ React.createElement(ConfigProvider, { config: config },
26
+ React.createElement(ApplicationsCacheProvider, null,
27
+ React.createElement(App, null))))))
28
+ : () => React.createElement(Error, { error: error });
29
+ return {
30
+ start: () => render(React.createElement(Conzo, null), {
31
+ exitOnCtrlC: true,
32
+ }),
33
+ };
34
+ };
package/Error.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { FallbackProps } from 'react-error-boundary';
3
+ export declare const Error: ({ error }: {
4
+ error: Error;
5
+ }) => React.JSX.Element;
6
+ export declare const Fallback: ({ error }: FallbackProps) => React.JSX.Element;
package/Error.js ADDED
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { EscapeBackToSearch } from './helpers/EscapeBackToSearch.js';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export const Error = ({ error }) => {
5
+ const [showStack, setShowStack] = React.useState(false);
6
+ // Note: This useInput sometimes conflicts with the parent component's useInput...
7
+ useInput((input) => {
8
+ if (input === 's') {
9
+ setShowStack((prev) => !prev);
10
+ }
11
+ });
12
+ return (React.createElement(EscapeBackToSearch, { title: "Oops..." },
13
+ React.createElement(Text, null, error.message),
14
+ React.createElement(Box, { marginTop: 1 }, showStack ? (React.createElement(Text, null, error.stack)) : (React.createElement(Text, null, "S = show stack trace")))));
15
+ };
16
+ // Standalone Error component
17
+ export const Fallback = ({ error }) => {
18
+ // Call resetErrorBoundary() to reset the error boundary and retry the render.
19
+ return React.createElement(Text, null, error.message);
20
+ };
package/Footer.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const Footer: () => React.JSX.Element;
package/Footer.js ADDED
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ export const Footer = () => (React.createElement(Box, { justifyContent: "flex-end" },
4
+ React.createElement(Text, null, "conzo")));
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { BookmarkInfo } from './items/types.js';
3
+ export declare const openChrome: (profile: string, url?: string) => Promise<void>;
4
+ export declare const openBookmark: (bookmark: BookmarkInfo) => void;
5
+ export declare const OpenChrome: ({ url }: {
6
+ url?: string;
7
+ }) => React.JSX.Element;
8
+ export declare const GoogleChromeWithProfile: ({ url }: {
9
+ url?: string;
10
+ }) => React.JSX.Element;
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { useView } from './ViewContext.js';
4
+ import { useConfig } from './config/ConfigContext.js';
5
+ import { spawnProcess } from './helpers/spawnProcess.js';
6
+ import { SelectBox } from './SelectBox.js';
7
+ // Open Google Chrome with the desired profile.
8
+ const DEFAULT_PROFILE = 'Default';
9
+ export const openChrome = async (profile, url) => {
10
+ await spawnProcess('open', [
11
+ '-na',
12
+ 'Google Chrome',
13
+ '--args',
14
+ `--profile-directory=${profile}`,
15
+ ...(url ? [url] : []),
16
+ ]);
17
+ };
18
+ export const openBookmark = (bookmark) => {
19
+ openChrome(bookmark.profile ?? DEFAULT_PROFILE, bookmark.url);
20
+ };
21
+ export const OpenChrome = ({ url }) => {
22
+ const { goToView } = useView();
23
+ const { chromeProfiles } = useConfig();
24
+ if (!chromeProfiles) {
25
+ return (React.createElement(Box, { flexDirection: "column" },
26
+ React.createElement(Text, { bold: true }, "No Chrome profiles found"),
27
+ React.createElement(Text, null, "Make sure you have set up your Chrome profiles in the config.")));
28
+ }
29
+ return (React.createElement(Box, { flexDirection: "column" },
30
+ React.createElement(Text, null, "Select your account"),
31
+ React.createElement(SelectBox, { items: Object.keys(chromeProfiles), onSelect: (account) => {
32
+ if (chromeProfiles[account])
33
+ openChrome(chromeProfiles[account], url);
34
+ goToView('search');
35
+ }, width: 30 })));
36
+ };
37
+ export const GoogleChromeWithProfile = ({ url }) => {
38
+ return React.createElement(OpenChrome, { url: url });
39
+ };
package/Notes.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const Notes: () => React.JSX.Element;
package/Notes.js ADDED
@@ -0,0 +1,4 @@
1
+ import { Box, Text } from 'ink';
2
+ import React from 'react';
3
+ export const Notes = () => (React.createElement(Box, null,
4
+ React.createElement(Text, null, "Notes...")));
package/Search.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { Item } from './items/types.js';
3
+ type Props = {
4
+ width: number;
5
+ choose: (item: Item) => void;
6
+ };
7
+ export declare const Search: ({ width, choose }: Props) => React.JSX.Element;
8
+ export {};
package/Search.js ADDED
@@ -0,0 +1,123 @@
1
+ import React from 'react';
2
+ import TextInput from 'ink-text-input';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { useApplicationsCache } from './apps/ApplicationsCacheContext.js';
5
+ import { useConfig } from './config/ConfigContext.js';
6
+ import { useView } from './ViewContext.js';
7
+ import { getItems } from './items/items.js';
8
+ const MAX_VISIBLE_RESULTS = 10;
9
+ const initialState = {
10
+ allItems: [],
11
+ filteredResults: [],
12
+ selectedItem: null,
13
+ visibleStartIndex: 0,
14
+ };
15
+ const resultsReducer = (state, action) => {
16
+ switch (action.type) {
17
+ case 'INIT_ITEMS': {
18
+ const allItems = action.payload;
19
+ return {
20
+ ...state,
21
+ allItems,
22
+ filteredResults: allItems,
23
+ selectedItem: allItems[0] ?? null,
24
+ };
25
+ }
26
+ case 'FILTER_RESULTS': {
27
+ const query = action.query.toLowerCase();
28
+ const visibleStartIndex = 0;
29
+ if (query === '') {
30
+ const filteredResults = state.allItems;
31
+ return {
32
+ ...state,
33
+ filteredResults,
34
+ visibleStartIndex,
35
+ selectedItem: filteredResults[visibleStartIndex] ?? null,
36
+ };
37
+ }
38
+ const filteredResults = state.allItems.filter((result) => result.name.toLowerCase().includes(query.toLowerCase()));
39
+ return {
40
+ ...state,
41
+ filteredResults,
42
+ visibleStartIndex,
43
+ selectedItem: filteredResults[visibleStartIndex] ?? null,
44
+ };
45
+ }
46
+ case 'SELECT_RESULT': {
47
+ if (state.filteredResults.length === 0 || state.selectedItem === null) {
48
+ return state;
49
+ }
50
+ const selectedIndex = state.filteredResults
51
+ .map((r) => r.name)
52
+ .indexOf(state.selectedItem.name);
53
+ let newSelectedIndex = action.which === 'previous'
54
+ ? (selectedIndex - 1 + state.filteredResults.length) %
55
+ state.filteredResults.length
56
+ : (selectedIndex + 1) % state.filteredResults.length;
57
+ const newSelectedName = state.filteredResults[newSelectedIndex] ?? null;
58
+ const start = Math.max(0, newSelectedIndex - MAX_VISIBLE_RESULTS + 1);
59
+ return {
60
+ ...state,
61
+ selectedItem: newSelectedName,
62
+ visibleStartIndex: start,
63
+ };
64
+ }
65
+ default:
66
+ return state;
67
+ }
68
+ };
69
+ export const Search = ({ width, choose }) => {
70
+ const [query, setQuery] = React.useState('');
71
+ const [{ filteredResults, visibleStartIndex, selectedItem }, dispatch] = React.useReducer(resultsReducer, initialState);
72
+ const config = useConfig();
73
+ const { needsRefresh, setNeedsRefresh } = useApplicationsCache();
74
+ const { goToView } = useView();
75
+ React.useEffect(() => {
76
+ const fetchItems = async () => {
77
+ try {
78
+ const items = await getItems({
79
+ refresh: needsRefresh,
80
+ config,
81
+ });
82
+ dispatch({ type: 'INIT_ITEMS', payload: items });
83
+ }
84
+ catch (error) {
85
+ goToView({ error: error });
86
+ }
87
+ };
88
+ fetchItems();
89
+ if (needsRefresh)
90
+ setNeedsRefresh(false);
91
+ }, [needsRefresh, config, setNeedsRefresh, goToView]);
92
+ useInput((_, key) => {
93
+ if (key.upArrow)
94
+ dispatch({ type: 'SELECT_RESULT', which: 'previous' });
95
+ if (key.downArrow)
96
+ dispatch({ type: 'SELECT_RESULT', which: 'next' });
97
+ if (key.escape)
98
+ filter('');
99
+ });
100
+ const filter = (query) => {
101
+ setQuery(query);
102
+ dispatch({ type: 'FILTER_RESULTS', query });
103
+ };
104
+ return (React.createElement(React.Fragment, null,
105
+ React.createElement(Box, { borderStyle: "round", borderColor: config.theme.color },
106
+ React.createElement(TextInput, { value: query, onChange: filter, showCursor: true, placeholder: "Search...", onSubmit: () => {
107
+ if (selectedItem)
108
+ choose(selectedItem);
109
+ } })),
110
+ React.createElement(Text, null, filteredResults.length === 0 && 'No items...'),
111
+ React.createElement(Box, { borderStyle: filteredResults.length > 0 ? 'round' : undefined, borderColor: filteredResults.length > 0 ? 'grey' : undefined, flexDirection: "column" }, filteredResults
112
+ .slice(visibleStartIndex, visibleStartIndex + MAX_VISIBLE_RESULTS)
113
+ .map((item) => {
114
+ const selected = selectedItem?.name === item.name;
115
+ // 5 is an arbitrary number of some spaces around the item name, and some built-in padding.
116
+ let name = ` ${item.name}${' '.repeat(width > 0 ? width - 5 - item.name.length : 0)}`;
117
+ if (item.actionType === 'Show UI')
118
+ name = name.slice(0, -2) + '> ';
119
+ return (React.createElement(Box, { key: item.name },
120
+ React.createElement(Text, { backgroundColor: selected ? config.theme.color : undefined, bold: selected },
121
+ React.createElement(Text, null, name))));
122
+ }))));
123
+ };
package/SelectBox.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ export declare const SelectBox: ({ items, maxVisibleResults, onChange, onSelect, width, }: {
3
+ items: string[];
4
+ maxVisibleResults?: number;
5
+ onChange?: (item: string) => void;
6
+ onSelect: (item: string) => void;
7
+ width?: number;
8
+ }) => React.JSX.Element | null;
package/SelectBox.js ADDED
@@ -0,0 +1,48 @@
1
+ import { Box, Text, useInput } from 'ink';
2
+ import React from 'react';
3
+ import { useConfig } from './config/ConfigContext.js';
4
+ const noop = () => { };
5
+ export const SelectBox = ({ items, maxVisibleResults = 10, onChange = noop, onSelect, width, }) => {
6
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
7
+ const [startIndex, setStartIndex] = React.useState(0);
8
+ const config = useConfig();
9
+ const select = (which) => {
10
+ let newSelectedIndex = which === 'previous'
11
+ ? (selectedIndex - 1 + items.length) % items.length
12
+ : (selectedIndex + 1) % items.length;
13
+ setSelectedIndex(newSelectedIndex);
14
+ setStartIndex(Math.max(0, newSelectedIndex - maxVisibleResults + 1));
15
+ return newSelectedIndex;
16
+ };
17
+ useInput((_, key) => {
18
+ if (items.length === 0 || !items[selectedIndex])
19
+ return;
20
+ if (key.return) {
21
+ onSelect(items[selectedIndex]);
22
+ }
23
+ if (key.upArrow) {
24
+ const newSelectedIndex = select('previous');
25
+ if (items[newSelectedIndex])
26
+ onChange(items[newSelectedIndex]);
27
+ }
28
+ if (key.downArrow) {
29
+ const newSelectedIndex = select('next');
30
+ if (items[newSelectedIndex])
31
+ onChange(items[newSelectedIndex]);
32
+ }
33
+ });
34
+ if (items.length === 0)
35
+ return null;
36
+ // 4 is some padding, and spaces at the beginning and end of the item
37
+ // const width = items.reduce((max, item) => Math.max(max, item.length), 0) + 4
38
+ if (!width) {
39
+ width = items.reduce((max, item) => Math.max(max, item.length), 0) + 4;
40
+ }
41
+ return (React.createElement(Box, { borderStyle: "round", borderColor: "grey", flexDirection: "column", width: width }, items.slice(startIndex, startIndex + maxVisibleResults).map((item) => {
42
+ const selected = item === items[selectedIndex];
43
+ // 3 is an arbitrary number of some spaces around the item name, and some built-in padding.
44
+ let name = ` ${item}${' '.repeat(width > 0 ? width - 3 - item.length : 0)}`;
45
+ return (React.createElement(Box, { key: item },
46
+ React.createElement(Text, { backgroundColor: selected ? config.theme.color : undefined, bold: selected }, name)));
47
+ })));
48
+ };
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { ReactItem } from './items/types.js';
3
+ type ErrorView = {
4
+ error: Error;
5
+ };
6
+ type View = 'search' | 'loading' | ErrorView | ReactItem;
7
+ type ViewContextType = {
8
+ view: View;
9
+ goToView: (view: View) => void;
10
+ };
11
+ declare function ViewProvider({ children }: React.PropsWithChildren): React.JSX.Element;
12
+ declare function useView(): ViewContextType;
13
+ export { ViewProvider, useView };
package/ViewContext.js ADDED
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ const ViewContext = React.createContext(null);
3
+ function ViewProvider({ children }) {
4
+ const [view, setView] = React.useState('search');
5
+ const goToView = React.useCallback((view) => {
6
+ setView(view);
7
+ }, []);
8
+ const value = { view, goToView };
9
+ return React.createElement(ViewContext.Provider, { value: value }, children);
10
+ }
11
+ function useView() {
12
+ const context = React.useContext(ViewContext);
13
+ if (!context)
14
+ throw new Error('useView must be used within a ViewProvider');
15
+ return context;
16
+ }
17
+ export { ViewProvider, useView };
package/app.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function App(): React.JSX.Element;
package/app.js ADDED
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { Box, measureElement, Text } from 'ink';
3
+ import { Search } from './Search.js';
4
+ import { Component } from './Component.js';
5
+ import { useView } from './ViewContext.js';
6
+ import { Error } from './Error.js';
7
+ import { Footer } from './Footer.js';
8
+ export default function App() {
9
+ const { view, goToView } = useView();
10
+ // The searchKey is used to force a re-render of the Search component
11
+ const [searchKey, setSearchKey] = React.useState(0);
12
+ const [containerWidth, setContainerWidth] = React.useState(0);
13
+ const containerRef = React.useRef(null);
14
+ React.useEffect(() => {
15
+ if (!containerRef.current)
16
+ return;
17
+ const { width } = measureElement(containerRef.current);
18
+ setContainerWidth(width);
19
+ }, []);
20
+ React.useEffect(() => {
21
+ if (view === 'loading') {
22
+ setTimeout(() => {
23
+ renderSearch();
24
+ }, 1500);
25
+ }
26
+ }, [view]);
27
+ const renderSearch = () => {
28
+ setSearchKey((prev) => prev + 1);
29
+ goToView('search');
30
+ };
31
+ const choose = async (item) => {
32
+ if (item.actionType === 'Show UI') {
33
+ try {
34
+ goToView(item);
35
+ }
36
+ catch (error) {
37
+ goToView({ error: error });
38
+ }
39
+ }
40
+ else if (item.actionType === 'Fire and forget') {
41
+ try {
42
+ await item.action();
43
+ goToView('loading');
44
+ }
45
+ catch (error) {
46
+ goToView({ error: error });
47
+ }
48
+ }
49
+ };
50
+ return (React.createElement(Box, { flexDirection: "column" },
51
+ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "grey", width: "100%", minHeight: 20, ref: containerRef }, view === 'search' ? (React.createElement(Search, { key: searchKey, width: containerWidth, choose: choose })) : view === 'loading' ? (React.createElement(Text, null, "\\n\\n\\n\\n\\n\\n\\n\\nLoading...")) : 'error' in view ? (React.createElement(Error, { error: view.error })) : 'actionType' in view && view.actionType === 'Show UI' ? (React.createElement(Component, { item: view })) : null),
52
+ React.createElement(Footer, null)));
53
+ }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ type ApplicationsCacheContextType = {
3
+ needsRefresh: boolean;
4
+ setNeedsRefresh: (needsRefresh: boolean) => void;
5
+ };
6
+ declare function ApplicationsCacheProvider({ children }: React.PropsWithChildren): React.JSX.Element;
7
+ declare function useApplicationsCache(): ApplicationsCacheContextType;
8
+ export { ApplicationsCacheProvider, useApplicationsCache };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ const ApplicationsCache = React.createContext(null);
3
+ function ApplicationsCacheProvider({ children }) {
4
+ const [needsRefresh, setNeedsRefresh] = React.useState(false);
5
+ const value = { needsRefresh, setNeedsRefresh };
6
+ return (React.createElement(ApplicationsCache.Provider, { value: value }, children));
7
+ }
8
+ function useApplicationsCache() {
9
+ const context = React.useContext(ApplicationsCache);
10
+ if (!context)
11
+ throw new Error('useApplicationsCache must be used within a ApplicationsCacheProvider');
12
+ return context;
13
+ }
14
+ export { ApplicationsCacheProvider, useApplicationsCache };
@@ -0,0 +1 @@
1
+ export declare const RefreshApps: () => null;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { useApplicationsCache } from './ApplicationsCacheContext.js';
3
+ import { useView } from '../ViewContext.js';
4
+ // Indicates to the app cache that it needs to refresh the apps.
5
+ export const RefreshApps = () => {
6
+ const { setNeedsRefresh } = useApplicationsCache();
7
+ const { goToView } = useView();
8
+ React.useEffect(() => {
9
+ setNeedsRefresh(true);
10
+ goToView('search');
11
+ }, []);
12
+ return null;
13
+ };
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { Config } from './types.js';
3
+ type ConfigContextType = Config;
4
+ declare const Config: React.Context<Config | null>;
5
+ type Props = {
6
+ children: React.ReactNode;
7
+ config: ConfigContextType;
8
+ };
9
+ declare function ConfigProvider({ children, config }: Props): React.JSX.Element;
10
+ declare function useConfig(): Config;
11
+ export { ConfigProvider, useConfig };
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ const Config = React.createContext(null);
3
+ function ConfigProvider({ children, config }) {
4
+ return React.createElement(Config.Provider, { value: config }, children);
5
+ }
6
+ function useConfig() {
7
+ const context = React.useContext(Config);
8
+ if (!context)
9
+ throw new Error('useConfig must be used within a ConfigProvider');
10
+ return context;
11
+ }
12
+ export { ConfigProvider, useConfig };
@@ -0,0 +1,2 @@
1
+ import { Config, UserConfig } from './types.js';
2
+ export declare const parseAndValidate: (userConfig?: UserConfig) => Config;
@@ -0,0 +1,51 @@
1
+ import { openBookmark } from '../GoogleChrome.js';
2
+ const defaultConfig = {
3
+ excludeApps: [],
4
+ favoriteItems: [],
5
+ chromeProfiles: false,
6
+ items: [],
7
+ theme: {
8
+ color: 'green',
9
+ },
10
+ };
11
+ export const parseAndValidate = (userConfig) => {
12
+ if (!userConfig)
13
+ return defaultConfig;
14
+ const chromeProfiles = userConfig.chromeProfiles || false;
15
+ const items = userConfig.items
16
+ ?.map((userItem) => {
17
+ const actionString = userItem.action.toString();
18
+ // A function that returns JSX is considered a UI action
19
+ if (actionString.startsWith('() => React.createElement('))
20
+ return {
21
+ name: userItem.name,
22
+ action: userItem.action,
23
+ actionType: 'Show UI',
24
+ };
25
+ // A function that returns a promise is considered a fire-and-forget action
26
+ if (actionString.startsWith('() => Promise.resolve('))
27
+ return {
28
+ name: userItem.name,
29
+ action: userItem.action,
30
+ actionType: 'Fire and forget',
31
+ };
32
+ // An object is considered a bookmark object, which is converted to a fire-and-forget actions
33
+ if (actionString === '[object Object]')
34
+ return {
35
+ name: userItem.name,
36
+ action: () => openBookmark(userItem.action),
37
+ actionType: 'Fire and forget',
38
+ };
39
+ return null;
40
+ })
41
+ .filter((item) => item !== null);
42
+ return {
43
+ excludeApps: userConfig.excludeApps || [],
44
+ favoriteItems: userConfig.favoriteItems || [],
45
+ chromeProfiles,
46
+ items,
47
+ theme: {
48
+ color: userConfig.theme?.color || defaultConfig.theme.color,
49
+ },
50
+ };
51
+ };
@@ -0,0 +1,26 @@
1
+ import { BookmarkAction, FunctionAction, Item, ReactAction } from '../items/types.js';
2
+ export type UserItem = {
3
+ name: string;
4
+ action: FunctionAction | ReactAction | BookmarkAction;
5
+ };
6
+ type FriendlyName = string;
7
+ type ChromeProfile = string;
8
+ export type ChromeProfiles = Record<FriendlyName, ChromeProfile> | false;
9
+ export type Theme = {
10
+ color: 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'cyan' | 'magenta' | 'white' | 'gray' | 'grey' | 'blackBright' | 'redBright' | 'greenBright' | 'yellowBright' | 'blueBright' | 'cyanBright' | 'magentaBright' | 'whiteBright';
11
+ };
12
+ export type UserConfig = {
13
+ theme?: Theme;
14
+ chromeProfiles?: ChromeProfiles;
15
+ excludeApps?: string[];
16
+ favoriteItems?: string[];
17
+ items?: UserItem[];
18
+ };
19
+ export type Config = {
20
+ theme: Theme;
21
+ chromeProfiles: ChromeProfiles;
22
+ excludeApps: string[];
23
+ favoriteItems: string[];
24
+ items: Item[];
25
+ };
26
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const useExitAppRightAway: () => void;
@@ -0,0 +1,9 @@
1
+ import { useApp } from 'ink';
2
+ import { useEffect } from 'react';
3
+ // Helper for exiting the app right away, for debugging purposes, for example when debugging styling issues.
4
+ export const useExitAppRightAway = () => {
5
+ const { exit } = useApp();
6
+ useEffect(() => {
7
+ exit();
8
+ }, []);
9
+ };
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ type Props = {
3
+ title: string;
4
+ children: React.ReactNode;
5
+ };
6
+ export declare const EscapeBackToSearch: ({ title, children }: Props) => React.JSX.Element;
7
+ export {};
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { useInput } from 'ink';
3
+ import { useView } from '../ViewContext.js';
4
+ import { Box, Text } from 'ink';
5
+ import { useConfig } from '../config/ConfigContext.js';
6
+ export const EscapeBackToSearch = ({ title, children }) => {
7
+ const { goToView } = useView();
8
+ const config = useConfig();
9
+ useInput((_, key) => {
10
+ if (key.escape)
11
+ goToView('search');
12
+ });
13
+ return (React.createElement(React.Fragment, null,
14
+ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: config.theme.color },
15
+ React.createElement(Box, { marginBottom: 1 },
16
+ React.createElement(Text, { color: config.theme.color }, title)),
17
+ children),
18
+ React.createElement(Box, { borderStyle: "round", borderColor: "grey" },
19
+ React.createElement(Text, null, "ESC = back"))));
20
+ };
@@ -0,0 +1 @@
1
+ export declare const spawnProcess: (command: string, args: string[]) => Promise<Buffer>;
@@ -0,0 +1,20 @@
1
+ import childProcess from 'child_process';
2
+ export const spawnProcess = (command, args) => {
3
+ return new Promise((resolve, reject) => {
4
+ const process = childProcess.spawn(command, args);
5
+ const output = [];
6
+ process.stdout?.on('data', (data) => output.push(data));
7
+ process.stderr?.on('data', (data) => output.push(data));
8
+ process.on('error', (error) => {
9
+ reject(new Error(error.message));
10
+ });
11
+ process.on('close', (code) => {
12
+ if (code === 0) {
13
+ resolve(Buffer.concat(output));
14
+ }
15
+ else {
16
+ reject(new Error(`The following command failed with exit code ${code}:\n\n${command} ${args.join(' ')}`));
17
+ }
18
+ });
19
+ });
20
+ };
@@ -0,0 +1,9 @@
1
+ type Result<T> = {
2
+ value: T;
3
+ error: null;
4
+ } | {
5
+ value: null;
6
+ error: Error;
7
+ };
8
+ export declare function tryCatch<T>(fn: () => T): Result<T>;
9
+ export {};
@@ -0,0 +1,11 @@
1
+ export function tryCatch(fn) {
2
+ try {
3
+ return { value: fn(), error: null };
4
+ }
5
+ catch (error) {
6
+ return {
7
+ value: null,
8
+ error: error instanceof Error ? error : new Error(String(error)),
9
+ };
10
+ }
11
+ }
@@ -0,0 +1,6 @@
1
+ import { Item } from './types.js';
2
+ import { Config } from '../config/types.js';
3
+ export declare const getItems: ({ refresh, config, }: {
4
+ refresh: boolean;
5
+ config: Config;
6
+ }) => Promise<Item[]>;
package/items/items.js ADDED
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import { RefreshApps } from '../apps/RefreshApps.js';
3
+ import { spawnProcess } from '../helpers/spawnProcess.js';
4
+ // Fetch installed apps using the `ls` command
5
+ const getApplications = async (excludeApps) => {
6
+ const data = await spawnProcess('ls', ['/Applications']);
7
+ const apps = data
8
+ .toString()
9
+ .split('\n')
10
+ .map((app) => app.replace('.app', '').trim())
11
+ .filter((app) => app !== '' && !excludeApps.includes(app));
12
+ const items = apps.map((app) => ({
13
+ name: app,
14
+ action: async () => {
15
+ await spawnProcess('open', ['-a', app]);
16
+ },
17
+ actionType: 'Fire and forget',
18
+ }));
19
+ return items;
20
+ };
21
+ // Fetch installed preference panes using the `find` command
22
+ const getPrefPanes = async () => {
23
+ // Somehow this command does not return anything...
24
+ // const data = await spawnProcess('find', [
25
+ // '/System/Library/PreferencePanes',
26
+ // '-name',
27
+ // '"*.prefPane"',
28
+ // ])
29
+ // ScriptKit lists the following applications:
30
+ // const APP_DIR = '/Applications'
31
+ // const UTILITIES_DIR = `${APP_DIR}/Utilities`
32
+ // const SYSTEM_UTILITIES_DIR = '/System/Applications/Utilities'
33
+ // const CHROME_APPS_DIR = home('Applications', 'Chrome Apps.localized')
34
+ // as long as the command does not work, here is a hardcoded list with the most common prefPanes
35
+ const data = `
36
+ /System/Library/PreferencePanes/Bluetooth.prefPane
37
+ /System/Library/PreferencePanes/Network.prefPane
38
+ /System/Library/PreferencePanes/Battery.prefPane
39
+ /System/Library/PreferencePanes/Security.prefPane
40
+ /System/Library/PreferencePanes/Dock.prefPane
41
+ /System/Library/PreferencePanes/Sound.prefPane
42
+ /System/Library/PreferencePanes/Appearance.prefPane
43
+ /System/Library/PreferencePanes/Displays.prefPane
44
+ /System/Library/PreferencePanes/Notifications.prefPane
45
+ /System/Library/PreferencePanes/Accounts.prefPane
46
+ /System/Library/PreferencePanes/Trackpad.prefPane
47
+ /System/Library/PreferencePanes/DateAndTime.prefPane
48
+ /System/Library/PreferencePanes/EnergySaverPref.prefPane
49
+ /System/Library/PreferencePanes/Keyboard.prefPane
50
+ /System/Library/PreferencePanes/Spotlight.prefPane
51
+ /System/Library/PreferencePanes/AppleIDPrefPane.prefPane
52
+ /System/Library/PreferencePanes/SharingPref.prefPane
53
+ /System/Library/PreferencePanes/Profiles.prefPane
54
+ /System/Library/PreferencePanes/SoftwareUpdate.prefPane
55
+ /System/Library/PreferencePanes/Mouse.prefPane
56
+ `;
57
+ const prefPanes = data
58
+ .toString()
59
+ .split('\n')
60
+ .map((prefPane) => prefPane.trim())
61
+ .filter((prefPane) => prefPane !== '');
62
+ const items = prefPanes.map((prefPane) => ({
63
+ name: prefPane.split('/').pop()?.replace('.prefPane', '').trim() || '',
64
+ action: async () => {
65
+ await spawnProcess('open', [prefPane]);
66
+ },
67
+ actionType: 'Fire and forget',
68
+ }));
69
+ return items;
70
+ };
71
+ const getCachableItems = async (excludeApps) => {
72
+ const [apps, prefPanes] = await Promise.all([
73
+ getApplications(excludeApps),
74
+ getPrefPanes(),
75
+ ]);
76
+ return [...apps, ...prefPanes];
77
+ };
78
+ const getDefaultUIs = () => [
79
+ {
80
+ name: 'Refresh apps',
81
+ action: () => React.createElement(RefreshApps, null),
82
+ actionType: 'Show UI',
83
+ },
84
+ ];
85
+ const getDefaultBookmarks = () => [];
86
+ // Main function to get items, with caching support for apps only
87
+ export const getItems = (() => {
88
+ let cachedItems = null;
89
+ return async ({ refresh = false, config, }) => {
90
+ if (!cachedItems || refresh)
91
+ cachedItems = await getCachableItems(config.excludeApps);
92
+ const defaultItems = [
93
+ ...getDefaultUIs(),
94
+ ...getDefaultBookmarks(),
95
+ ];
96
+ const items = [...cachedItems, ...defaultItems, ...config.items];
97
+ return items.sort((a, b) => {
98
+ const isAFavorite = config.favoriteItems.includes(a.name);
99
+ const isBFavorite = config.favoriteItems.includes(b.name);
100
+ if (isAFavorite && !isBFavorite)
101
+ return -1;
102
+ if (!isAFavorite && isBFavorite)
103
+ return 1;
104
+ return a.name.localeCompare(b.name);
105
+ });
106
+ };
107
+ })();
@@ -0,0 +1,23 @@
1
+ type BaseItem<T extends string> = {
2
+ name: string;
3
+ actionType: T;
4
+ };
5
+ export type FunctionAction = () => Promise<void>;
6
+ export type FunctionItem = BaseItem<'Fire and forget'> & {
7
+ action: FunctionAction;
8
+ };
9
+ export type ReactAction = () => JSX.Element;
10
+ export type ReactItem = BaseItem<'Show UI'> & {
11
+ action: ReactAction;
12
+ };
13
+ export type BookmarkInfo = {
14
+ browser?: string;
15
+ profile?: string;
16
+ url: string;
17
+ };
18
+ export type BookmarkAction = BookmarkInfo;
19
+ export type BookmarkItem = BaseItem<'Open URL'> & {
20
+ action: BookmarkAction;
21
+ };
22
+ export type Item = FunctionItem | ReactItem | BookmarkItem;
23
+ export {};
package/items/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,35 +1,10 @@
1
1
  {
2
2
  "name": "conzo",
3
- "version": "0.0.8",
4
- "license": "MIT",
5
- "main": "dist/Conzo.js",
3
+ "version": "0.0.10",
6
4
  "type": "module",
7
- "engines": {
8
- "node": ">=16"
9
- },
10
- "scripts": {
11
- "build": "rm -rf dist && tsc && tsc-alias && cp package.json README.md ./dist"
12
- },
13
- "files": [
14
- "dist"
15
- ],
16
- "dependencies": {
17
- "ink": "^5.0.1",
18
- "ink-text-input": "^6.0.0",
19
- "meow": "^13.2.0",
20
- "react": "^18.3.1",
21
- "react-error-boundary": "^4.1.2"
22
- },
23
- "devDependencies": {
24
- "@sindresorhus/tsconfig": "^6.0.0",
25
- "@types/react": "^18.3.12",
26
- "chalk": "^5.3.0",
27
- "eslint-plugin-react": "^7.37.2",
28
- "eslint-plugin-react-hooks": "^5.0.0",
29
- "prettier": "^3.3.3",
30
- "react-devtools-core": "^4.28.5",
31
- "ts-node": "^10.9.2",
32
- "tsc-alias": "^1.8.10",
33
- "typescript": "^5.6.3"
34
- }
5
+ "main": "./Conzo.js",
6
+ "files": ["**/*"],
7
+ "dependencies": {"ink":"^5.0.1","ink-text-input":"^6.0.0","meow":"^13.2.0","react":"^18.3.1","react-error-boundary":"^4.1.2"},
8
+ "author": "Bouwe",
9
+ "license": "MIT"
35
10
  }