openmemo 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/LICENSE +21 -0
- package/README.ja.md +96 -0
- package/README.ko.md +96 -0
- package/README.md +291 -0
- package/dist/cli.js +190 -0
- package/dist/config.js +15 -0
- package/dist/editor.js +11 -0
- package/dist/storage.js +133 -0
- package/dist/tui.js +116 -0
- package/dist/utils.js +23 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 arkjun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# openmemo
|
|
2
|
+
|
|
3
|
+
mattn/memo に着想を得た OpenTUI ベースのメモアプリです。
|
|
4
|
+
|
|
5
|
+
## 特長
|
|
6
|
+
- ターミナル UI でメモを閲覧・開く。
|
|
7
|
+
- new / list / edit / delete / grep / cat コマンドに対応。
|
|
8
|
+
- title, date, tags を含む軽量なフロントマター付き Markdown 保存。
|
|
9
|
+
- 環境変数でエディタを指定可能。
|
|
10
|
+
|
|
11
|
+
## 要件
|
|
12
|
+
- `readline/promises` をサポートする Node.js(Node 17+)。
|
|
13
|
+
|
|
14
|
+
## インストール
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
CLI を有効化:
|
|
21
|
+
```bash
|
|
22
|
+
npm link
|
|
23
|
+
# または
|
|
24
|
+
npm install -g .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 使い方
|
|
28
|
+
TUI を起動:
|
|
29
|
+
```bash
|
|
30
|
+
openmemo
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
メモを作成:
|
|
34
|
+
```bash
|
|
35
|
+
openmemo new
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
その他のコマンド:
|
|
39
|
+
```bash
|
|
40
|
+
openmemo list
|
|
41
|
+
openmemo edit <query>
|
|
42
|
+
openmemo delete <query>
|
|
43
|
+
openmemo grep <pattern>
|
|
44
|
+
openmemo cat <query>
|
|
45
|
+
openmemo help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
メモ:
|
|
49
|
+
- `<query>` はメモの id またはタイトル(部分一致)で検索します。複数一致した場合は選択を促します。
|
|
50
|
+
- `grep` は有効な場合は大文字小文字を無視した JavaScript 正規表現、無効な場合は大文字小文字を無視した部分一致検索として動作します。
|
|
51
|
+
- TUI 終了: `q` または `Esc`。
|
|
52
|
+
|
|
53
|
+
## 設定
|
|
54
|
+
環境変数:
|
|
55
|
+
- `OPEN_MEMO_DIR`: メモ保存ディレクトリを指定。
|
|
56
|
+
- `OPEN_MEMO_EDITOR`: 使用するエディタのコマンド。
|
|
57
|
+
- `VISUAL` / `EDITOR`: `OPEN_MEMO_EDITOR` が未設定の場合のフォールバック。
|
|
58
|
+
|
|
59
|
+
エディタ優先順位:
|
|
60
|
+
1. `OPEN_MEMO_EDITOR`
|
|
61
|
+
2. `VISUAL`
|
|
62
|
+
3. `EDITOR`
|
|
63
|
+
4. `vi`
|
|
64
|
+
|
|
65
|
+
## データ保存
|
|
66
|
+
デフォルト保存先:
|
|
67
|
+
```
|
|
68
|
+
~/.openmemo/memos
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
ファイル名形式:
|
|
72
|
+
```
|
|
73
|
+
YYYY-MM-DD-<slug>.md
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
テンプレート例:
|
|
77
|
+
```markdown
|
|
78
|
+
---
|
|
79
|
+
title: Your Title
|
|
80
|
+
date: 2026-01-29 12:34
|
|
81
|
+
tags: tag1, tag2
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
# Your Title
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 開発
|
|
88
|
+
ビルド:
|
|
89
|
+
```bash
|
|
90
|
+
npm run build
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
ソースから実行:
|
|
94
|
+
```bash
|
|
95
|
+
npm start
|
|
96
|
+
```
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# openmemo
|
|
2
|
+
|
|
3
|
+
mattn/memo에서 영감을 받은 OpenTUI 기반 메모 앱입니다.
|
|
4
|
+
|
|
5
|
+
## 주요 기능
|
|
6
|
+
- 터미널 UI로 메모 목록을 탐색하고 열기.
|
|
7
|
+
- new, list, edit, delete, grep, cat 명령 지원.
|
|
8
|
+
- title, date, tags를 포함한 가벼운 프런트매터로 Markdown 저장.
|
|
9
|
+
- 환경 변수로 에디터 지정.
|
|
10
|
+
|
|
11
|
+
## 요구사항
|
|
12
|
+
- `readline/promises`를 지원하는 Node.js (Node 17+).
|
|
13
|
+
|
|
14
|
+
## 설치
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
CLI 노출:
|
|
21
|
+
```bash
|
|
22
|
+
npm link
|
|
23
|
+
# 또는
|
|
24
|
+
npm install -g .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 사용 방법
|
|
28
|
+
TUI 실행:
|
|
29
|
+
```bash
|
|
30
|
+
openmemo
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
메모 생성:
|
|
34
|
+
```bash
|
|
35
|
+
openmemo new
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
기타 명령:
|
|
39
|
+
```bash
|
|
40
|
+
openmemo list
|
|
41
|
+
openmemo edit <query>
|
|
42
|
+
openmemo delete <query>
|
|
43
|
+
openmemo grep <pattern>
|
|
44
|
+
openmemo cat <query>
|
|
45
|
+
openmemo help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
참고:
|
|
49
|
+
- `<query>`는 메모 id 또는 제목(부분 일치)을 기준으로 검색합니다. 여러 개가 일치하면 선택을 요청합니다.
|
|
50
|
+
- `grep`는 유효한 경우 대소문자 무시 JavaScript 정규식을 사용하고, 그렇지 않으면 대소문자 무시 부분 문자열 검색으로 동작합니다.
|
|
51
|
+
- TUI 종료: `q` 또는 `Esc`.
|
|
52
|
+
|
|
53
|
+
## 설정
|
|
54
|
+
환경 변수:
|
|
55
|
+
- `OPEN_MEMO_DIR`: 메모 저장 경로를 지정합니다.
|
|
56
|
+
- `OPEN_MEMO_EDITOR`: 선호하는 에디터 명령.
|
|
57
|
+
- `VISUAL` / `EDITOR`: `OPEN_MEMO_EDITOR`가 없을 때의 대안.
|
|
58
|
+
|
|
59
|
+
에디터 우선순위:
|
|
60
|
+
1. `OPEN_MEMO_EDITOR`
|
|
61
|
+
2. `VISUAL`
|
|
62
|
+
3. `EDITOR`
|
|
63
|
+
4. `vi`
|
|
64
|
+
|
|
65
|
+
## 데이터 저장
|
|
66
|
+
기본 경로:
|
|
67
|
+
```
|
|
68
|
+
~/.openmemo/memos
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
파일 이름 형식:
|
|
72
|
+
```
|
|
73
|
+
YYYY-MM-DD-<slug>.md
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
템플릿 예시:
|
|
77
|
+
```markdown
|
|
78
|
+
---
|
|
79
|
+
title: Your Title
|
|
80
|
+
date: 2026-01-29 12:34
|
|
81
|
+
tags: tag1, tag2
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
# Your Title
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 개발
|
|
88
|
+
빌드:
|
|
89
|
+
```bash
|
|
90
|
+
npm run build
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
소스에서 실행:
|
|
94
|
+
```bash
|
|
95
|
+
npm start
|
|
96
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# openmemo
|
|
2
|
+
|
|
3
|
+
[English](#english) | [한국어](#한국어) | [日本語](#日本語)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## English
|
|
8
|
+
|
|
9
|
+
OpenTUI-based memo app inspired by mattn/memo.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
- Terminal UI for browsing and opening memos
|
|
13
|
+
- Create, list, edit, delete, grep, and cat commands
|
|
14
|
+
- Markdown storage with lightweight frontmatter (title, date, tags)
|
|
15
|
+
- Uses your preferred editor via environment variables
|
|
16
|
+
|
|
17
|
+
### Requirements
|
|
18
|
+
- Node.js 17+ or Bun
|
|
19
|
+
- pnpm
|
|
20
|
+
|
|
21
|
+
### Install
|
|
22
|
+
```bash
|
|
23
|
+
pnpm install
|
|
24
|
+
pnpm build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Expose the CLI:
|
|
28
|
+
```bash
|
|
29
|
+
pnpm link --global
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Usage
|
|
33
|
+
Launch the TUI:
|
|
34
|
+
```bash
|
|
35
|
+
openmemo
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Create a memo:
|
|
39
|
+
```bash
|
|
40
|
+
openmemo new
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Other commands:
|
|
44
|
+
```bash
|
|
45
|
+
openmemo list
|
|
46
|
+
openmemo edit <query>
|
|
47
|
+
openmemo delete <query>
|
|
48
|
+
openmemo grep <pattern>
|
|
49
|
+
openmemo cat <query>
|
|
50
|
+
openmemo help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- `<query>` matches the memo id or title (partial match). If multiple match, you will be prompted to select.
|
|
55
|
+
- `grep` uses a case-insensitive JavaScript regex when the pattern is valid; otherwise it falls back to a case-insensitive substring search.
|
|
56
|
+
- TUI: press `q` or `Esc` to quit.
|
|
57
|
+
|
|
58
|
+
### Configuration
|
|
59
|
+
Environment variables:
|
|
60
|
+
- `OPEN_MEMO_DIR`: override memo storage directory
|
|
61
|
+
- `OPEN_MEMO_EDITOR`: preferred editor command
|
|
62
|
+
- `VISUAL` / `EDITOR`: fallbacks if `OPEN_MEMO_EDITOR` is not set
|
|
63
|
+
|
|
64
|
+
Editor resolution order:
|
|
65
|
+
1. `OPEN_MEMO_EDITOR`
|
|
66
|
+
2. `VISUAL`
|
|
67
|
+
3. `EDITOR`
|
|
68
|
+
4. `vi`
|
|
69
|
+
|
|
70
|
+
### Data Storage
|
|
71
|
+
Default directory:
|
|
72
|
+
```
|
|
73
|
+
~/.openmemo/memos
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
File naming format:
|
|
77
|
+
```
|
|
78
|
+
YYYY-MM-DD-<slug>.md
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Template content:
|
|
82
|
+
```markdown
|
|
83
|
+
---
|
|
84
|
+
title: Your Title
|
|
85
|
+
date: 2026-01-30 12:34
|
|
86
|
+
tags: tag1, tag2
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# Your Title
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Development
|
|
93
|
+
```bash
|
|
94
|
+
pnpm build # Build
|
|
95
|
+
pnpm start # Run from dist
|
|
96
|
+
pnpm test # Run tests (watch mode)
|
|
97
|
+
pnpm test:run # Run tests once
|
|
98
|
+
pnpm test:coverage # Coverage report
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 한국어
|
|
104
|
+
|
|
105
|
+
mattn/memo에서 영감을 받은 OpenTUI 기반 터미널 메모 앱입니다.
|
|
106
|
+
|
|
107
|
+
### 기능
|
|
108
|
+
- 메모 탐색 및 열기를 위한 터미널 UI
|
|
109
|
+
- 생성, 목록, 편집, 삭제, 검색, 출력 명령어 지원
|
|
110
|
+
- 마크다운 저장 (title, date, tags frontmatter 포함)
|
|
111
|
+
- 환경 변수를 통한 선호 에디터 설정
|
|
112
|
+
|
|
113
|
+
### 요구사항
|
|
114
|
+
- Node.js 17+ 또는 Bun
|
|
115
|
+
- pnpm
|
|
116
|
+
|
|
117
|
+
### 설치
|
|
118
|
+
```bash
|
|
119
|
+
pnpm install
|
|
120
|
+
pnpm build
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
CLI 전역 등록:
|
|
124
|
+
```bash
|
|
125
|
+
pnpm link --global
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 사용법
|
|
129
|
+
TUI 실행:
|
|
130
|
+
```bash
|
|
131
|
+
openmemo
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
메모 생성:
|
|
135
|
+
```bash
|
|
136
|
+
openmemo new
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
기타 명령어:
|
|
140
|
+
```bash
|
|
141
|
+
openmemo list # 메모 목록
|
|
142
|
+
openmemo edit <query> # 메모 편집
|
|
143
|
+
openmemo delete <query> # 메모 삭제
|
|
144
|
+
openmemo grep <pattern> # 내용 검색
|
|
145
|
+
openmemo cat <query> # 메모 출력
|
|
146
|
+
openmemo help # 도움말
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
참고:
|
|
150
|
+
- `<query>`는 메모 ID 또는 제목과 부분 일치합니다. 여러 개가 일치하면 선택 프롬프트가 표시됩니다.
|
|
151
|
+
- `grep`은 유효한 정규식이면 대소문자 무시 정규식 검색을, 아니면 부분 문자열 검색을 수행합니다.
|
|
152
|
+
- TUI에서 `q` 또는 `Esc`를 눌러 종료합니다.
|
|
153
|
+
|
|
154
|
+
### 설정
|
|
155
|
+
환경 변수:
|
|
156
|
+
- `OPEN_MEMO_DIR`: 메모 저장 디렉토리 변경
|
|
157
|
+
- `OPEN_MEMO_EDITOR`: 선호 에디터 명령어
|
|
158
|
+
- `VISUAL` / `EDITOR`: `OPEN_MEMO_EDITOR` 미설정 시 대체
|
|
159
|
+
|
|
160
|
+
에디터 우선순위:
|
|
161
|
+
1. `OPEN_MEMO_EDITOR`
|
|
162
|
+
2. `VISUAL`
|
|
163
|
+
3. `EDITOR`
|
|
164
|
+
4. `vi`
|
|
165
|
+
|
|
166
|
+
### 데이터 저장
|
|
167
|
+
기본 디렉토리:
|
|
168
|
+
```
|
|
169
|
+
~/.openmemo/memos
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
파일명 형식:
|
|
173
|
+
```
|
|
174
|
+
YYYY-MM-DD-<slug>.md
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
템플릿:
|
|
178
|
+
```markdown
|
|
179
|
+
---
|
|
180
|
+
title: 제목
|
|
181
|
+
date: 2026-01-30 12:34
|
|
182
|
+
tags: 태그1, 태그2
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
# 제목
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 개발
|
|
189
|
+
```bash
|
|
190
|
+
pnpm build # 빌드
|
|
191
|
+
pnpm start # dist에서 실행
|
|
192
|
+
pnpm test # 테스트 (watch 모드)
|
|
193
|
+
pnpm test:run # 테스트 단일 실행
|
|
194
|
+
pnpm test:coverage # 커버리지 리포트
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 日本語
|
|
200
|
+
|
|
201
|
+
mattn/memoにインスパイアされたOpenTUIベースのターミナルメモアプリです。
|
|
202
|
+
|
|
203
|
+
### 機能
|
|
204
|
+
- メモの閲覧・開くためのターミナルUI
|
|
205
|
+
- 作成、一覧、編集、削除、検索、表示コマンド
|
|
206
|
+
- マークダウン保存(title, date, tags frontmatter付き)
|
|
207
|
+
- 環境変数でお好みのエディターを設定
|
|
208
|
+
|
|
209
|
+
### 必要条件
|
|
210
|
+
- Node.js 17+ または Bun
|
|
211
|
+
- pnpm
|
|
212
|
+
|
|
213
|
+
### インストール
|
|
214
|
+
```bash
|
|
215
|
+
pnpm install
|
|
216
|
+
pnpm build
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
CLIをグローバル登録:
|
|
220
|
+
```bash
|
|
221
|
+
pnpm link --global
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 使い方
|
|
225
|
+
TUIを起動:
|
|
226
|
+
```bash
|
|
227
|
+
openmemo
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
メモを作成:
|
|
231
|
+
```bash
|
|
232
|
+
openmemo new
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
その他のコマンド:
|
|
236
|
+
```bash
|
|
237
|
+
openmemo list # メモ一覧
|
|
238
|
+
openmemo edit <query> # メモ編集
|
|
239
|
+
openmemo delete <query> # メモ削除
|
|
240
|
+
openmemo grep <pattern> # 内容検索
|
|
241
|
+
openmemo cat <query> # メモ表示
|
|
242
|
+
openmemo help # ヘルプ
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
備考:
|
|
246
|
+
- `<query>`はメモIDまたはタイトルに部分一致します。複数一致する場合は選択プロンプトが表示されます。
|
|
247
|
+
- `grep`は有効な正規表現なら大文字小文字無視の正規表現検索を、そうでなければ部分文字列検索を行います。
|
|
248
|
+
- TUIでは`q`または`Esc`で終了します。
|
|
249
|
+
|
|
250
|
+
### 設定
|
|
251
|
+
環境変数:
|
|
252
|
+
- `OPEN_MEMO_DIR`: メモ保存ディレクトリの変更
|
|
253
|
+
- `OPEN_MEMO_EDITOR`: お好みのエディターコマンド
|
|
254
|
+
- `VISUAL` / `EDITOR`: `OPEN_MEMO_EDITOR`未設定時の代替
|
|
255
|
+
|
|
256
|
+
エディター優先順位:
|
|
257
|
+
1. `OPEN_MEMO_EDITOR`
|
|
258
|
+
2. `VISUAL`
|
|
259
|
+
3. `EDITOR`
|
|
260
|
+
4. `vi`
|
|
261
|
+
|
|
262
|
+
### データ保存
|
|
263
|
+
デフォルトディレクトリ:
|
|
264
|
+
```
|
|
265
|
+
~/.openmemo/memos
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
ファイル名形式:
|
|
269
|
+
```
|
|
270
|
+
YYYY-MM-DD-<slug>.md
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
テンプレート:
|
|
274
|
+
```markdown
|
|
275
|
+
---
|
|
276
|
+
title: タイトル
|
|
277
|
+
date: 2026-01-30 12:34
|
|
278
|
+
tags: タグ1, タグ2
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
# タイトル
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 開発
|
|
285
|
+
```bash
|
|
286
|
+
pnpm build # ビルド
|
|
287
|
+
pnpm start # distから実行
|
|
288
|
+
pnpm test # テスト(watchモード)
|
|
289
|
+
pnpm test:run # テスト単発実行
|
|
290
|
+
pnpm test:coverage # カバレッジレポート
|
|
291
|
+
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from "node:readline/promises";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { openEditor } from "./editor.js";
|
|
5
|
+
import { createMemo, deleteMemo, listMemos, } from "./storage.js";
|
|
6
|
+
import { runTui } from "./tui.js";
|
|
7
|
+
const COLUMN_WIDTH = 30;
|
|
8
|
+
const COMMANDS = ["new", "list", "edit", "delete", "grep", "cat", "help"];
|
|
9
|
+
async function main() {
|
|
10
|
+
const [command, ...args] = process.argv.slice(2);
|
|
11
|
+
if (!command) {
|
|
12
|
+
await runTui();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
16
|
+
printHelp();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!COMMANDS.includes(command)) {
|
|
20
|
+
console.error("Unknown command: " + command);
|
|
21
|
+
printHelp();
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
switch (command) {
|
|
25
|
+
case "new":
|
|
26
|
+
await handleNew();
|
|
27
|
+
return;
|
|
28
|
+
case "list":
|
|
29
|
+
await handleList();
|
|
30
|
+
return;
|
|
31
|
+
case "edit":
|
|
32
|
+
await handleEdit(args.join(" ").trim());
|
|
33
|
+
return;
|
|
34
|
+
case "delete":
|
|
35
|
+
await handleDelete(args.join(" ").trim());
|
|
36
|
+
return;
|
|
37
|
+
case "grep":
|
|
38
|
+
await handleGrep(args.join(" ").trim());
|
|
39
|
+
return;
|
|
40
|
+
case "cat":
|
|
41
|
+
await handleCat(args.join(" ").trim());
|
|
42
|
+
return;
|
|
43
|
+
default:
|
|
44
|
+
printHelp();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function handleNew() {
|
|
49
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
50
|
+
try {
|
|
51
|
+
const title = (await rl.question("Title: ")).trim();
|
|
52
|
+
if (!title) {
|
|
53
|
+
console.error("Title is required.");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const tagsRaw = (await rl.question("Tags (comma separated, optional): ")).trim();
|
|
57
|
+
const tags = tagsRaw ? tagsRaw.split(",").map((tag) => tag.trim()).filter(Boolean) : [];
|
|
58
|
+
const memo = await createMemo(title, tags);
|
|
59
|
+
openEditor(memo.filePath);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
rl.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function handleList() {
|
|
66
|
+
const memos = await listMemos();
|
|
67
|
+
if (memos.length === 0) {
|
|
68
|
+
console.log("No memos found.");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
memos.forEach((memo) => {
|
|
72
|
+
const name = memo.id.padEnd(COLUMN_WIDTH, " ");
|
|
73
|
+
console.log(name + " : " + memo.title);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function handleEdit(query) {
|
|
77
|
+
const memo = await selectMemo(query);
|
|
78
|
+
openEditor(memo.filePath);
|
|
79
|
+
}
|
|
80
|
+
async function handleDelete(query) {
|
|
81
|
+
const memo = await selectMemo(query);
|
|
82
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
83
|
+
try {
|
|
84
|
+
const answer = (await rl.question('Delete "' + memo.title + '"? [y/N]: ')).trim().toLowerCase();
|
|
85
|
+
if (answer === "y" || answer === "yes") {
|
|
86
|
+
await deleteMemo(memo.id);
|
|
87
|
+
console.log("Deleted " + memo.id);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log("Canceled.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
rl.close();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function handleCat(query) {
|
|
98
|
+
const memo = await selectMemo(query);
|
|
99
|
+
console.log(memo.content);
|
|
100
|
+
}
|
|
101
|
+
async function handleGrep(pattern) {
|
|
102
|
+
if (!pattern) {
|
|
103
|
+
console.error("Pattern is required for grep.");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const memos = await listMemos();
|
|
107
|
+
if (memos.length === 0) {
|
|
108
|
+
console.log("No memos found.");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const matcher = buildMatcher(pattern);
|
|
112
|
+
memos.forEach((memo) => {
|
|
113
|
+
const lines = memo.content.split("\n");
|
|
114
|
+
lines.forEach((line, index) => {
|
|
115
|
+
const match = matcher ? matcher.test(line) : line.toLowerCase().includes(pattern.toLowerCase());
|
|
116
|
+
if (match) {
|
|
117
|
+
console.log(memo.id + ":" + String(index + 1) + ":" + line);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function selectMemo(query) {
|
|
123
|
+
const memos = await listMemos();
|
|
124
|
+
if (memos.length === 0) {
|
|
125
|
+
console.error("No memos found.");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
if (!query) {
|
|
129
|
+
return promptSelect(memos, "Select memo");
|
|
130
|
+
}
|
|
131
|
+
const normalized = query.toLowerCase();
|
|
132
|
+
const exact = memos.find((memo) => memo.id === query);
|
|
133
|
+
if (exact)
|
|
134
|
+
return exact;
|
|
135
|
+
const matches = memos.filter((memo) => memo.title.toLowerCase().includes(normalized) || memo.id.toLowerCase().includes(normalized));
|
|
136
|
+
if (matches.length === 1)
|
|
137
|
+
return matches[0];
|
|
138
|
+
if (matches.length === 0) {
|
|
139
|
+
console.error('No memo matches "' + query + '".');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
return promptSelect(matches, 'Multiple matches for "' + query + '"');
|
|
143
|
+
}
|
|
144
|
+
async function promptSelect(memos, label) {
|
|
145
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
146
|
+
try {
|
|
147
|
+
console.log(label);
|
|
148
|
+
memos.forEach((memo, index) => {
|
|
149
|
+
console.log(String(index + 1) + ". " + memo.title + " (" + memo.id + ")");
|
|
150
|
+
});
|
|
151
|
+
const answer = (await rl.question("Choose number: ")).trim();
|
|
152
|
+
const index = Number(answer) - 1;
|
|
153
|
+
if (Number.isNaN(index) || index < 0 || index >= memos.length) {
|
|
154
|
+
console.error("Invalid selection.");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
return memos[index];
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
rl.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function buildMatcher(pattern) {
|
|
164
|
+
try {
|
|
165
|
+
return new RegExp(pattern, "i");
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function printHelp() {
|
|
172
|
+
console.log([
|
|
173
|
+
"openmemo - OpenTUI-based memo app",
|
|
174
|
+
"",
|
|
175
|
+
"Usage:",
|
|
176
|
+
" openmemo Launch TUI",
|
|
177
|
+
" openmemo new Create memo",
|
|
178
|
+
" openmemo list List memos",
|
|
179
|
+
" openmemo edit <query> Edit memo",
|
|
180
|
+
" openmemo delete <query> Delete memo",
|
|
181
|
+
" openmemo grep <pattern> Search memo contents",
|
|
182
|
+
" openmemo cat <query> View memo",
|
|
183
|
+
" openmemo help Show help",
|
|
184
|
+
"",
|
|
185
|
+
].join("\n"));
|
|
186
|
+
}
|
|
187
|
+
main().catch((error) => {
|
|
188
|
+
console.error(error);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function getMemoDir() {
|
|
4
|
+
const fromEnv = process.env.OPEN_MEMO_DIR;
|
|
5
|
+
if (fromEnv && fromEnv.trim() !== "") {
|
|
6
|
+
return fromEnv;
|
|
7
|
+
}
|
|
8
|
+
return path.join(os.homedir(), ".openmemo", "memos");
|
|
9
|
+
}
|
|
10
|
+
export function getEditorCommand() {
|
|
11
|
+
return (process.env.OPEN_MEMO_EDITOR ||
|
|
12
|
+
process.env.VISUAL ||
|
|
13
|
+
process.env.EDITOR ||
|
|
14
|
+
"vi");
|
|
15
|
+
}
|
package/dist/editor.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { getEditorCommand } from "./config.js";
|
|
3
|
+
export function openEditor(filePath) {
|
|
4
|
+
const editor = getEditorCommand();
|
|
5
|
+
const result = spawnSync(editor, [filePath], {
|
|
6
|
+
stdio: "inherit",
|
|
7
|
+
});
|
|
8
|
+
if (result.error) {
|
|
9
|
+
throw result.error;
|
|
10
|
+
}
|
|
11
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getMemoDir } from "./config.js";
|
|
4
|
+
import { formatDate, formatDateTime, slugify } from "./utils.js";
|
|
5
|
+
export async function ensureMemoDir() {
|
|
6
|
+
const memoDir = getMemoDir();
|
|
7
|
+
await fs.mkdir(memoDir, { recursive: true });
|
|
8
|
+
return memoDir;
|
|
9
|
+
}
|
|
10
|
+
export async function listMemos() {
|
|
11
|
+
const memoDir = await ensureMemoDir();
|
|
12
|
+
const entries = await fs.readdir(memoDir, { withFileTypes: true });
|
|
13
|
+
const files = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md"));
|
|
14
|
+
const memos = await Promise.all(files.map(async (entry) => {
|
|
15
|
+
const filePath = path.join(memoDir, entry.name);
|
|
16
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
17
|
+
const { title, date, tags } = parseMemoMetadata(content, entry.name);
|
|
18
|
+
return {
|
|
19
|
+
id: entry.name,
|
|
20
|
+
title,
|
|
21
|
+
date,
|
|
22
|
+
tags,
|
|
23
|
+
filePath,
|
|
24
|
+
content,
|
|
25
|
+
};
|
|
26
|
+
}));
|
|
27
|
+
return memos.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
28
|
+
}
|
|
29
|
+
export async function loadMemo(id) {
|
|
30
|
+
const memoDir = await ensureMemoDir();
|
|
31
|
+
const filePath = path.join(memoDir, id);
|
|
32
|
+
try {
|
|
33
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
34
|
+
const { title, date, tags } = parseMemoMetadata(content, id);
|
|
35
|
+
return { id, title, date, tags, filePath, content };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code === "ENOENT") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function createMemo(title, tags) {
|
|
45
|
+
const memoDir = await ensureMemoDir();
|
|
46
|
+
const now = new Date();
|
|
47
|
+
const slug = slugify(title) || "memo";
|
|
48
|
+
const fileName = `${formatDate(now)}-${slug}.md`;
|
|
49
|
+
const filePath = path.join(memoDir, fileName);
|
|
50
|
+
const content = buildMemoTemplate(title, tags, now);
|
|
51
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
52
|
+
return {
|
|
53
|
+
id: fileName,
|
|
54
|
+
title,
|
|
55
|
+
date: now,
|
|
56
|
+
tags,
|
|
57
|
+
filePath,
|
|
58
|
+
content,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export async function deleteMemo(id) {
|
|
62
|
+
const memoDir = await ensureMemoDir();
|
|
63
|
+
const filePath = path.join(memoDir, id);
|
|
64
|
+
await fs.unlink(filePath);
|
|
65
|
+
}
|
|
66
|
+
export function buildMemoTemplate(title, tags, date) {
|
|
67
|
+
const tagValue = tags.length ? tags.join(", ") : "";
|
|
68
|
+
return [
|
|
69
|
+
"---",
|
|
70
|
+
`title: ${title}`,
|
|
71
|
+
`date: ${formatDateTime(date)}`,
|
|
72
|
+
`tags: ${tagValue}`,
|
|
73
|
+
"---",
|
|
74
|
+
"",
|
|
75
|
+
`# ${title}`,
|
|
76
|
+
"",
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
export function parseMemoMetadata(content, fallbackName) {
|
|
80
|
+
const frontmatter = parseFrontmatter(content);
|
|
81
|
+
const fallbackTitle = fallbackName.replace(/\.md$/, "");
|
|
82
|
+
const title = frontmatter.meta.title || extractTitleFromBody(frontmatter.body) || fallbackTitle;
|
|
83
|
+
const date = frontmatter.meta.date ? new Date(frontmatter.meta.date) : dateFromFilename(fallbackName);
|
|
84
|
+
const tags = parseTags(frontmatter.meta.tags);
|
|
85
|
+
return {
|
|
86
|
+
title,
|
|
87
|
+
date: isNaN(date.getTime()) ? new Date() : date,
|
|
88
|
+
tags,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function parseFrontmatter(content) {
|
|
92
|
+
if (!content.startsWith("---\n")) {
|
|
93
|
+
return { meta: {}, body: content };
|
|
94
|
+
}
|
|
95
|
+
const endIndex = content.indexOf("\n---", 4);
|
|
96
|
+
if (endIndex === -1) {
|
|
97
|
+
return { meta: {}, body: content };
|
|
98
|
+
}
|
|
99
|
+
const raw = content.slice(4, endIndex).trim();
|
|
100
|
+
const body = content.slice(endIndex + 4).replace(/^\s*\n/, "");
|
|
101
|
+
const meta = {};
|
|
102
|
+
raw.split("\n").forEach((line) => {
|
|
103
|
+
const [key, ...rest] = line.split(":");
|
|
104
|
+
const normalizedKey = key?.trim();
|
|
105
|
+
if (!normalizedKey)
|
|
106
|
+
return;
|
|
107
|
+
meta[normalizedKey] = rest.join(":").trim();
|
|
108
|
+
});
|
|
109
|
+
return { meta, body };
|
|
110
|
+
}
|
|
111
|
+
function extractTitleFromBody(body) {
|
|
112
|
+
const lines = body.split("\n").map((line) => line.trim());
|
|
113
|
+
const heading = lines.find((line) => line.startsWith("#"));
|
|
114
|
+
if (heading) {
|
|
115
|
+
return heading.replace(/^#+\s*/, "").trim();
|
|
116
|
+
}
|
|
117
|
+
return lines.find((line) => line.length > 0) || "";
|
|
118
|
+
}
|
|
119
|
+
function parseTags(raw) {
|
|
120
|
+
if (!raw)
|
|
121
|
+
return [];
|
|
122
|
+
const trimmed = raw.replace(/^[\[]|[\]]$/g, "");
|
|
123
|
+
return trimmed
|
|
124
|
+
.split(",")
|
|
125
|
+
.map((tag) => tag.trim())
|
|
126
|
+
.filter((tag) => tag.length > 0);
|
|
127
|
+
}
|
|
128
|
+
function dateFromFilename(fileName) {
|
|
129
|
+
const match = fileName.match(/^(\d{4}-\d{2}-\d{2})-/);
|
|
130
|
+
if (!match)
|
|
131
|
+
return new Date();
|
|
132
|
+
return new Date(`${match[1]}T00:00:00`);
|
|
133
|
+
}
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { BoxRenderable, createCliRenderer, SelectRenderable, SelectRenderableEvents, TextRenderable, } from "@opentui/core";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { openEditor } from "./editor.js";
|
|
4
|
+
import { listMemos } from "./storage.js";
|
|
5
|
+
import { truncateLines } from "./utils.js";
|
|
6
|
+
const EMPTY_MESSAGE = "No memos yet. Run openmemo new to create one.";
|
|
7
|
+
export async function runTui() {
|
|
8
|
+
const memos = await listMemos();
|
|
9
|
+
const renderer = await createCliRenderer({
|
|
10
|
+
exitOnCtrlC: true,
|
|
11
|
+
targetFps: 30,
|
|
12
|
+
});
|
|
13
|
+
renderer.setBackgroundColor("#0f172a");
|
|
14
|
+
const container = new BoxRenderable(renderer, {
|
|
15
|
+
id: "openmemo-container",
|
|
16
|
+
flexDirection: "row",
|
|
17
|
+
width: "100%",
|
|
18
|
+
height: "100%",
|
|
19
|
+
padding: 1,
|
|
20
|
+
});
|
|
21
|
+
renderer.root.add(container);
|
|
22
|
+
const listBox = new BoxRenderable(renderer, {
|
|
23
|
+
id: "openmemo-list",
|
|
24
|
+
flexGrow: 1,
|
|
25
|
+
marginRight: 1,
|
|
26
|
+
border: true,
|
|
27
|
+
borderStyle: "single",
|
|
28
|
+
borderColor: "#334155",
|
|
29
|
+
title: "Memos",
|
|
30
|
+
titleAlignment: "left",
|
|
31
|
+
backgroundColor: "#0b1220",
|
|
32
|
+
shouldFill: true,
|
|
33
|
+
});
|
|
34
|
+
const previewBox = new BoxRenderable(renderer, {
|
|
35
|
+
id: "openmemo-preview",
|
|
36
|
+
flexGrow: 2,
|
|
37
|
+
border: true,
|
|
38
|
+
borderStyle: "single",
|
|
39
|
+
borderColor: "#334155",
|
|
40
|
+
title: "Preview",
|
|
41
|
+
titleAlignment: "left",
|
|
42
|
+
backgroundColor: "#0b1220",
|
|
43
|
+
shouldFill: true,
|
|
44
|
+
});
|
|
45
|
+
container.add(listBox);
|
|
46
|
+
container.add(previewBox);
|
|
47
|
+
const previewText = new TextRenderable(renderer, {
|
|
48
|
+
id: "openmemo-preview-text",
|
|
49
|
+
width: "100%",
|
|
50
|
+
height: "100%",
|
|
51
|
+
fg: "#e2e8f0",
|
|
52
|
+
content: "",
|
|
53
|
+
});
|
|
54
|
+
previewBox.add(previewText);
|
|
55
|
+
if (memos.length === 0) {
|
|
56
|
+
previewText.content = EMPTY_MESSAGE;
|
|
57
|
+
renderer.start();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const options = memos.map((memo) => ({
|
|
61
|
+
name: memo.title,
|
|
62
|
+
description: memo.id,
|
|
63
|
+
value: memo,
|
|
64
|
+
}));
|
|
65
|
+
const selectElement = new SelectRenderable(renderer, {
|
|
66
|
+
id: "openmemo-select",
|
|
67
|
+
height: "100%",
|
|
68
|
+
options,
|
|
69
|
+
backgroundColor: "transparent",
|
|
70
|
+
focusedBackgroundColor: "transparent",
|
|
71
|
+
selectedBackgroundColor: "#1e293b",
|
|
72
|
+
textColor: "#e2e8f0",
|
|
73
|
+
selectedTextColor: "#38bdf8",
|
|
74
|
+
descriptionColor: "#94a3b8",
|
|
75
|
+
selectedDescriptionColor: "#cbd5e1",
|
|
76
|
+
showDescription: true,
|
|
77
|
+
showScrollIndicator: true,
|
|
78
|
+
wrapSelection: false,
|
|
79
|
+
fastScrollStep: 5,
|
|
80
|
+
});
|
|
81
|
+
listBox.add(selectElement);
|
|
82
|
+
selectElement.focus();
|
|
83
|
+
const updatePreview = (memo) => {
|
|
84
|
+
if (!memo) {
|
|
85
|
+
previewText.content = EMPTY_MESSAGE;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const header = `${memo.title}\n${memo.id}\n`;
|
|
89
|
+
const body = truncateLines(stripFrontmatter(memo.content), 24);
|
|
90
|
+
previewText.content = `${header}\n${body}`;
|
|
91
|
+
};
|
|
92
|
+
updatePreview(memos[0]);
|
|
93
|
+
selectElement.on(SelectRenderableEvents.SELECTION_CHANGED, (index, option) => {
|
|
94
|
+
updatePreview(option.value);
|
|
95
|
+
});
|
|
96
|
+
selectElement.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
|
|
97
|
+
const memo = option.value;
|
|
98
|
+
renderer.destroy();
|
|
99
|
+
openEditor(memo.filePath);
|
|
100
|
+
process.exit(0);
|
|
101
|
+
});
|
|
102
|
+
renderer.keyInput.on("keypress", (key) => {
|
|
103
|
+
if (key.name === "q" || key.name === "escape") {
|
|
104
|
+
renderer.destroy();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
renderer.start();
|
|
108
|
+
}
|
|
109
|
+
function stripFrontmatter(content) {
|
|
110
|
+
if (!content.startsWith("---\n"))
|
|
111
|
+
return content;
|
|
112
|
+
const endIndex = content.indexOf("\n---", 4);
|
|
113
|
+
if (endIndex === -1)
|
|
114
|
+
return content;
|
|
115
|
+
return content.slice(endIndex + 4).replace(/^\s*\n/, "");
|
|
116
|
+
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function pad2(value) {
|
|
2
|
+
return String(value).padStart(2, "0");
|
|
3
|
+
}
|
|
4
|
+
export function formatDate(date) {
|
|
5
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
|
6
|
+
}
|
|
7
|
+
export function formatDateTime(date) {
|
|
8
|
+
return `${formatDate(date)} ${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
|
|
9
|
+
}
|
|
10
|
+
export function slugify(input) {
|
|
11
|
+
return input
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/\s+/g, "-")
|
|
16
|
+
.replace(/-+/g, "-");
|
|
17
|
+
}
|
|
18
|
+
export function truncateLines(input, maxLines) {
|
|
19
|
+
const lines = input.split("\n");
|
|
20
|
+
if (lines.length <= maxLines)
|
|
21
|
+
return input;
|
|
22
|
+
return `${lines.slice(0, maxLines).join("\n")}\n...`;
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openmemo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenTUI-based memo app inspired by mattn/memo",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openmemo": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/arkjun/openmemo.git"
|
|
15
|
+
},
|
|
16
|
+
"author": "arkjun",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"memo",
|
|
20
|
+
"cli",
|
|
21
|
+
"terminal",
|
|
22
|
+
"tui",
|
|
23
|
+
"note",
|
|
24
|
+
"markdown"
|
|
25
|
+
],
|
|
26
|
+
"homepage": "https://github.com/arkjun/openmemo#readme",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/arkjun/openmemo/issues"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"start": "bun dist/cli.js",
|
|
36
|
+
"test": "vitest",
|
|
37
|
+
"test:run": "vitest run",
|
|
38
|
+
"test:watch": "vitest watch",
|
|
39
|
+
"test:coverage": "vitest run --coverage",
|
|
40
|
+
"prepublishOnly": "npm run build && npm run test:run"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@opentui/core": "^0.1.75"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.11.30",
|
|
47
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
48
|
+
"typescript": "^5.4.5",
|
|
49
|
+
"vitest": "^4.0.18"
|
|
50
|
+
}
|
|
51
|
+
}
|