@burakboduroglu/penote 3.0.1

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/library/cli.js ADDED
@@ -0,0 +1,1253 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * penote CLI v2
4
+ *
5
+ * Komutlar:
6
+ * list [--cat <cat>] [--search <q>] Notları listele / filtrele
7
+ * open <id> Notu varsayılan editörde aç
8
+ * open --tui Interaktif TUI (ok tuşu + Enter)
9
+ * open --editor Kitaplık UI'ını tarayıcıda aç
10
+ * open --browser <id> Tek notu tarayıcıda render et
11
+ * search <q> Kısayol: list --search <q>
12
+ * help Yardım
13
+ *
14
+ * Flag'ler (list komutu):
15
+ * --cat java | js | py | sql | mongo
16
+ * --search Başlık/açıklama arama
17
+ */
18
+ "use strict";
19
+
20
+ const path = require("path");
21
+ const fs = require("fs");
22
+ const { spawn, spawnSync, execSync } = require("child_process");
23
+ const readline = require("readline");
24
+
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+ // ANSI renk yardımcıları
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+ const C = {
29
+ reset: "\x1b[0m",
30
+ bold: "\x1b[1m",
31
+ dim: "\x1b[2m",
32
+ reverse: "\x1b[7m",
33
+ cyan: "\x1b[36m",
34
+ yellow: "\x1b[33m",
35
+ green: "\x1b[32m",
36
+ blue: "\x1b[34m",
37
+ magenta: "\x1b[35m",
38
+ red: "\x1b[31m",
39
+ white: "\x1b[37m",
40
+ bgBlue: "\x1b[44m",
41
+ bgCyan: "\x1b[46m",
42
+ };
43
+ const c = (col, s) => `${C[col] || ""}${s}${C.reset}`;
44
+ const cb = (col, s) => `${C.bold}${C[col] || ""}${s}${C.reset}`;
45
+
46
+ function renderInline(text) {
47
+ const code = [];
48
+ let out = text.replace(/`([^`]+)`/g, (_, value) => {
49
+ code.push(c("yellow", value));
50
+ return `\x00CODE${code.length - 1}\x00`;
51
+ });
52
+
53
+ out = out
54
+ .replace(/\*\*\*(.+?)\*\*\*/g, (_, value) => cb("white", value))
55
+ .replace(/\*\*(.+?)\*\*/g, (_, value) => cb("white", value))
56
+ .replace(/\*(.+?)\*/g, (_, value) => c("magenta", value))
57
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `${c("cyan", label)} ${c("dim", `(${href})`)}`);
58
+
59
+ return out.replace(/\x00CODE(\d+)\x00/g, (_, i) => code[Number(i)] || "");
60
+ }
61
+
62
+ function renderMarkdownTerminal(md) {
63
+ const lines = md.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
64
+ const out = [];
65
+ let inCode = false;
66
+ let codeLang = "";
67
+
68
+ for (const raw of lines) {
69
+ const line = raw.replace(/\t/g, " ");
70
+ const fence = line.match(/^```(.*)$/);
71
+
72
+ if (fence) {
73
+ inCode = !inCode;
74
+ codeLang = inCode ? fence[1].trim() : "";
75
+ out.push(c("dim", inCode ? `┌─ code ${codeLang}`.trimEnd() : "└─"));
76
+ continue;
77
+ }
78
+
79
+ if (inCode) {
80
+ out.push(c("dim", "│ ") + c("yellow", line || " "));
81
+ continue;
82
+ }
83
+
84
+ if (!line.trim()) {
85
+ out.push("");
86
+ continue;
87
+ }
88
+
89
+ const heading = line.match(/^(#{1,6})\s+(.+)$/);
90
+ if (heading) {
91
+ const level = heading[1].length;
92
+ const title = renderInline(heading[2].trim());
93
+ const prefix = level <= 2 ? "█" : "◆";
94
+ const color = level === 1 ? "cyan" : level === 2 ? "blue" : "magenta";
95
+ out.push(level <= 2 ? "" : c("dim", ""));
96
+ out.push(cb(color, `${prefix} ${title}`));
97
+ if (level <= 2) out.push(c("dim", "─".repeat(Math.min(70, Math.max(12, heading[2].length + 4)))));
98
+ continue;
99
+ }
100
+
101
+ if (/^\s*[-*_]{3,}\s*$/.test(line)) {
102
+ out.push(c("dim", "─".repeat(70)));
103
+ continue;
104
+ }
105
+
106
+ const quote = line.match(/^>\s?(.*)$/);
107
+ if (quote) {
108
+ out.push(c("blue", "▌ ") + c("dim", renderInline(quote[1])));
109
+ continue;
110
+ }
111
+
112
+ const unordered = line.match(/^(\s*)[-*+]\s+(.+)$/);
113
+ if (unordered) {
114
+ out.push(`${unordered[1]}${c("cyan", "•")} ${renderInline(unordered[2])}`);
115
+ continue;
116
+ }
117
+
118
+ const ordered = line.match(/^(\s*)(\d+)[.)]\s+(.+)$/);
119
+ if (ordered) {
120
+ out.push(`${ordered[1]}${c("cyan", `${ordered[2]}.`)} ${renderInline(ordered[3])}`);
121
+ continue;
122
+ }
123
+
124
+ if (line.includes("|") && /^\s*\|?[-:|\s]+\|/.test(line)) {
125
+ out.push(c("dim", line));
126
+ continue;
127
+ }
128
+
129
+ if (line.includes("|")) {
130
+ out.push(c("cyan", line));
131
+ continue;
132
+ }
133
+
134
+ out.push(renderInline(line));
135
+ }
136
+
137
+ return out;
138
+ }
139
+
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+ // Katalog
142
+ // ─────────────────────────────────────────────────────────────────────────────
143
+ const ROOT = path.resolve(__dirname, "..");
144
+
145
+ const NOTES = [
146
+ { id: 1, cat: "java", title: "Lombok", desc: "Lombok anotasyonları ve örnekleri", file: "Java-Notes/lombok.md" },
147
+ { id: 2, cat: "java", title: "JPA / Hibernate", desc: "Hibernate anotasyonları ve ORM örnekleri", file: "Java-Notes/jpa_hibernate.md" },
148
+ { id: 3, cat: "java", title: "Spring Boot", desc: "Spring Boot anotasyonları ve uygulamalar", file: "Java-Notes/spring_boot_framework.md" },
149
+ { id: 4, cat: "js", title: "Dizi Metodları", desc: "map, filter, reduce ve diğer dizi metodları", file: "Javascript-Notes/javascirpt_array_methods.md" },
150
+ { id: 5, cat: "js", title: "Closure/Curry/Compose",desc: "Fonksiyonel JS kavramları", file: "Javascript-Notes/closures_currying_compose.md" },
151
+ { id: 6, cat: "js", title: "Async JS", desc: "Fetch API, Promise, async/await", file: "Javascript-Notes/async_js.md" },
152
+ { id: 7, cat: "js", title: "Regex (1)", desc: "Düzenli ifadeler — temel kalıplar", file: "Javascript-Notes/regex_part_1.md" },
153
+ { id: 8, cat: "py", title: "Python Temel 1", desc: "Veri yapıları: list, dict, tuple, set", file: "Python-Notes/python_basic_1.md" },
154
+ { id: 9, cat: "py", title: "Python Temel 2", desc: "Fonksiyonlar, döngüler, koşullar", file: "Python-Notes/python_basic_2.md" },
155
+ { id: 10, cat: "py", title: "Python Temel 3", desc: "Hata yönetimi, dosya işlemleri", file: "Python-Notes/python_basic_3.md" },
156
+ { id: 11, cat: "py", title: "Python İleri 1", desc: "List / dict / set comprehension", file: "Python-Notes/advanced_python_1.md" },
157
+ { id: 12, cat: "py", title: "Python İleri 2", desc: "map, filter, reduce, lambda", file: "Python-Notes/advanced_python_2.md" },
158
+ { id: 13, cat: "py", title: "Python Veritabanı", desc: "psycopg2, SQLite, veritabanı işlemleri", file: "Python-Notes/python_db_process.md" },
159
+ { id: 14, cat: "sql", title: "SQL Temel 1", desc: "SELECT, INSERT, UPDATE, DELETE", file: "SQL-Notes/sql_basic_1.md" },
160
+ { id: 15, cat: "sql", title: "SQL Temel 2", desc: "WHERE, AND, OR, LIKE, IN, BETWEEN", file: "SQL-Notes/sql_basic_2.md" },
161
+ { id: 16, cat: "sql", title: "SQL İleri", desc: "JOIN, GROUP BY, HAVING, subquery", file: "SQL-Notes/sql_advanced_1.md" },
162
+ { id: 17, cat: "sql", title: "psql Terminal", desc: "PostgreSQL komut satırı kullanımı", file: "SQL-Notes/psql_on_terminal.md" },
163
+ { id: 18, cat: "mongo", title: "MongoDB Temel 1", desc: "CRUD işlemleri, sorgular, operatörler", file: "MongoDB-Notes/mongodb_basic_1.md" },
164
+ ];
165
+
166
+ const CAT_COLOR = { java: "yellow", js: "cyan", py: "blue", sql: "magenta", mongo: "green" };
167
+ const CAT_LABEL = { java: "Java ", js: "JavaScript", py: "Python ", sql: "SQL ", mongo: "MongoDB " };
168
+ const VALID_CATS = Object.keys(CAT_COLOR);
169
+
170
+ // ─────────────────────────────────────────────────────────────────────────────
171
+ // Flag parser (--key value veya --flag)
172
+ // ─────────────────────────────────────────────────────────────────────────────
173
+ function parseArgs(argv) {
174
+ const flags = {};
175
+ const pos = [];
176
+ for (let i = 0; i < argv.length; i++) {
177
+ const a = argv[i];
178
+ if (a.startsWith("--")) {
179
+ const key = a.slice(2);
180
+ const next = argv[i + 1];
181
+ if (next && !next.startsWith("--")) { flags[key] = next; i++; }
182
+ else { flags[key] = true; }
183
+ } else {
184
+ pos.push(a);
185
+ }
186
+ }
187
+ return { flags, pos };
188
+ }
189
+
190
+ // ─────────────────────────────────────────────────────────────────────────────
191
+ // Yazdırma yardımcıları
192
+ // ─────────────────────────────────────────────────────────────────────────────
193
+ function printNote(n, highlight) {
194
+ const col = CAT_COLOR[n.cat] || "white";
195
+ const label = c(col, CAT_LABEL[n.cat]);
196
+ const id = c("dim", `[${String(n.id).padStart(2, "0")}]`);
197
+ const title = highlight
198
+ ? cb("white", n.title)
199
+ : cb("white", n.title);
200
+ console.log(` ${id} ${label} ${title}`);
201
+ console.log(` ${c("dim", n.desc)}`);
202
+ console.log(` ${c("dim", n.file)}`);
203
+ }
204
+
205
+ function printSectionHeader(text) {
206
+ console.log();
207
+ const line = `── ${text} `;
208
+ process.stdout.write(c("cyan", line));
209
+ console.log(c("dim", "─".repeat(Math.max(0, 52 - line.length))));
210
+ }
211
+
212
+ function printHelp() {
213
+ const D = (s) => c("dim", s);
214
+ console.log(`
215
+ ${cb("white", "penote")} ${D("v2")} — agentic learning docs CLI
216
+ ${D("─".repeat(58))}
217
+ ${cb("white", "KOMUTLAR")}
218
+
219
+ ${D("penote help")} ${D("# Komutları gösterir")}
220
+ ${D("penote list")} ${D("# Tüm notları listeler")}
221
+ ${D("penote list --cat java")} ${D("# Java notlarını listeler")}
222
+ ${D("penote list --cat py --search temel")} ${D("# Python notlarında arama yapar")}
223
+ ${D("penote search hibernate")} ${D("# Tüm notlarda arama yapar")}
224
+ ${D("penote open 3")} ${D("# 3 numaralı notu editörde açar")}
225
+ ${D("penote open --tui")} ${D("# TUI modunu açar")}
226
+ ${D("penote open --editor")} ${D("# Web arayüzünü açar")}
227
+ ${D("penote open --browser 6")} ${D("# 6 numaralı notu tarayıcıda açar")}
228
+
229
+ ${D("Kategori: java | js | py | sql | mongo")}
230
+ `);
231
+ }
232
+
233
+ // ─────────────────────────────────────────────────────────────────────────────
234
+ // Sistem açma yardımcıları
235
+ // ─────────────────────────────────────────────────────────────────────────────
236
+
237
+ /**
238
+ * Linux: text/markdown veya text/plain için kayıtlı default handler'ın
239
+ * .desktop dosya yolunu çözer. xdg-open içerik sniff'i yapıp .md dosyalarını
240
+ * Python (text/x-script.python) gibi tespit edebildiği için bu kestirme
241
+ * MIME tipini zorlayarak doğru editörü bulur.
242
+ */
243
+ function resolveLinuxTextHandler() {
244
+ if (process.platform !== "linux") return null;
245
+ const opts = { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] };
246
+ // text/plain öncelikli — kullanıcının genel editörünü tutar.
247
+ // text/markdown çoğu sistemde özel markdown viewer'a (OnlyOffice gibi)
248
+ // bağlı olabilir; bu istenmiyorsa DEVNOTE_EDITOR env var ile override edilir.
249
+ let desktop = "";
250
+ try { desktop = execSync("xdg-mime query default text/plain", opts).trim(); } catch {}
251
+ if (!desktop) {
252
+ try { desktop = execSync("xdg-mime query default text/markdown", opts).trim(); } catch {}
253
+ }
254
+ if (!desktop) return null;
255
+ const home = process.env.HOME || "";
256
+ const dirs = [
257
+ `${home}/.local/share/applications`,
258
+ "/usr/share/applications",
259
+ "/usr/local/share/applications",
260
+ "/var/lib/flatpak/exports/share/applications",
261
+ `${home}/.local/share/flatpak/exports/share/applications`,
262
+ ];
263
+ for (const d of dirs) {
264
+ if (!d) continue;
265
+ const p = path.join(d, desktop);
266
+ if (fs.existsSync(p)) return p;
267
+ }
268
+ return null;
269
+ }
270
+
271
+ /** Yerel dosya yolu açar (editör, dosya gezgini vb.) */
272
+ function openWithOS(absPath) {
273
+ // 1. Env var override — sadece DEVNOTE_EDITOR.
274
+ // VISUAL/EDITOR çoğu sistemde terminal editör (nano/vim) için ayarlı,
275
+ // GUI launcher yerine onu spawn etmek sessiz başarısızlığa neden olur.
276
+ const editor = process.env.DEVNOTE_EDITOR;
277
+ if (editor) {
278
+ try {
279
+ const parts = editor.split(/\s+/).filter(Boolean);
280
+ const child = spawn(parts[0], [...parts.slice(1), absPath], { detached: true, stdio: "ignore" });
281
+ child.unref();
282
+ return;
283
+ } catch {}
284
+ }
285
+
286
+ // 2. Linux: MIME content sniff bypass — text/markdown handler'ını zorla
287
+ if (process.platform === "linux") {
288
+ const desktopFile = resolveLinuxTextHandler();
289
+ if (desktopFile) {
290
+ try {
291
+ const r = spawnSync("gio", ["launch", desktopFile, absPath], { stdio: "ignore" });
292
+ if (r.status === 0) return;
293
+ } catch {}
294
+ }
295
+ }
296
+
297
+ // 3. Platform fallback (xdg-open / open / start)
298
+ try {
299
+ const child = process.platform === "win32"
300
+ ? spawn("cmd", ["/c", "start", "", absPath], { detached: true, stdio: "ignore", windowsHide: true })
301
+ : process.platform === "darwin"
302
+ ? spawn("open", [absPath], { detached: true, stdio: "ignore" })
303
+ : spawn("xdg-open", [absPath], { detached: true, stdio: "ignore" });
304
+ child.unref();
305
+ } catch {
306
+ console.log(c("yellow", ` Yol: ${absPath}`));
307
+ }
308
+ }
309
+
310
+ /** HTTP/HTTPS URL'yi varsayılan tarayıcıda açar */
311
+ function openUrl(url) {
312
+ try {
313
+ const child = process.platform === "win32"
314
+ ? spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore", windowsHide: true })
315
+ : process.platform === "darwin"
316
+ ? spawn("open", [url], { detached: true, stdio: "ignore" })
317
+ : spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
318
+ child.unref();
319
+ } catch {
320
+ console.log(c("yellow", ` URL: ${url}`));
321
+ }
322
+ }
323
+
324
+ // ─────────────────────────────────────────────────────────────────────────────
325
+ // CMD: list
326
+ // ─────────────────────────────────────────────────────────────────────────────
327
+ function cmdList(flags) {
328
+ const catFilter = flags.cat ? String(flags.cat).toLowerCase() : null;
329
+ const searchFilter = flags.search ? String(flags.search).toLowerCase() : null;
330
+
331
+ if (catFilter && !VALID_CATS.includes(catFilter)) {
332
+ console.error(c("red", ` Geçersiz kategori: "${catFilter}"`));
333
+ console.error(c("dim", ` Geçerli: ${VALID_CATS.join(", ")}`));
334
+ process.exit(1);
335
+ }
336
+
337
+ let results = NOTES;
338
+ if (catFilter) results = results.filter(n => n.cat === catFilter);
339
+ if (searchFilter) results = results.filter(n =>
340
+ n.title.toLowerCase().includes(searchFilter) ||
341
+ n.desc.toLowerCase().includes(searchFilter) ||
342
+ n.file.toLowerCase().includes(searchFilter)
343
+ );
344
+
345
+ console.log();
346
+ console.log(cb("white", " penote kitaplığı"));
347
+ if (catFilter || searchFilter) {
348
+ const parts = [];
349
+ if (catFilter) parts.push(`kategori: ${catFilter}`);
350
+ if (searchFilter) parts.push(`arama: "${searchFilter}"`);
351
+ console.log(c("dim", ` Filtre — ${parts.join(" · ")}`));
352
+ }
353
+ console.log(c("dim", " " + "─".repeat(40)));
354
+
355
+ if (results.length === 0) {
356
+ console.log(c("yellow", "\n Sonuç bulunamadı.\n"));
357
+ return;
358
+ }
359
+
360
+ const sections = [...new Set(results.map(n => n.cat))];
361
+ sections.forEach(cat => {
362
+ printSectionHeader(CAT_LABEL[cat].trim());
363
+ results.filter(n => n.cat === cat).forEach(n => printNote(n));
364
+ });
365
+
366
+ console.log();
367
+ console.log(c("dim", ` ${results.length} not · açmak için: open <id> · TUI için: open --tui`));
368
+ console.log();
369
+ }
370
+
371
+ // ─────────────────────────────────────────────────────────────────────────────
372
+ // CMD: open (varsayılan editör)
373
+ // ─────────────────────────────────────────────────────────────────────────────
374
+ function cmdOpenId(idArg) {
375
+ const id = parseInt(idArg, 10);
376
+ const note = NOTES.find(n => n.id === id);
377
+ if (!note) {
378
+ console.error(c("red", ` Not bulunamadı: id=${idArg}`));
379
+ process.exit(1);
380
+ }
381
+ const abs = path.join(ROOT, note.file);
382
+ console.log(c("dim", ` Açılıyor: ${note.title}`));
383
+ openWithOS(abs);
384
+ console.log(c("green", ` ✓ ${note.file}`));
385
+ }
386
+
387
+ // ─────────────────────────────────────────────────────────────────────────────
388
+ // HTTP server (open --editor ve open --browser için ortak)
389
+ // ─────────────────────────────────────────────────────────────────────────────
390
+ const http = require("http");
391
+ const url = require("url");
392
+
393
+ /** Dosya uzantısına göre Content-Type döner */
394
+ function mimeType(filePath) {
395
+ const ext = path.extname(filePath).toLowerCase();
396
+ return { ".html":"text/html;charset=utf-8", ".md":"text/plain;charset=utf-8",
397
+ ".css":"text/css", ".js":"application/javascript",
398
+ ".png":"image/png", ".jpg":"image/jpeg", ".svg":"image/svg+xml" }[ext]
399
+ || "application/octet-stream";
400
+ }
401
+
402
+ /**
403
+ * ROOT dizinini serve eden minimal HTTP server başlatır.
404
+ * startPath: tarayıcıda açılacak ilk URL path'i (örn. "/library/index.html")
405
+ * Sunucu Ctrl+C ile kapatılana kadar çalışır.
406
+ */
407
+ function startServer(startPath, port, options = {}) {
408
+ port = port || 7700;
409
+ const shouldOpenBrowser = options.openBrowser !== false;
410
+ options.tries = (options.tries || 0) + 1;
411
+ if (options.tries > 20 || port > 65535) {
412
+ console.error(c("red", " Boş port bulunamadı (20 deneme aşıldı)."));
413
+ process.exit(1);
414
+ }
415
+
416
+ const server = http.createServer((req, res) => {
417
+ // Sadece GET
418
+ if (req.method !== "GET") { res.writeHead(405); res.end(); return; }
419
+
420
+ let reqPath = url.parse(req.url).pathname;
421
+ if (!reqPath) { res.writeHead(400); res.end("bad request"); return; }
422
+ // Kök isteği → kitaplık ana sayfası
423
+ if (reqPath === "/") reqPath = "/library/index.html";
424
+
425
+ // Markdown dosyaları için render sayfasına yönlendir
426
+ if (reqPath.endsWith(".md")) {
427
+ // ?raw=1 ile ham içerik, aksi halde render sayfası
428
+ const qs = url.parse(req.url).query || "";
429
+ if (!qs.includes("raw=1")) {
430
+ // Hangi nota karşılık geliyor?
431
+ const relFile = reqPath.replace(/^\//, "");
432
+ const note = NOTES.find(n => n.file === relFile);
433
+ if (note) {
434
+ res.writeHead(302, { Location: `/library/note.html?id=${note.id}` });
435
+ res.end();
436
+ return;
437
+ }
438
+ }
439
+ }
440
+
441
+ // note.html → dinamik render
442
+ if (reqPath === "/library/note.html") {
443
+ const qs = url.parse(req.url).query || "";
444
+ const idStr = (qs.match(/id=(\d+)/) || [])[1];
445
+ const note = NOTES.find(n => n.id === parseInt(idStr, 10));
446
+ if (!note) { res.writeHead(404); res.end("Not bulunamadı"); return; }
447
+ const mdPath = path.join(ROOT, note.file);
448
+ if (!fs.existsSync(mdPath)) { res.writeHead(404); res.end("Dosya yok"); return; }
449
+ const mdContent = fs.readFileSync(mdPath, "utf8");
450
+ const html = buildNoteHtml(note, mdContent);
451
+ res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
452
+ res.end(html);
453
+ return;
454
+ }
455
+
456
+ // Statik dosya — path traversal'a karşı ROOT prefix kontrolü
457
+ const absPath = path.resolve(ROOT, "." + reqPath);
458
+ const rootPrefix = ROOT.endsWith(path.sep) ? ROOT : ROOT + path.sep;
459
+ if (absPath !== ROOT && !absPath.startsWith(rootPrefix)) {
460
+ res.writeHead(403);
461
+ res.end("403 — forbidden");
462
+ return;
463
+ }
464
+ if (!fs.existsSync(absPath) || fs.statSync(absPath).isDirectory()) {
465
+ res.writeHead(404);
466
+ res.end(`404 — ${reqPath}`);
467
+ return;
468
+ }
469
+ res.writeHead(200, { "Content-Type": mimeType(absPath) });
470
+ res.end(fs.readFileSync(absPath));
471
+ });
472
+
473
+ server.listen(port, "127.0.0.1", () => {
474
+ const targetUrl = `http://localhost:${port}${startPath}`;
475
+ console.log(c("green", ` ✓ Server başladı → ${targetUrl}`));
476
+ console.log(c("dim", ` Durdurmak için Ctrl+C`));
477
+ if (shouldOpenBrowser) openUrl(targetUrl);
478
+ });
479
+
480
+ server.on("error", (err) => {
481
+ if (err.code === "EADDRINUSE") {
482
+ console.log(c("yellow", ` Port ${port} meşgul, ${port + 1} deneniyor…`));
483
+ startServer(startPath, port + 1, options);
484
+ } else {
485
+ console.error(c("red", ` Server hatası: ${err.message}`));
486
+ process.exit(1);
487
+ }
488
+ });
489
+ }
490
+
491
+ function startServerDetached(startPath) {
492
+ const child = spawn(process.execPath, [__filename, "serve", "--path", startPath], {
493
+ cwd: ROOT,
494
+ detached: true,
495
+ stdio: "ignore",
496
+ windowsHide: true,
497
+ });
498
+ child.unref();
499
+ }
500
+
501
+ // ─────────────────────────────────────────────────────────────────────────────
502
+ // CMD: open --editor (kitaplık index.html'i local server üzerinden aç)
503
+ // ─────────────────────────────────────────────────────────────────────────────
504
+ function cmdOpenEditor() {
505
+ const htmlPath = path.join(__dirname, "index.html");
506
+ if (!fs.existsSync(htmlPath)) {
507
+ console.error(c("red", " library/index.html bulunamadı."));
508
+ process.exit(1);
509
+ }
510
+ startServerDetached("/library/index.html");
511
+ console.log(c("green", " ✓ Web UI bağımsız başlatıldı."));
512
+ console.log(c("dim", " Terminali kullanmaya devam edebilirsin."));
513
+ }
514
+
515
+ // ─────────────────────────────────────────────────────────────────────────────
516
+ // CMD: open --browser <id> (tek notu geçici HTML olarak render et)
517
+ // ─────────────────────────────────────────────────────────────────────────────
518
+ function mdToHtml(md) {
519
+ // \r\n → \n normalize et (Windows dosyaları için kritik)
520
+ md = md.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
521
+
522
+ // ── 1. Fenced kod bloklarını önce çıkar, placeholder koy ──────────────────
523
+ // Sonraki adımlarda içleri bozulmasın diye
524
+ const codeBlocks = [];
525
+ md = md.replace(/^```(\w*)\n([\s\S]*?)^```/gm, (_, lang, code) => {
526
+ const escaped = code
527
+ .replace(/&/g, "&amp;")
528
+ .replace(/</g, "&lt;")
529
+ .replace(/>/g, "&gt;");
530
+ const langClass = lang ? ` class="language-${lang}"` : ' class="language-plaintext"';
531
+ codeBlocks.push(`<pre><code${langClass}>${escaped}</code></pre>`);
532
+ return `\x00CODE${codeBlocks.length - 1}\x00`;
533
+ });
534
+
535
+ // ── 2. Satır bazlı dönüşümler ─────────────────────────────────────────────
536
+ const lines = md.split("\n");
537
+ const out = [];
538
+ let inUl = false;
539
+ let inOl = false;
540
+
541
+ for (let i = 0; i < lines.length; i++) {
542
+ let line = lines[i];
543
+
544
+ // Placeholder — dokunma
545
+ if (/^\x00CODE\d+\x00$/.test(line.trim())) {
546
+ if (inUl) { out.push("</ul>"); inUl = false; }
547
+ if (inOl) { out.push("</ol>"); inOl = false; }
548
+ out.push(line.trim());
549
+ continue;
550
+ }
551
+
552
+ // Yatay çizgi
553
+ if (/^---+$/.test(line.trim())) {
554
+ if (inUl) { out.push("</ul>"); inUl = false; }
555
+ if (inOl) { out.push("</ol>"); inOl = false; }
556
+ out.push("<hr>");
557
+ continue;
558
+ }
559
+
560
+ // Başlıklar
561
+ const hMatch = line.match(/^(#{1,6})\s+(.+)$/);
562
+ if (hMatch) {
563
+ if (inUl) { out.push("</ul>"); inUl = false; }
564
+ if (inOl) { out.push("</ol>"); inOl = false; }
565
+ const lvl = hMatch[1].length;
566
+ out.push(`<h${lvl}>${inlineFormat(hMatch[2])}</h${lvl}>`);
567
+ continue;
568
+ }
569
+
570
+ // Sırasız liste
571
+ const ulMatch = line.match(/^[\*\-]\s+(.+)$/);
572
+ if (ulMatch) {
573
+ if (inOl) { out.push("</ol>"); inOl = false; }
574
+ if (!inUl) { out.push("<ul>"); inUl = true; }
575
+ out.push(`<li>${inlineFormat(ulMatch[1])}</li>`);
576
+ continue;
577
+ }
578
+
579
+ // Numaralı liste
580
+ const olMatch = line.match(/^\d+\.\s+(.+)$/);
581
+ if (olMatch) {
582
+ if (inUl) { out.push("</ul>"); inUl = false; }
583
+ if (!inOl) { out.push("<ol>"); inOl = true; }
584
+ out.push(`<li>${inlineFormat(olMatch[1])}</li>`);
585
+ continue;
586
+ }
587
+
588
+ // Blockquote
589
+ const bqMatch = line.match(/^>\s*(.*)$/);
590
+ if (bqMatch) {
591
+ if (inUl) { out.push("</ul>"); inUl = false; }
592
+ if (inOl) { out.push("</ol>"); inOl = false; }
593
+ out.push(`<blockquote>${inlineFormat(bqMatch[1])}</blockquote>`);
594
+ continue;
595
+ }
596
+
597
+ // Liste kapama
598
+ if (inUl) { out.push("</ul>"); inUl = false; }
599
+ if (inOl) { out.push("</ol>"); inOl = false; }
600
+
601
+ // Boş satır
602
+ if (line.trim() === "") {
603
+ out.push("");
604
+ continue;
605
+ }
606
+
607
+ // Normal paragraf satırı
608
+ out.push(`<p>${inlineFormat(line)}</p>`);
609
+ }
610
+
611
+ if (inUl) out.push("</ul>");
612
+ if (inOl) out.push("</ol>");
613
+
614
+ // ── 3. Placeholder'ları geri koy ──────────────────────────────────────────
615
+ let html = out.join("\n");
616
+ html = html.replace(/\x00CODE(\d+)\x00/g, (_, i) => codeBlocks[parseInt(i)]);
617
+
618
+ return html;
619
+ }
620
+
621
+ /** Satır içi Markdown formatlaması (bold, italic, inline code, link) */
622
+ function inlineFormat(text) {
623
+ // 1. Inline kodu çıkar — içerik escape edilip <code> ile sarmalanır, token bırakılır
624
+ const tokens = [];
625
+ let out = text.replace(/`([^`]+)`/g, (_, code) => {
626
+ const esc = code.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
627
+ tokens.push(`<code>${esc}</code>`);
628
+ return `\x00T${tokens.length - 1}\x00`;
629
+ });
630
+
631
+ // 2. Geri kalan metni HTML escape et (XSS surface kapatılır)
632
+ out = out.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
633
+
634
+ // 3. Markdown markup uygula — link href'i şema beyaz listesinden geçiriyoruz
635
+ out = out
636
+ .replace(/\*\*\*(.+?)\*\*\*/g, "<strong><em>$1</em></strong>")
637
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
638
+ .replace(/\*(.+?)\*/g, "<em>$1</em>")
639
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => {
640
+ const safe = /^(https?:|\/|#|mailto:)/i.test(href) ? href : "#";
641
+ return `<a href="${safe}" target="_blank" rel="noopener noreferrer">${label}</a>`;
642
+ });
643
+
644
+ // 4. Token'ları geri yerleştir
645
+ return out.replace(/\x00T(\d+)\x00/g, (_, i) => tokens[Number(i)] || "");
646
+ }
647
+
648
+ function buildNoteHtml(note, mdContent) {
649
+ const body = mdToHtml(mdContent);
650
+ const catColor = { java:"#f89820", js:"#c9b800", py:"#6baed6", sql:"#74b0d4", mongo:"#6fcf60" };
651
+ const tagColor = catColor[note.cat] || "#aaa";
652
+
653
+ return `<!DOCTYPE html>
654
+ <html lang="tr">
655
+ <head>
656
+ <meta charset="UTF-8">
657
+ <meta name="viewport" content="width=device-width,initial-scale=1">
658
+ <title>${note.title} — penote</title>
659
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230f1117'/%3E%3Crect x='6' y='8' width='14' height='2' rx='1' fill='%236c8ef5'/%3E%3Crect x='6' y='13' width='20' height='2' rx='1' fill='%234a5568'/%3E%3Crect x='6' y='18' width='16' height='2' rx='1' fill='%234a5568'/%3E%3Ccircle cx='26' cy='24' r='5' fill='%236c8ef5'/%3E%3Ccircle cx='26' cy='24' r='2.5' fill='%230f1117'/%3E%3C/svg%3E" />
660
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
661
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
662
+ <style>
663
+ :root{--bg:#0f1117;--surface:#1a1d27;--border:#2e3250;--text:#e2e8f0;--muted:#8892b0;--code:#161b22;--accent:#6c8ef5;}
664
+ *{box-sizing:border-box;margin:0;padding:0}
665
+ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);padding:2rem 1rem;line-height:1.75}
666
+ .wrap{max-width:860px;margin:0 auto}
667
+ nav{display:flex;align-items:center;gap:.75rem;margin-bottom:2rem;padding-bottom:1rem;border-bottom:1px solid var(--border)}
668
+ nav a{color:var(--accent);text-decoration:none;font-size:.85rem}
669
+ nav a:hover{text-decoration:underline}
670
+ .tag{font-size:.7rem;font-weight:700;padding:.2rem .6rem;border-radius:4px;text-transform:uppercase;background:${tagColor}22;color:${tagColor};letter-spacing:.5px}
671
+ h1{font-size:1.7rem;margin-bottom:.3rem;line-height:1.2}
672
+ h2{font-size:1.2rem;margin:2.25rem 0 .6rem;color:var(--accent);padding-bottom:.3rem;border-bottom:1px solid var(--border)}
673
+ h3{font-size:1.05rem;margin:1.75rem 0 .4rem;color:#a5b4fc}
674
+ h4,h5,h6{margin:1.25rem 0 .3rem;color:var(--muted)}
675
+ p{margin:.6rem 0;color:var(--text)}
676
+ li{color:var(--text);margin:.3rem 0}
677
+ ul,ol{padding-left:1.6rem;margin:.5rem 0}
678
+ a{color:var(--accent)}
679
+ hr{border:none;border-top:1px solid var(--border);margin:1.75rem 0}
680
+ pre{background:var(--code)!important;border:1px solid var(--border);border-radius:10px;padding:1.1rem 1.4rem;overflow-x:auto;margin:1.1rem 0;font-size:.84rem;line-height:1.6}
681
+ pre code{background:transparent!important;padding:0!important;font-family:'Cascadia Code','Fira Code','Consolas',monospace;font-size:.84rem}
682
+ :not(pre)>code{background:var(--code);color:#e6edf3;padding:.18rem .45rem;border-radius:5px;font-size:.82rem;font-family:'Cascadia Code','Fira Code',monospace;border:1px solid var(--border)}
683
+ strong{color:#fff;font-weight:600}
684
+ em{color:#c9d1d9}
685
+ .meta{font-size:.8rem;color:var(--muted);margin-top:.3rem}
686
+ table{width:100%;border-collapse:collapse;margin:1rem 0;font-size:.88rem}
687
+ th{background:var(--surface);color:var(--accent);padding:.5rem .75rem;text-align:left;border:1px solid var(--border)}
688
+ td{padding:.45rem .75rem;border:1px solid var(--border);color:var(--text)}
689
+ tr:nth-child(even) td{background:#ffffff08}
690
+ blockquote{border-left:3px solid var(--accent);padding:.5rem 1rem;margin:1rem 0;color:var(--muted);background:var(--surface);border-radius:0 6px 6px 0}
691
+ </style>
692
+ </head>
693
+ <body>
694
+ <div class="wrap">
695
+ <nav>
696
+ <a href="/library/index.html">← Kitaplık</a>
697
+ <span class="tag">${note.cat}</span>
698
+ </nav>
699
+ <h1>${note.title}</h1>
700
+ <p class="meta">${note.desc} &nbsp;·&nbsp; <code>${note.file}</code></p>
701
+ <hr>
702
+ ${body}
703
+ </div>
704
+ <script>
705
+ document.addEventListener('DOMContentLoaded', () => {
706
+ document.querySelectorAll('pre code').forEach(el => hljs.highlightElement(el));
707
+ });
708
+ </script>
709
+ </body>
710
+ </html>`;
711
+ }
712
+
713
+ function cmdOpenBrowser(idArg) {
714
+ const id = parseInt(idArg, 10);
715
+ const note = NOTES.find(n => n.id === id);
716
+ if (!note) {
717
+ console.error(c("red", ` Not bulunamadı: id=${idArg}`));
718
+ process.exit(1);
719
+ }
720
+
721
+ const mdPath = path.join(ROOT, note.file);
722
+ if (!fs.existsSync(mdPath)) {
723
+ console.error(c("red", ` Dosya bulunamadı: ${mdPath}`));
724
+ process.exit(1);
725
+ }
726
+
727
+ startServerDetached(`/library/note.html?id=${note.id}`);
728
+ console.log(c("green", ` ✓ Tarayıcı bağımsız başlatıldı: ${note.title}`));
729
+ console.log(c("dim", " Terminali kullanmaya devam edebilirsin."));
730
+ }
731
+
732
+ function cmdServe(flags) {
733
+ const startPath = flags.path || "/library/index.html";
734
+ const port = flags.port ? parseInt(flags.port, 10) : 7700;
735
+ startServer(String(startPath), Number.isFinite(port) ? port : 7700);
736
+ }
737
+
738
+ // ─────────────────────────────────────────────────────────────────────────────
739
+ // CMD: open --tui — 4 ekranlı interaktif TUI
740
+ // Ekran 1: Splash + dil seçimi
741
+ // Ekran 2: Seçilen dilin not listesi
742
+ // Ekran 3: Seçilen not için açma modu
743
+ // Ekran 4: Markdown önizleme
744
+ // ─────────────────────────────────────────────────────────────────────────────
745
+ function cmdTUI() {
746
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
747
+ console.error(c("red", " TUI için interaktif terminal gerekli."));
748
+ process.exit(1);
749
+ }
750
+
751
+ // ── Sabitler ──────────────────────────────────────────────────────────────
752
+ // Variation Selector-16 (️) bazı emoji'leri text→emoji presentation'a zorlar
753
+ // Böylece tüm icon'lar 2-cell genişlikte render olur, hizalama bozulmaz
754
+ const CATS = [
755
+ { key: "java", label: "Java", icon: "☕️", color: "yellow", desc: "Spring Boot · JPA · Lombok" },
756
+ { key: "js", label: "JavaScript", icon: "⚡️", color: "cyan", desc: "Async · Closure · Regex · Array" },
757
+ { key: "py", label: "Python", icon: "🐍", color: "blue", desc: "Temel · İleri · Veritabanı" },
758
+ { key: "sql", label: "SQL", icon: "🗄️", color: "magenta", desc: "Temel · İleri · psql Terminal" },
759
+ { key: "mongo", label: "MongoDB", icon: "🍃", color: "green", desc: "CRUD · Sorgular · Operatörler" },
760
+ ];
761
+
762
+ const ASCII = [
763
+ " ██████╗ ███████╗██╗ ██╗ ███╗ ██╗ ██████╗ ████████╗███████╗███████╗",
764
+ " ██╔══██╗██╔════╝██║ ██║ ████╗ ██║██╔═══██╗╚══██╔══╝██╔════╝██╔════╝",
765
+ " ██║ ██║█████╗ ██║ ██║ ██╔██╗ ██║██║ ██║ ██║ █████╗ ███████╗",
766
+ " ██║ ██║██╔══╝ ╚██╗ ██╔╝ ██║╚██╗██║██║ ██║ ██║ ██╔══╝ ╚════██║",
767
+ " ██████╔╝███████╗ ╚████╔╝ ██║ ╚████║╚██████╔╝ ██║ ███████╗███████║",
768
+ " ╚═════╝ ╚══════╝ ╚═══╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚══════╝╚══════╝",
769
+ ];
770
+
771
+ // ── State ─────────────────────────────────────────────────────────────────
772
+ // screen: "lang" | "notes" | "detail" | "preview"
773
+ let screen = "lang";
774
+ let langCursor = 0;
775
+ let noteCursor = 0;
776
+ let actionCursor = 0;
777
+ let activeCat = null; // seçilen kategori key'i
778
+ let activeNote = null; // seçilen note objesi
779
+ let previewLines = [];
780
+ let previewScroll = 0;
781
+ let statusMsg = "";
782
+
783
+ const W = () => process.stdout.columns || 80;
784
+ const H = () => process.stdout.rows || 30;
785
+ let cursorHidden = false;
786
+ let wrapDisabled = false;
787
+
788
+ // ── readline / raw mode ───────────────────────────────────────────────────
789
+ const rl = readline.createInterface({ input: process.stdin });
790
+ readline.emitKeypressEvents(process.stdin);
791
+ if (process.stdin.setRawMode) process.stdin.setRawMode(true);
792
+
793
+ // Alt-screen buffer — frame'ler ana terminal scrollback'i kirletmeden çizilir
794
+ process.stdout.write("\x1b[?1049h");
795
+
796
+ let cleanedUp = false;
797
+
798
+ function hideCursor() {
799
+ if (!cursorHidden) {
800
+ process.stdout.write("\x1b[?25l");
801
+ cursorHidden = true;
802
+ }
803
+ }
804
+
805
+ function showCursor() {
806
+ if (wrapDisabled) {
807
+ process.stdout.write("\x1b[?7h");
808
+ wrapDisabled = false;
809
+ }
810
+ if (cursorHidden) {
811
+ process.stdout.write("\x1b[?25h");
812
+ cursorHidden = false;
813
+ }
814
+ }
815
+
816
+ function disableWrap() {
817
+ if (!wrapDisabled) {
818
+ process.stdout.write("\x1b[?7l");
819
+ wrapDisabled = true;
820
+ }
821
+ }
822
+
823
+ function cleanup(msg) {
824
+ if (cleanedUp) return;
825
+ cleanedUp = true;
826
+ if (process.stdin.setRawMode) process.stdin.setRawMode(false);
827
+ showCursor();
828
+ rl.close();
829
+ process.stdin.removeAllListeners("keypress");
830
+ process.stdout.removeAllListeners("resize");
831
+ // Alt-screen'den çık — orijinal terminal buffer'ı geri gelir
832
+ process.stdout.write("\x1b[?1049l");
833
+ if (msg) process.stdout.write(msg + "\n\n");
834
+ }
835
+
836
+ // ── Ortak header ──────────────────────────────────────────────────────────
837
+ function drawHeader() {
838
+ hideCursor();
839
+ // Home + full clear — önceki ekranın artık satırlarını temizler
840
+ process.stdout.write("\x1b[H\x1b[2J");
841
+ const w = W();
842
+ // ASCII art — sığmıyorsa kısa başlık
843
+ if (w >= 76) {
844
+ ASCII.forEach((line, i) => {
845
+ const colored = i < 2 ? cb("cyan", line) : i < 4 ? c("blue", line) : c("magenta", line);
846
+ process.stdout.write(colored + "\n");
847
+ });
848
+ } else {
849
+ const short = " DEV-NOTES";
850
+ process.stdout.write("\n" + cb("cyan", short) + "\n");
851
+ }
852
+ process.stdout.write(c("dim", " kişisel programlama notları kitaplığı") + "\n");
853
+ process.stdout.write(c("dim", " " + "─".repeat(Math.min(w - 2, 72))) + "\n");
854
+ }
855
+
856
+ function finishDraw() {
857
+ process.stdout.write("\x1b[J");
858
+ }
859
+
860
+ // ── EKRAN 1: Dil seçimi ───────────────────────────────────────────────────
861
+ function drawLang() {
862
+ drawHeader();
863
+ const w = W();
864
+ process.stdout.write("\n");
865
+ process.stdout.write(cb("white", " Bir dil / teknoloji seçin:\n"));
866
+ process.stdout.write(c("dim", " ↑↓ gezin Enter seç q çık\n\n"));
867
+
868
+ CATS.forEach((cat, i) => {
869
+ const selected = i === langCursor;
870
+ const col = cat.color;
871
+ const icon = cat.icon;
872
+ const count = NOTES.filter(n => n.cat === cat.key).length;
873
+
874
+ if (selected) {
875
+ // Seçili satır — vurgulu kutu
876
+ const label = ` ${icon} ${cat.label.padEnd(12)}`;
877
+ const meta = `${count} not · ${cat.desc}`;
878
+ const line = ` ▶ ${label} ${meta}`;
879
+ process.stdout.write(
880
+ "\x1b[7m" + cb(col, ` ▶ ${icon} `) +
881
+ cb("white", cat.label.padEnd(12)) +
882
+ c("dim", ` ${count} not · ${cat.desc}`) +
883
+ " \x1b[27m\n"
884
+ );
885
+ } else {
886
+ process.stdout.write(
887
+ c("dim", " ") +
888
+ c(col, `${icon} `) +
889
+ cb("white", cat.label.padEnd(12)) +
890
+ c("dim", ` ${count} not · ${cat.desc}`) +
891
+ "\n"
892
+ );
893
+ }
894
+ });
895
+
896
+ process.stdout.write("\n");
897
+ process.stdout.write(c("dim", " " + "─".repeat(Math.min(w - 2, 72))) + "\n");
898
+ process.stdout.write(c("dim", " open --editor → web UI · open --browser <id> → tarayıcı render\n"));
899
+ finishDraw();
900
+ }
901
+
902
+ // ── EKRAN 2: Not listesi ──────────────────────────────────────────────────
903
+ function drawNotes() {
904
+ drawHeader();
905
+ const cat = CATS.find(c => c.key === activeCat);
906
+ const notes = NOTES.filter(n => n.cat === activeCat);
907
+ const w = W();
908
+
909
+ process.stdout.write("\n");
910
+ process.stdout.write(
911
+ c(cat.color, ` ${cat.icon} `) +
912
+ cb("white", cat.label) +
913
+ c("dim", ` — ${notes.length} not`) +
914
+ "\n"
915
+ );
916
+ process.stdout.write(c("dim", " ↑↓ gezin Enter detay Backspace geri q çık\n\n"));
917
+
918
+ notes.forEach((note, i) => {
919
+ const selected = i === noteCursor;
920
+ const idStr = String(note.id).padStart(2, "0");
921
+
922
+ if (selected) {
923
+ process.stdout.write(
924
+ "\x1b[7m" +
925
+ c("dim", ` ▶ [${idStr}] `) +
926
+ cb("white", note.title.padEnd(32)) +
927
+ c("dim", note.desc) +
928
+ " \x1b[27m\n"
929
+ );
930
+ } else {
931
+ process.stdout.write(
932
+ c("dim", ` [${idStr}] `) +
933
+ cb("white", note.title.padEnd(32)) +
934
+ c("dim", note.desc) +
935
+ "\n"
936
+ );
937
+ }
938
+ });
939
+
940
+ process.stdout.write("\n");
941
+ process.stdout.write(c("dim", " " + "─".repeat(Math.min(w - 2, 72))) + "\n");
942
+ finishDraw();
943
+ }
944
+
945
+ // ── EKRAN 3: Not detayı ───────────────────────────────────────────────────
946
+ function drawDetail() {
947
+ drawHeader();
948
+ const note = activeNote;
949
+ const cat = CATS.find(c => c.key === note.cat);
950
+ const w = W();
951
+ const actions = [
952
+ { key: "p", label: "Markdown önizle", desc: "Terminal içinde renkli Markdown okuyucu" },
953
+ { key: "e", label: "Editörde aç", desc: "Varsayılan editörde bağımsız aç" },
954
+ { key: "b", label: "Tarayıcıda aç", desc: "Yerel web okuyucuda bağımsız aç" },
955
+ ];
956
+
957
+ process.stdout.write("\n");
958
+ process.stdout.write(cb("white", " Açma modu seçin") + c("dim", " — ↑↓ gezin Enter seç Backspace notlara dön q çık\n\n"));
959
+
960
+ const titleLine = ` ${cat.icon} ${note.title}`;
961
+ process.stdout.write(cb(cat.color, titleLine) + "\n");
962
+ process.stdout.write(c("dim", ` ${note.desc}`) + "\n");
963
+ process.stdout.write(c("dim", ` ${note.file}`) + "\n");
964
+ process.stdout.write(c("dim", " " + "─".repeat(Math.min(w - 2, 72))) + "\n\n");
965
+
966
+ actions.forEach((action, i) => {
967
+ const selected = i === actionCursor;
968
+ const line = ` ${selected ? "▶" : " "} [${action.key}] ${action.label.padEnd(18)} ${action.desc}`;
969
+ process.stdout.write(selected ? "\x1b[7m" + cb("white", line) + " \x1b[27m\n" : c("dim", line) + "\n");
970
+ });
971
+
972
+ const absPath = path.join(ROOT, note.file);
973
+ const exists = fs.existsSync(absPath);
974
+ process.stdout.write("\n");
975
+ process.stdout.write(c("dim", " " + "─".repeat(Math.min(w - 2, 72))) + "\n");
976
+ if (exists) {
977
+ const stat = fs.statSync(absPath);
978
+ const sizeKb = (stat.size / 1024).toFixed(1);
979
+ const mtime = stat.mtime.toLocaleDateString("tr-TR");
980
+ process.stdout.write(c("dim", ` ID ${note.id} · ${cat.label.trim()} · ${sizeKb} KB · Güncell. ${mtime}\n`));
981
+ }
982
+
983
+ if (statusMsg) {
984
+ process.stdout.write("\n" + c("green", ` ${statusMsg}`) + "\n");
985
+ statusMsg = "";
986
+ }
987
+ finishDraw();
988
+ }
989
+
990
+ function renderMarkdownPreview(note) {
991
+ const absPath = path.join(ROOT, note.file);
992
+ if (!fs.existsSync(absPath)) {
993
+ return [c("red", `Dosya bulunamadı: ${note.file}`)];
994
+ }
995
+
996
+ const md = fs.readFileSync(absPath, "utf8");
997
+ return renderMarkdownTerminal(md);
998
+ }
999
+
1000
+ function previewControlLine() {
1001
+ return c("dim", " ") + cb("white", "p") + c("dim", " markdown önizle ") +
1002
+ cb("white", "e") + c("dim", " editörde aç ") +
1003
+ cb("white", "b") + c("dim", " tarayıcıda aç ") +
1004
+ cb("white", "Backspace") + c("dim", " geri ") +
1005
+ cb("white", "q") + c("dim", " çık");
1006
+ }
1007
+
1008
+ function openPreview() {
1009
+ previewLines = renderMarkdownPreview(activeNote);
1010
+ previewScroll = 0;
1011
+ screen = "preview";
1012
+ drawPreview(true);
1013
+ }
1014
+
1015
+ function drawPreview(clear = false) {
1016
+ hideCursor();
1017
+ disableWrap();
1018
+ // Full clear her zaman — kısa satır → uzun satır geçişinde leftover olmaz
1019
+ process.stdout.write("\x1b[H\x1b[2J");
1020
+ const note = activeNote;
1021
+ const w = W();
1022
+ const h = H();
1023
+ const visibleRows = Math.max(4, h - 7);
1024
+ const maxScroll = Math.max(0, previewLines.length - visibleRows);
1025
+ previewScroll = Math.max(0, Math.min(previewScroll, maxScroll));
1026
+ const clean = (text = "") => {
1027
+ const plain = String(text).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
1028
+ const pad = Math.max(0, w - plain.length);
1029
+ return text + " ".repeat(pad) + "\n";
1030
+ };
1031
+
1032
+ process.stdout.write(clean(cb("white", ` ${note.title}`) + c("dim", " — markdown preview")));
1033
+ process.stdout.write(clean(c("dim", " " + "─".repeat(Math.min(w - 2, 72)))));
1034
+ process.stdout.write(clean(""));
1035
+
1036
+ const page = previewLines.slice(previewScroll, previewScroll + visibleRows);
1037
+ for (let i = 0; i < visibleRows; i++) {
1038
+ process.stdout.write(clean(" " + (page[i] || "")));
1039
+ }
1040
+
1041
+ process.stdout.write(clean(""));
1042
+ process.stdout.write(clean(c("dim", " " + "─".repeat(Math.min(w - 2, 72)))));
1043
+ process.stdout.write(clean(previewControlLine()));
1044
+ process.stdout.write(clean(c("dim", ` ↑↓ kaydır · ${previewScroll + 1}-${Math.min(previewScroll + visibleRows, previewLines.length)} / ${previewLines.length}`)));
1045
+ }
1046
+
1047
+ // ── İlk çizim ─────────────────────────────────────────────────────────────
1048
+ drawLang();
1049
+
1050
+ // ── Klavye olayları ───────────────────────────────────────────────────────
1051
+ process.stdin.on("keypress", (str, key) => {
1052
+ const isUp = key.name === "up" || (key.ctrl && key.name === "p");
1053
+ const isDown = key.name === "down" || (key.ctrl && key.name === "n");
1054
+ const isEnter = key.name === "return";
1055
+ const isBack = key.name === "backspace" || key.name === "escape";
1056
+ const isQuit = str === "q" || str === "Q" || (key.ctrl && key.name === "c");
1057
+
1058
+ if (isQuit) {
1059
+ cleanup(c("dim", " penote kapatıldı."));
1060
+ process.exit(0);
1061
+ }
1062
+
1063
+ // ── Dil seçim ekranı ──
1064
+ if (screen === "lang") {
1065
+ if (isUp) langCursor = (langCursor - 1 + CATS.length) % CATS.length;
1066
+ if (isDown) langCursor = (langCursor + 1) % CATS.length;
1067
+ if (isEnter) {
1068
+ activeCat = CATS[langCursor].key;
1069
+ noteCursor = 0;
1070
+ screen = "notes";
1071
+ drawNotes();
1072
+ return;
1073
+ }
1074
+ drawLang();
1075
+ return;
1076
+ }
1077
+
1078
+ // ── Not listesi ekranı ──
1079
+ if (screen === "notes") {
1080
+ const notes = NOTES.filter(n => n.cat === activeCat);
1081
+ if (isUp) noteCursor = (noteCursor - 1 + notes.length) % notes.length;
1082
+ if (isDown) noteCursor = (noteCursor + 1) % notes.length;
1083
+ if (isBack) {
1084
+ screen = "lang";
1085
+ drawLang();
1086
+ return;
1087
+ }
1088
+ if (isEnter) {
1089
+ activeNote = notes[noteCursor];
1090
+ actionCursor = 0;
1091
+ screen = "detail";
1092
+ drawDetail();
1093
+ return;
1094
+ }
1095
+ drawNotes();
1096
+ return;
1097
+ }
1098
+
1099
+ // ── Detay ekranı ──
1100
+ if (screen === "detail") {
1101
+ const actions = ["p", "e", "b"];
1102
+ if (isBack) {
1103
+ screen = "notes";
1104
+ drawNotes();
1105
+ return;
1106
+ }
1107
+ if (isUp) {
1108
+ actionCursor = (actionCursor - 1 + actions.length) % actions.length;
1109
+ drawDetail();
1110
+ return;
1111
+ }
1112
+ if (isDown) {
1113
+ actionCursor = (actionCursor + 1) % actions.length;
1114
+ drawDetail();
1115
+ return;
1116
+ }
1117
+ if (isEnter) {
1118
+ str = actions[actionCursor];
1119
+ }
1120
+ if (str === "e" || str === "E") {
1121
+ cmdOpenId(String(activeNote.id));
1122
+ statusMsg = `✓ Editör bağımsız açıldı: ${activeNote.title}`;
1123
+ drawDetail();
1124
+ return;
1125
+ }
1126
+ if (str === "b" || str === "B") {
1127
+ cmdOpenBrowser(String(activeNote.id));
1128
+ statusMsg = `✓ Tarayıcı bağımsız açıldı: ${activeNote.title}`;
1129
+ drawDetail();
1130
+ return;
1131
+ }
1132
+ if (str === "p" || str === "P") {
1133
+ openPreview();
1134
+ return;
1135
+ }
1136
+ }
1137
+
1138
+ // ── Markdown önizleme ekranı ──
1139
+ if (screen === "preview") {
1140
+ if (isBack) {
1141
+ showCursor();
1142
+ screen = "detail";
1143
+ drawDetail();
1144
+ return;
1145
+ }
1146
+ if (str === "e" || str === "E") {
1147
+ cmdOpenId(String(activeNote.id));
1148
+ statusMsg = `✓ Editör bağımsız açıldı: ${activeNote.title}`;
1149
+ showCursor();
1150
+ screen = "detail";
1151
+ drawDetail();
1152
+ return;
1153
+ }
1154
+ if (str === "b" || str === "B") {
1155
+ cmdOpenBrowser(String(activeNote.id));
1156
+ statusMsg = `✓ Tarayıcı bağımsız açıldı: ${activeNote.title}`;
1157
+ showCursor();
1158
+ screen = "detail";
1159
+ drawDetail();
1160
+ return;
1161
+ }
1162
+ if (str === "p" || str === "P") {
1163
+ openPreview();
1164
+ return;
1165
+ }
1166
+ if (isUp) {
1167
+ previewScroll = Math.max(0, previewScroll - 1);
1168
+ drawPreview();
1169
+ return;
1170
+ }
1171
+ if (isDown) {
1172
+ const visibleRows = Math.max(4, H() - 7);
1173
+ previewScroll = Math.min(Math.max(0, previewLines.length - visibleRows), previewScroll + 1);
1174
+ drawPreview();
1175
+ return;
1176
+ }
1177
+ }
1178
+ });
1179
+
1180
+ // Resize handler — terminal boyutu değişirse aktif ekranı tam yeniden çiz
1181
+ process.stdout.on("resize", () => {
1182
+ if (screen === "lang") drawLang();
1183
+ else if (screen === "notes") drawNotes();
1184
+ else if (screen === "detail") drawDetail();
1185
+ else if (screen === "preview") drawPreview(true);
1186
+ });
1187
+
1188
+ // SIGINT (dış kaynaklı Ctrl+C) — cleanup garanti
1189
+ process.on("SIGINT", () => {
1190
+ cleanup(c("dim", " penote kapatıldı."));
1191
+ process.exit(0);
1192
+ });
1193
+
1194
+ // Beklenmedik çıkış — terminali geri yükle (raw mode + alt-screen)
1195
+ process.on("exit", () => {
1196
+ if (cleanedUp) return;
1197
+ if (process.stdin.setRawMode) process.stdin.setRawMode(false);
1198
+ process.stdout.write("\x1b[?25h\x1b[?1049l");
1199
+ });
1200
+ }
1201
+
1202
+ // ─────────────────────────────────────────────────────────────────────────────
1203
+ // Giriş noktası
1204
+ // ─────────────────────────────────────────────────────────────────────────────
1205
+ const rawArgs = process.argv.slice(2);
1206
+ const { flags, pos } = parseArgs(rawArgs);
1207
+ const cmd = (pos[0] || "help").toLowerCase();
1208
+
1209
+ // --help / -h her yerde çalışsın
1210
+ if (flags.help || flags.h) { printHelp(); process.exit(0); }
1211
+
1212
+ switch (cmd) {
1213
+ case "list":
1214
+ cmdList(flags);
1215
+ break;
1216
+
1217
+ case "search":
1218
+ if (!pos[1] && !flags.search) {
1219
+ console.error(c("red", " Kullanım: search <sorgu>"));
1220
+ process.exit(1);
1221
+ }
1222
+ cmdList({ ...flags, search: pos[1] || flags.search });
1223
+ break;
1224
+
1225
+ case "serve":
1226
+ cmdServe(flags);
1227
+ break;
1228
+
1229
+ case "open": {
1230
+ if (flags.tui) { cmdTUI(); break; }
1231
+ if (flags.editor) { cmdOpenEditor(); break; }
1232
+ if (flags.browser) { cmdOpenBrowser(String(flags.browser)); break; }
1233
+ // open <id>
1234
+ const idArg = pos[1];
1235
+ if (!idArg) {
1236
+ console.error(c("red", " Kullanım: open <id> veya open --tui veya open --editor"));
1237
+ process.exit(1);
1238
+ }
1239
+ cmdOpenId(idArg);
1240
+ break;
1241
+ }
1242
+
1243
+ case "help":
1244
+ case "--help":
1245
+ case "-h":
1246
+ printHelp();
1247
+ break;
1248
+
1249
+ default:
1250
+ console.error(c("red", ` Bilinmeyen komut: "${cmd}"`));
1251
+ printHelp();
1252
+ process.exit(1);
1253
+ }