Skip to content

Commit 33375b3

Browse files
minsoo-webclaude
andauthored
fix: improve CLI UX across stdout/stderr channels, TTY errors, help text, and docs (#29)
- Move all progress/status messages (Fetching, Checking, Converting, auto-detect) from stdout to stderr for proper pipe composability - Unify non-TTY error messages with branded format and how-to-fix guidance via new require_tty() helper in interactive.rs - Add is_tty() guards to create and presets install before interactive prompts - Move presets install TTY check before network fetch to avoid wasteful requests - Add Examples section to --help via clap after_help - Add long_about descriptions to all subcommands (update, apply, create, presets list, presets install) - Warn when --editor/--target flags are used with subcommands that ignore them - Update README.md and README.ko.md: add OpenCode/iTerm2 editors, OpenCode/Obsidian/iTerm2 targets, fix theme store path, update usage example - Bump version to 0.10.1 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fac43a1 commit 33375b3

File tree

10 files changed

+119
-67
lines changed

10 files changed

+119
-67
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "chromaport"
3-
version = "0.10.0"
3+
version = "0.10.1"
44
edition = "2021"
55
description = "Migrate VS Code / Cursor / OpenCode / iTerm2 themes to Superset, Warp, Ghostty, OpenCode, Obsidian, iTerm2, and more"
66
license = "MIT"

README.ko.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,11 @@ CI 환경과 비대화형 셸에서는 자동으로 비활성화됩니다.
7878
```
7979
$ chromaport
8080
> Select editor: Cursor
81-
> Select themes to migrate: One Monokai, Ayu Dark
82-
> Select target app: Superset
81+
> Select theme: One Monokai (TUI 라이브 미리보기)
82+
> Select target app: Ghostty
8383
84-
Converting 2 theme(s)...
85-
✔ One Monokai → ~/.config/chromaport/themes/one-monokai.json
86-
✔ Ayu Dark → ~/.config/chromaport/themes/ayu-dark.json
84+
Converting theme...
85+
✔ One Monokai → ~/chromaport/themes/one-monokai.json
8786
```
8887

8988
### 명령어
@@ -99,8 +98,8 @@ Commands:
9998
10099
Options:
101100
-v, --version 버전 출력
102-
-e, --editor <EDITOR> 소스 에디터 [가능한 값: vscode, cursor]
103-
-t, --target <TARGET> 대상 앱 [가능한 값: superset, warp, ghostty]
101+
-e, --editor <EDITOR> 소스 에디터 [가능한 값: vscode, cursor, opencode, iterm2]
102+
-t, --target <TARGET> 대상 앱 [가능한 값: superset, warp, ghostty, opencode, obsidian, iterm2]
104103
-h, --help 도움말 출력
105104
```
106105

@@ -110,7 +109,7 @@ Options:
110109
# 대화형 모드 — 에디터, 테마, 대상을 단계별로 선택
111110
chromaport
112111

113-
# 비대화형 — 에디터와 대상을 직접 지정
112+
# 에디터/대상 선택 건너뛰기 — 직접 지정
114113
chromaport --editor vscode --target ghostty
115114
```
116115

@@ -155,10 +154,12 @@ chromaport 저장소에서 엄선된 프리셋 테마를 탐색하고 설치할
155154

156155
## 지원 에디터
157156

158-
| 에디터 | 경로 |
159-
| ------- | ----------------------- |
160-
| VS Code | `~/.vscode/extensions/` |
161-
| Cursor | `~/.cursor/extensions/` |
157+
| 에디터 | 경로 |
158+
| -------- | ----------------------------------------------------------- |
159+
| VS Code | `~/.vscode/extensions/` |
160+
| Cursor | `~/.cursor/extensions/` |
161+
| OpenCode | `~/.config/opencode/themes/` |
162+
| iTerm2 | `~/Library/Preferences/com.googlecode.iterm2.plist` |
162163

163164
## 지원 대상
164165

@@ -167,6 +168,9 @@ chromaport 저장소에서 엄선된 프리셋 테마를 탐색하고 설치할
167168
| Superset | `~/.superset/chromaport-themes/`에 기록 — Superset UI에서 가져오기 |
168169
| Warp | `~/.warp/themes/`에 심볼릭 링크 — 실행 중 자동 감지 |
169170
| Ghostty | `~/.config/ghostty/themes/`에 심볼릭 링크 — 설정 파일 또는 리로드로 적용 |
171+
| OpenCode | `~/.config/opencode/themes/`에 심볼릭 링크 — 재시작 시 자동 감지 |
172+
| Obsidian | 볼트의 `.obsidian/themes/`에 복사 — 설정 → 외관에서 활성화 |
173+
| iTerm2 | `~/.config/iterm2/themes/`에 심볼릭 링크 — Color Presets에서 가져오기 |
170174

171175
---
172176

@@ -175,7 +179,7 @@ chromaport 저장소에서 엄선된 프리셋 테마를 탐색하고 설치할
175179
1. 에디터 확장 디렉토리에서 `package.json`의 테마 기여(contribution)를 스캔
176180
2. VS Code 테마 JSON 파싱 (JSONC 주석 제거 및 `include` 상속 처리)
177181
3. 중간 표현(IR)으로 변환
178-
4. 중앙 테마 저장소(`~/.config/chromaport/themes/`)에 저장
182+
4. 중앙 테마 저장소(`~/chromaport/themes/`)에 저장
179183
5. 선택한 대상 형식으로 심볼릭 링크 또는 기록
180184

181185
---

README.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,11 @@ Run `chromaport` and follow the interactive prompts:
7070
```
7171
$ chromaport
7272
> Select editor: Cursor
73-
> Select themes to migrate: One Monokai, Ayu Dark
74-
> Select target app: Superset
73+
> Select theme: One Monokai (with live TUI preview)
74+
> Select target app: Ghostty
7575
76-
Converting 2 theme(s)...
77-
✔ One Monokai → ~/.config/chromaport/themes/one-monokai.json
78-
✔ Ayu Dark → ~/.config/chromaport/themes/ayu-dark.json
76+
Converting theme...
77+
✔ One Monokai → ~/chromaport/themes/one-monokai.json
7978
```
8079

8180
### Commands
@@ -91,8 +90,8 @@ Commands:
9190
9291
Options:
9392
-v, --version Print version
94-
-e, --editor <EDITOR> Source editor [possible values: vscode, cursor]
95-
-t, --target <TARGET> Target app [possible values: superset, warp, ghostty]
93+
-e, --editor <EDITOR> Source editor [possible values: vscode, cursor, opencode, iterm2]
94+
-t, --target <TARGET> Target app [possible values: superset, warp, ghostty, opencode, obsidian, iterm2]
9695
-h, --help Print help
9796
```
9897

@@ -102,7 +101,7 @@ Options:
102101
# Interactive mode — select editor, themes, and target step by step
103102
chromaport
104103

105-
# Non-interactive — specify editor and target directly
104+
# Skip editor/target selection — specify directly
106105
chromaport --editor vscode --target ghostty
107106
```
108107

@@ -145,10 +144,12 @@ Browse and install curated preset themes from the chromaport repository.
145144

146145
## Supported editors
147146

148-
| Editor | Path |
149-
| ------- | ----------------------- |
150-
| VS Code | `~/.vscode/extensions/` |
151-
| Cursor | `~/.cursor/extensions/` |
147+
| Editor | Path |
148+
| -------- | ----------------------------------------------------------- |
149+
| VS Code | `~/.vscode/extensions/` |
150+
| Cursor | `~/.cursor/extensions/` |
151+
| OpenCode | `~/.config/opencode/themes/` |
152+
| iTerm2 | `~/Library/Preferences/com.googlecode.iterm2.plist` |
152153

153154
## Supported targets
154155

@@ -157,13 +158,16 @@ Browse and install curated preset themes from the chromaport repository.
157158
| Superset | Writes to `~/.superset/chromaport-themes/` — import via Superset UI |
158159
| Warp | Symlinks to `~/.warp/themes/` — auto-detected while running |
159160
| Ghostty | Symlinks to `~/.config/ghostty/themes/` — apply via config or reload |
161+
| OpenCode | Symlinks to `~/.config/opencode/themes/` — auto-detected on restart |
162+
| Obsidian | Copies to vault's `.obsidian/themes/` — activate in Settings → Appearance |
163+
| iTerm2 | Symlinks to `~/.config/iterm2/themes/` — import via Color Presets |
160164

161165
## How it works
162166

163167
1. Scans editor extension directories for `package.json` theme contributions
164168
2. Parses VS Code theme JSON (with JSONC comment stripping and `include` inheritance)
165169
3. Converts to an intermediate representation (IR)
166-
4. Saves to a central theme store (`~/.config/chromaport/themes/`)
170+
4. Saves to a central theme store (`~/chromaport/themes/`)
167171
5. Symlinks or writes to the selected target format
168172

169173
## Development

src/apply.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ use crate::target::{self, LinkResult, PostWriteAction};
66
use anyhow::Result;
77

88
pub fn run() -> Result<()> {
9-
if !interactive::is_tty() {
10-
anyhow::bail!("Not a TTY. chromaport apply requires an interactive terminal.");
11-
}
9+
interactive::require_tty("chromaport apply")?;
1210

1311
// ── 1. Load saved IRs ───────────────────────────────────────────────
1412
let ir_files = store::list_ir_files()?;
@@ -71,7 +69,7 @@ pub fn run() -> Result<()> {
7169

7270
// ── 5. Select targets (with applied markers) ─────────────────────
7371
let selected_targets = if all_targets.len() == 1 && !applied[0] {
74-
println!(
72+
eprintln!(
7573
"\nTarget: {} (only detected target)",
7674
all_targets[0].display_name()
7775
);
@@ -91,7 +89,7 @@ pub fn run() -> Result<()> {
9189
// Write
9290
let written_path = match t.write(&selected_ir) {
9391
Ok(path) => {
94-
println!(
92+
eprintln!(
9593
" {} {} → {}",
9694
console::style("✔").green(),
9795
selected_ir.name,

src/cli.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ use clap::{Parser, Subcommand, ValueEnum};
55
version,
66
about = "Migrate VS Code / Cursor / OpenCode / iTerm2 themes to Superset, Warp, Ghostty, OpenCode, Obsidian, iTerm2, and more",
77
long_about = None,
8-
disable_version_flag = true
8+
disable_version_flag = true,
9+
after_help = "\
10+
Examples:
11+
chromaport Interactive mode (select editor, theme, target)
12+
chromaport -e vscode -t ghostty Import VS Code theme to Ghostty
13+
chromaport apply Apply a saved theme to new targets
14+
chromaport create Create a custom theme with color picker
15+
chromaport presets list Browse available preset themes
16+
chromaport presets install Install preset themes"
917
)]
1018
pub struct Cli {
1119
/// Print version
@@ -27,14 +35,23 @@ pub struct Cli {
2735
#[derive(Subcommand)]
2836
pub enum Command {
2937
/// Check for updates and upgrade chromaport
38+
#[command(
39+
long_about = "Check for updates and upgrade chromaport.\n\nChecks GitHub releases for new versions and auto-detects your install method\n(Homebrew or Cargo) to run the appropriate upgrade command.\nUse -y to skip the confirmation prompt."
40+
)]
3041
Update {
3142
/// Skip confirmation prompt
3243
#[arg(short = 'y', long)]
3344
yes: bool,
3445
},
3546
/// Apply a saved theme to additional targets
47+
#[command(
48+
long_about = "Apply a saved theme to additional targets.\n\nOpens a TUI preview to select from themes saved in ~/chromaport/themes/,\nthen lets you choose which target apps to apply the theme to."
49+
)]
3650
Apply,
3751
/// Create a custom theme from scratch
52+
#[command(
53+
long_about = "Create a custom theme from scratch.\n\nOpens an interactive color picker (HSL sliders + hex input) to choose\nbackground, foreground, and accent colors. A full palette is automatically\nderived from your 3 base colors."
54+
)]
3855
Create,
3956
/// Manage preset themes
4057
Presets {
@@ -46,8 +63,14 @@ pub enum Command {
4663
#[derive(Subcommand)]
4764
pub enum PresetsAction {
4865
/// List available preset themes
66+
#[command(
67+
long_about = "List available preset themes.\n\nFetches the preset catalog from GitHub and displays each theme's name,\nauthor, license, and install status."
68+
)]
4969
List,
5070
/// Install preset themes
71+
#[command(
72+
long_about = "Install preset themes.\n\nFetches available presets from GitHub and opens a multi-select prompt\nto choose which themes to download and save locally."
73+
)]
5174
Install,
5275
}
5376

src/create.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ enum Phase {
2727
}
2828

2929
pub fn run_create() -> Result<()> {
30+
crate::interactive::require_tty("chromaport create")?;
3031
// 1. Dark/Light selection
3132
let options = vec!["Dark", "Light"];
3233
let theme_type = match inquire::Select::new("Theme type:", options).prompt() {

src/interactive.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ pub fn is_tty() -> bool {
88
std::io::stdin().is_terminal()
99
}
1010

11+
/// Guard: bail if not running in an interactive terminal.
12+
/// `context` is inserted into the message, e.g. "chromaport create" or "chromaport".
13+
pub fn require_tty(context: &str) -> Result<()> {
14+
if is_tty() {
15+
return Ok(());
16+
}
17+
anyhow::bail!(
18+
"Not a TTY. {context} requires an interactive terminal.\n\
19+
Run this command directly in your terminal."
20+
);
21+
}
22+
1123
/// Let user pick dark or light theme type.
1224
pub fn select_theme_type() -> Result<crate::ir::ThemeType> {
1325
let options = vec!["Dark", "Light"];
@@ -51,7 +63,7 @@ pub fn select_target(available: &[Target]) -> Result<Target> {
5163

5264
if available.len() == 1 {
5365
let t = available[0].clone();
54-
println!("Target: {} (auto-detected)", t.display_name());
66+
eprintln!("Target: {} (auto-detected)", t.display_name());
5567
return Ok(t);
5668
}
5769

@@ -186,7 +198,10 @@ pub fn select_vault(vaults: &[std::path::PathBuf]) -> Result<std::path::PathBuf>
186198
fn handle_inquire_error(e: InquireError) -> anyhow::Error {
187199
match e {
188200
InquireError::NotTTY => {
189-
anyhow::anyhow!("Not a TTY. chromaport requires an interactive terminal.")
201+
anyhow::anyhow!(
202+
"Not a TTY. chromaport requires an interactive terminal.\n\
203+
Run this command directly in your terminal."
204+
)
190205
}
191206
InquireError::OperationCanceled => {
192207
std::process::exit(0);

src/main.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ fn run() -> Result<()> {
3434

3535
// Handle subcommands
3636
if let Some(ref cmd) = cli.command {
37+
if cli.editor.is_some() || cli.target.is_some() {
38+
let cmd_name = match cmd {
39+
Command::Update { .. } => "update",
40+
Command::Apply => "apply",
41+
Command::Create => "create",
42+
Command::Presets { .. } => "presets",
43+
};
44+
eprintln!(
45+
"Note: --editor/--target options are not used with the '{}' command.",
46+
cmd_name
47+
);
48+
}
3749
match cmd {
3850
Command::Update { yes } => return update::run_update(*yes),
3951
Command::Apply => return apply::run(),
@@ -84,7 +96,7 @@ fn run() -> Result<()> {
8496
)
8597
})?
8698
} else if all_editors.len() == 1 {
87-
println!(
99+
eprintln!(
88100
"Editor: {} (auto-detected)",
89101
match &all_editors[0].0 {
90102
Editor::Vscode => "VS Code",
@@ -141,9 +153,7 @@ fn run() -> Result<()> {
141153
};
142154

143155
// ── 4. Select theme (single-select with live preview) ─────────────────
144-
if !interactive::is_tty() {
145-
anyhow::bail!("Not a TTY. chromaport requires an interactive terminal.");
146-
}
156+
interactive::require_tty("chromaport")?;
147157

148158
let selected_entry = match preview::select_theme_with_preview(
149159
&all_themes,
@@ -156,7 +166,7 @@ fn run() -> Result<()> {
156166
};
157167

158168
// ── 5. Convert ────────────────────────────────────────────────────────
159-
println!("\nConverting theme...");
169+
eprintln!("\nConverting theme...");
160170
let theme_json = reader.read_theme_json(&selected_entry)?;
161171
let ir = converter::convert(&selected_entry, &theme_json)?;
162172

@@ -204,9 +214,7 @@ fn run_opencode_import(cli: &Cli) -> Result<()> {
204214
};
205215

206216
// Select theme
207-
if !interactive::is_tty() {
208-
anyhow::bail!("Not a TTY. chromaport requires an interactive terminal.");
209-
}
217+
interactive::require_tty("chromaport")?;
210218

211219
let theme_names: Vec<String> = opencode_themes.iter().map(|(n, _)| n.clone()).collect();
212220
let selected_name = inquire::Select::new("Select OpenCode theme:", theme_names)
@@ -219,7 +227,7 @@ fn run_opencode_import(cli: &Cli) -> Result<()> {
219227
.unwrap();
220228

221229
// Convert
222-
println!("\nConverting theme...");
230+
eprintln!("\nConverting theme...");
223231
let theme_type = converter_opencode::infer_theme_type(&theme_map);
224232
let ir = converter_opencode::convert_opencode(&name, &theme_map, theme_type)?;
225233

@@ -263,9 +271,7 @@ fn run_iterm2_import(cli: &Cli) -> Result<()> {
263271
};
264272

265273
// Select preset
266-
if !interactive::is_tty() {
267-
anyhow::bail!("Not a TTY. chromaport requires an interactive terminal.");
268-
}
274+
interactive::require_tty("chromaport")?;
269275

270276
let preset_names: Vec<String> = presets.iter().map(|(n, _)| n.clone()).collect();
271277
let selected_name = inquire::Select::new("Select iTerm2 color preset:", preset_names)
@@ -281,7 +287,7 @@ fn run_iterm2_import(cli: &Cli) -> Result<()> {
281287
let theme_type = interactive::select_theme_type()?;
282288

283289
// Convert
284-
println!("\nConverting theme...");
290+
eprintln!("\nConverting theme...");
285291
let ir = converter_iterm2::convert_iterm2(&name, &preset, theme_type)?;
286292

287293
// Write, link, post-write, save IR
@@ -301,10 +307,10 @@ fn write_link_and_save(target: &Target, ir: &ir::ThemeIR) -> Result<()> {
301307
}
302308

303309
// Write
304-
println!();
310+
eprintln!();
305311
let written_path = match target.write(ir) {
306312
Ok(path) => {
307-
println!(
313+
eprintln!(
308314
" {} {} \u{2192} {}",
309315
console::style("\u{2714}").green(),
310316
ir.name,

0 commit comments

Comments
 (0)