codeblog-app 1.5.0 → 1.5.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "codeblog-app",
4
- "version": "1.5.0",
4
+ "version": "1.5.1",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -56,11 +56,11 @@
56
56
  "typescript": "5.8.2"
57
57
  },
58
58
  "optionalDependencies": {
59
- "codeblog-app-darwin-arm64": "1.5.0",
60
- "codeblog-app-darwin-x64": "1.5.0",
61
- "codeblog-app-linux-arm64": "1.5.0",
62
- "codeblog-app-linux-x64": "1.5.0",
63
- "codeblog-app-windows-x64": "1.5.0"
59
+ "codeblog-app-darwin-arm64": "1.5.1",
60
+ "codeblog-app-darwin-x64": "1.5.1",
61
+ "codeblog-app-linux-arm64": "1.5.1",
62
+ "codeblog-app-linux-x64": "1.5.1",
63
+ "codeblog-app-windows-x64": "1.5.1"
64
64
  },
65
65
  "dependencies": {
66
66
  "@ai-sdk/amazon-bedrock": "^4.0.60",
@@ -67,6 +67,9 @@ export const UpdateCommand: CommandModule = {
67
67
  if (os !== "windows") {
68
68
  await fs.chmod(bin, 0o755)
69
69
  }
70
+ if (os === "darwin") {
71
+ Bun.spawn(["codesign", "--sign", "-", "--force", bin], { stdout: "ignore", stderr: "ignore" })
72
+ }
70
73
 
71
74
  await fs.rm(tmp, { recursive: true, force: true })
72
75
 
package/src/tui/app.tsx CHANGED
@@ -5,6 +5,7 @@ import { ExitProvider, useExit } from "./context/exit"
5
5
  import { ThemeProvider, useTheme } from "./context/theme"
6
6
  import { Home } from "./routes/home"
7
7
  import { Chat } from "./routes/chat"
8
+ import { ThemeSetup } from "./routes/setup"
8
9
 
9
10
  import pkg from "../../package.json"
10
11
  const VERSION = pkg.version
@@ -98,6 +99,9 @@ function App() {
98
99
  return (
99
100
  <box flexDirection="column" width={dimensions().width} height={dimensions().height}>
100
101
  <Switch>
102
+ <Match when={theme.needsSetup}>
103
+ <ThemeSetup />
104
+ </Match>
101
105
  <Match when={route.data.type === "home"}>
102
106
  <Home
103
107
  loggedIn={loggedIn()}
@@ -1,5 +1,8 @@
1
+ import path from "path"
2
+ import fs from "fs"
1
3
  import { createStore } from "solid-js/store"
2
4
  import { createSimpleContext } from "./helper"
5
+ import { Global } from "../../global"
3
6
 
4
7
  export type ThemeColors = {
5
8
  text: string
@@ -223,6 +226,180 @@ const solarized: ThemeDef = {
223
226
  },
224
227
  }
225
228
 
229
+ const catppuccin: ThemeDef = {
230
+ dark: {
231
+ text: "#cdd6f4",
232
+ textMuted: "#6c7086",
233
+ primary: "#89b4fa",
234
+ accent: "#cba6f7",
235
+ success: "#a6e3a1",
236
+ error: "#f38ba8",
237
+ warning: "#f9e2af",
238
+ input: "#cdd6f4",
239
+ cursor: "#6c7086",
240
+ logo1: "#cba6f7",
241
+ logo2: "#89b4fa",
242
+ },
243
+ light: {
244
+ text: "#4c4f69",
245
+ textMuted: "#8c8fa1",
246
+ primary: "#1e66f5",
247
+ accent: "#8839ef",
248
+ success: "#40a02b",
249
+ error: "#d20f39",
250
+ warning: "#df8e1d",
251
+ input: "#4c4f69",
252
+ cursor: "#8c8fa1",
253
+ logo1: "#8839ef",
254
+ logo2: "#1e66f5",
255
+ },
256
+ }
257
+
258
+ const rosepine: ThemeDef = {
259
+ dark: {
260
+ text: "#e0def4",
261
+ textMuted: "#6e6a86",
262
+ primary: "#c4a7e7",
263
+ accent: "#ebbcba",
264
+ success: "#31748f",
265
+ error: "#eb6f92",
266
+ warning: "#f6c177",
267
+ input: "#e0def4",
268
+ cursor: "#6e6a86",
269
+ logo1: "#ebbcba",
270
+ logo2: "#c4a7e7",
271
+ },
272
+ light: {
273
+ text: "#575279",
274
+ textMuted: "#9893a5",
275
+ primary: "#907aa9",
276
+ accent: "#d7827e",
277
+ success: "#286983",
278
+ error: "#b4637a",
279
+ warning: "#ea9d34",
280
+ input: "#575279",
281
+ cursor: "#9893a5",
282
+ logo1: "#d7827e",
283
+ logo2: "#907aa9",
284
+ },
285
+ }
286
+
287
+ const gruvbox: ThemeDef = {
288
+ dark: {
289
+ text: "#ebdbb2",
290
+ textMuted: "#928374",
291
+ primary: "#83a598",
292
+ accent: "#d3869b",
293
+ success: "#b8bb26",
294
+ error: "#fb4934",
295
+ warning: "#fabd2f",
296
+ input: "#ebdbb2",
297
+ cursor: "#928374",
298
+ logo1: "#fe8019",
299
+ logo2: "#83a598",
300
+ },
301
+ light: {
302
+ text: "#3c3836",
303
+ textMuted: "#928374",
304
+ primary: "#076678",
305
+ accent: "#8f3f71",
306
+ success: "#79740e",
307
+ error: "#9d0006",
308
+ warning: "#b57614",
309
+ input: "#3c3836",
310
+ cursor: "#928374",
311
+ logo1: "#af3a03",
312
+ logo2: "#076678",
313
+ },
314
+ }
315
+
316
+ const onedark: ThemeDef = {
317
+ dark: {
318
+ text: "#abb2bf",
319
+ textMuted: "#5c6370",
320
+ primary: "#61afef",
321
+ accent: "#c678dd",
322
+ success: "#98c379",
323
+ error: "#e06c75",
324
+ warning: "#e5c07b",
325
+ input: "#abb2bf",
326
+ cursor: "#5c6370",
327
+ logo1: "#e06c75",
328
+ logo2: "#61afef",
329
+ },
330
+ light: {
331
+ text: "#383a42",
332
+ textMuted: "#a0a1a7",
333
+ primary: "#4078f2",
334
+ accent: "#a626a4",
335
+ success: "#50a14f",
336
+ error: "#e45649",
337
+ warning: "#c18401",
338
+ input: "#383a42",
339
+ cursor: "#a0a1a7",
340
+ logo1: "#e45649",
341
+ logo2: "#4078f2",
342
+ },
343
+ }
344
+
345
+ const kanagawa: ThemeDef = {
346
+ dark: {
347
+ text: "#dcd7ba",
348
+ textMuted: "#727169",
349
+ primary: "#7e9cd8",
350
+ accent: "#957fb8",
351
+ success: "#76946a",
352
+ error: "#c34043",
353
+ warning: "#dca561",
354
+ input: "#dcd7ba",
355
+ cursor: "#727169",
356
+ logo1: "#ff5d62",
357
+ logo2: "#7e9cd8",
358
+ },
359
+ light: {
360
+ text: "#1f1f28",
361
+ textMuted: "#8a8980",
362
+ primary: "#4e8ca2",
363
+ accent: "#624c83",
364
+ success: "#6f894e",
365
+ error: "#c84053",
366
+ warning: "#cc6d00",
367
+ input: "#1f1f28",
368
+ cursor: "#8a8980",
369
+ logo1: "#d7474b",
370
+ logo2: "#4e8ca2",
371
+ },
372
+ }
373
+
374
+ const everforest: ThemeDef = {
375
+ dark: {
376
+ text: "#d3c6aa",
377
+ textMuted: "#859289",
378
+ primary: "#7fbbb3",
379
+ accent: "#d699b6",
380
+ success: "#a7c080",
381
+ error: "#e67e80",
382
+ warning: "#dbbc7f",
383
+ input: "#d3c6aa",
384
+ cursor: "#859289",
385
+ logo1: "#e69875",
386
+ logo2: "#7fbbb3",
387
+ },
388
+ light: {
389
+ text: "#5c6a72",
390
+ textMuted: "#939f91",
391
+ primary: "#3a94c5",
392
+ accent: "#df69ba",
393
+ success: "#8da101",
394
+ error: "#f85552",
395
+ warning: "#dfa000",
396
+ input: "#5c6a72",
397
+ cursor: "#939f91",
398
+ logo1: "#f57d26",
399
+ logo2: "#3a94c5",
400
+ },
401
+ }
402
+
226
403
  export const THEMES: Record<string, ThemeDef> = {
227
404
  codeblog,
228
405
  dracula,
@@ -231,26 +408,40 @@ export const THEMES: Record<string, ThemeDef> = {
231
408
  monokai,
232
409
  github,
233
410
  solarized,
411
+ catppuccin,
412
+ rosepine,
413
+ gruvbox,
414
+ onedark,
415
+ kanagawa,
416
+ everforest,
234
417
  }
235
418
 
236
419
  export const THEME_NAMES = Object.keys(THEMES)
237
420
 
238
- function detect(): "dark" | "light" {
239
- const env = process.env.COLORFGBG
240
- if (env) {
241
- const parts = env.split(";")
242
- const bg = parseInt(parts[parts.length - 1] || "0", 10)
243
- if (bg > 6 && bg !== 8) return "light"
244
- }
245
- return "dark"
421
+ const configPath = path.join(Global.Path.config, "theme.json")
422
+
423
+ type SavedTheme = { name: string; mode: "dark" | "light" }
424
+
425
+ function load(): SavedTheme | null {
426
+ try {
427
+ const data = JSON.parse(fs.readFileSync(configPath, "utf-8"))
428
+ if (data.name && data.mode) return data as SavedTheme
429
+ } catch {}
430
+ return null
431
+ }
432
+
433
+ function save(cfg: SavedTheme) {
434
+ Bun.write(configPath, JSON.stringify(cfg, null, 2)).catch(() => {})
246
435
  }
247
436
 
248
437
  export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
249
438
  name: "Theme",
250
439
  init: () => {
440
+ const saved = load()
251
441
  const [store, setStore] = createStore({
252
- name: "codeblog" as string,
253
- mode: detect() as "dark" | "light",
442
+ name: saved?.name || "codeblog",
443
+ mode: (saved?.mode || "dark") as "dark" | "light",
444
+ needsSetup: !saved,
254
445
  })
255
446
 
256
447
  return {
@@ -260,14 +451,19 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
260
451
  },
261
452
  get name() { return store.name },
262
453
  get mode() { return store.mode },
454
+ get needsSetup() { return store.needsSetup },
263
455
  set(name: string) {
264
- if (THEMES[name]) setStore("name", name)
265
- },
266
- toggle() {
267
- setStore("mode", store.mode === "dark" ? "light" : "dark")
456
+ if (!THEMES[name]) return
457
+ setStore("name", name)
458
+ save({ name, mode: store.mode })
268
459
  },
269
460
  setMode(mode: "dark" | "light") {
270
461
  setStore("mode", mode)
462
+ save({ name: store.name, mode })
463
+ },
464
+ finishSetup() {
465
+ setStore("needsSetup", false)
466
+ save({ name: store.name, mode: store.mode })
271
467
  },
272
468
  }
273
469
  },
@@ -27,7 +27,7 @@ const HELP_TEXT = [
27
27
  " /notifications View notifications",
28
28
  " /dashboard Your stats",
29
29
  " /models List available AI models",
30
- " /theme [name] Switch theme (codeblog, dracula, nord, tokyonight, monokai, github, solarized)",
30
+ " /theme [name] Switch theme (codeblog, dracula, nord, tokyonight, monokai, github, solarized, catppuccin, rosepine, gruvbox, onedark, kanagawa, everforest)",
31
31
  " /dark | /light Toggle dark/light mode",
32
32
  " /help Show this help",
33
33
  " /exit Exit",
@@ -0,0 +1,127 @@
1
+ import { createSignal } from "solid-js"
2
+ import { useKeyboard } from "@opentui/solid"
3
+ import { useTheme, THEME_NAMES, THEMES } from "../context/theme"
4
+
5
+ export function ThemeSetup() {
6
+ const theme = useTheme()
7
+ const modes = ["dark", "light"] as const
8
+ const [step, setStep] = createSignal<"mode" | "theme">("mode")
9
+ const [modeIdx, setModeIdx] = createSignal(0)
10
+ const [themeIdx, setThemeIdx] = createSignal(0)
11
+
12
+ useKeyboard((evt) => {
13
+ if (step() === "mode") {
14
+ if (evt.name === "up" || evt.name === "k") {
15
+ setModeIdx((i) => (i - 1 + modes.length) % modes.length)
16
+ theme.setMode(modes[(modeIdx() - 1 + modes.length) % modes.length])
17
+ evt.preventDefault()
18
+ return
19
+ }
20
+ if (evt.name === "down" || evt.name === "j") {
21
+ setModeIdx((i) => (i + 1) % modes.length)
22
+ theme.setMode(modes[(modeIdx() + 1) % modes.length])
23
+ evt.preventDefault()
24
+ return
25
+ }
26
+ if (evt.name === "return") {
27
+ theme.setMode(modes[modeIdx()])
28
+ setStep("theme")
29
+ evt.preventDefault()
30
+ return
31
+ }
32
+ }
33
+
34
+ if (step() === "theme") {
35
+ if (evt.name === "up" || evt.name === "k") {
36
+ setThemeIdx((i) => (i - 1 + THEME_NAMES.length) % THEME_NAMES.length)
37
+ theme.set(THEME_NAMES[(themeIdx() - 1 + THEME_NAMES.length) % THEME_NAMES.length])
38
+ evt.preventDefault()
39
+ return
40
+ }
41
+ if (evt.name === "down" || evt.name === "j") {
42
+ setThemeIdx((i) => (i + 1) % THEME_NAMES.length)
43
+ theme.set(THEME_NAMES[(themeIdx() + 1) % THEME_NAMES.length])
44
+ evt.preventDefault()
45
+ return
46
+ }
47
+ if (evt.name === "return") {
48
+ theme.finishSetup()
49
+ evt.preventDefault()
50
+ return
51
+ }
52
+ if (evt.name === "escape") {
53
+ setStep("mode")
54
+ evt.preventDefault()
55
+ return
56
+ }
57
+ }
58
+ })
59
+
60
+ return (
61
+ <box flexDirection="column" flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
62
+ <box flexGrow={1} minHeight={0} />
63
+
64
+ <box flexShrink={0} flexDirection="column" alignItems="center">
65
+ <text fg={theme.colors.primary}>
66
+ <span style={{ bold: true }}>Welcome to CodeBlog</span>
67
+ </text>
68
+ <box height={1} />
69
+ <text fg={theme.colors.textMuted}>Let's set up your terminal theme</text>
70
+ <box height={1} />
71
+ </box>
72
+
73
+ {step() === "mode" ? (
74
+ <box flexShrink={0} flexDirection="column" width="100%" maxWidth={50}>
75
+ <text fg={theme.colors.text}>
76
+ <span style={{ bold: true }}>Step 1: Is your terminal background dark or light?</span>
77
+ </text>
78
+ <box height={1} />
79
+ {modes.map((m, i) => (
80
+ <box flexDirection="row" paddingLeft={2}>
81
+ <text fg={modeIdx() === i ? theme.colors.primary : theme.colors.textMuted}>
82
+ {modeIdx() === i ? "❯ " : " "}
83
+ </text>
84
+ <text fg={modeIdx() === i ? theme.colors.text : theme.colors.textMuted}>
85
+ <span style={{ bold: modeIdx() === i }}>
86
+ {m === "dark" ? "🌙 Dark background" : "☀️ Light background"}
87
+ </span>
88
+ </text>
89
+ </box>
90
+ ))}
91
+ <box height={1} />
92
+ <text fg={theme.colors.textMuted}>↑↓ to select · Enter to confirm</text>
93
+ </box>
94
+ ) : (
95
+ <box flexShrink={0} flexDirection="column" width="100%" maxWidth={50}>
96
+ <text fg={theme.colors.text}>
97
+ <span style={{ bold: true }}>Step 2: Choose a color theme</span>
98
+ </text>
99
+ <box height={1} />
100
+ {THEME_NAMES.map((name, i) => {
101
+ const def = THEMES[name]
102
+ const c = def[theme.mode]
103
+ return (
104
+ <box flexDirection="row" paddingLeft={2}>
105
+ <text fg={themeIdx() === i ? c.primary : theme.colors.textMuted}>
106
+ {themeIdx() === i ? "❯ " : " "}
107
+ </text>
108
+ <text fg={themeIdx() === i ? c.text : theme.colors.textMuted}>
109
+ <span style={{ bold: themeIdx() === i }}>{name}</span>
110
+ </text>
111
+ <text fg={c.logo1}>{" ●"}</text>
112
+ <text fg={c.logo2}>{"●"}</text>
113
+ <text fg={c.primary}>{"●"}</text>
114
+ <text fg={c.success}>{"●"}</text>
115
+ <text fg={c.error}>{"●"}</text>
116
+ </box>
117
+ )
118
+ })}
119
+ <box height={1} />
120
+ <text fg={theme.colors.textMuted}>↑↓ to select · Enter to confirm · Esc to go back</text>
121
+ </box>
122
+ )}
123
+
124
+ <box flexGrow={1} minHeight={0} />
125
+ </box>
126
+ )
127
+ }