chameleon-cv 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/AGENTS.md +186 -0
- package/LICENSE +21 -0
- package/Makefile +136 -0
- package/README.md +356 -0
- package/bin/chameleon.js +136 -0
- package/chameleon.bat +286 -0
- package/chameleon.sh +353 -0
- package/package.json +39 -0
- package/profiles/github_repos.json +1266 -0
- package/requirements.txt +5 -0
- package/scan.sh +12 -0
- package/score.sh +12 -0
- package/scripts/__init__.py +1 -0
- package/scripts/__main_tui__.py +11 -0
- package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/scripts/__pycache__/__main_tui__.cpython-313.pyc +0 -0
- package/scripts/__pycache__/ats_ghost.cpython-313.pyc +0 -0
- package/scripts/__pycache__/cache.cpython-313.pyc +0 -0
- package/scripts/__pycache__/config.cpython-312.pyc +0 -0
- package/scripts/__pycache__/config.cpython-313.pyc +0 -0
- package/scripts/__pycache__/cover_letter.cpython-313.pyc +0 -0
- package/scripts/__pycache__/db.cpython-312.pyc +0 -0
- package/scripts/__pycache__/db.cpython-313.pyc +0 -0
- package/scripts/__pycache__/fonts.cpython-312.pyc +0 -0
- package/scripts/__pycache__/fonts.cpython-313.pyc +0 -0
- package/scripts/__pycache__/ghost.cpython-313.pyc +0 -0
- package/scripts/__pycache__/job_matcher.cpython-312.pyc +0 -0
- package/scripts/__pycache__/job_matcher.cpython-313.pyc +0 -0
- package/scripts/__pycache__/question.cpython-313.pyc +0 -0
- package/scripts/__pycache__/render.cpython-313.pyc +0 -0
- package/scripts/__pycache__/review.cpython-313.pyc +0 -0
- package/scripts/__pycache__/setup.cpython-312.pyc +0 -0
- package/scripts/__pycache__/tailor_brief.cpython-312.pyc +0 -0
- package/scripts/__pycache__/tailor_brief.cpython-313.pyc +0 -0
- package/scripts/__pycache__/tailor_cv.cpython-312.pyc +0 -0
- package/scripts/__pycache__/tailor_cv.cpython-313.pyc +0 -0
- package/scripts/__pycache__/tailor_prompts.cpython-312.pyc +0 -0
- package/scripts/__pycache__/tailor_prompts.cpython-313.pyc +0 -0
- package/scripts/__pycache__/tui_app.cpython-312.pyc +0 -0
- package/scripts/__pycache__/tui_app.cpython-313.pyc +0 -0
- package/scripts/ats_ghost.py +170 -0
- package/scripts/cache.py +63 -0
- package/scripts/cli.py +109 -0
- package/scripts/config.py +376 -0
- package/scripts/cover_letter.py +105 -0
- package/scripts/db.py +331 -0
- package/scripts/fonts.py +186 -0
- package/scripts/ghost.py +88 -0
- package/scripts/job_matcher.py +636 -0
- package/scripts/job_scanner/__init__.py +8 -0
- package/scripts/job_scanner/__main__.py +6 -0
- package/scripts/job_scanner/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/job_scanner/__pycache__/__init__.cpython-313.pyc +0 -0
- package/scripts/job_scanner/__pycache__/__main__.cpython-312.pyc +0 -0
- package/scripts/job_scanner/__pycache__/__main__.cpython-313.pyc +0 -0
- package/scripts/job_scanner/__pycache__/base.cpython-312.pyc +0 -0
- package/scripts/job_scanner/__pycache__/base.cpython-313.pyc +0 -0
- package/scripts/job_scanner/__pycache__/browser.cpython-312.pyc +0 -0
- package/scripts/job_scanner/__pycache__/browser.cpython-313.pyc +0 -0
- package/scripts/job_scanner/__pycache__/cli.cpython-313.pyc +0 -0
- package/scripts/job_scanner/__pycache__/registry.cpython-312.pyc +0 -0
- package/scripts/job_scanner/__pycache__/registry.cpython-313.pyc +0 -0
- package/scripts/job_scanner/__pycache__/scanner.cpython-312.pyc +0 -0
- package/scripts/job_scanner/__pycache__/scanner.cpython-313.pyc +0 -0
- package/scripts/job_scanner/base.py +197 -0
- package/scripts/job_scanner/browser.py +244 -0
- package/scripts/job_scanner/cli.py +6 -0
- package/scripts/job_scanner/parsers/__init__.py +24 -0
- package/scripts/job_scanner/parsers/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/adzuna.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/adzuna.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/arc.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/arc.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/crypto_careers.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/crypto_careers.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/dailyremote.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/dailyremote.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/flexjobs.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/himalayas.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/himalayas.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/hn_hiring.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/hn_hiring.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/indeed.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/jobicy.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/jobicy.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/jooble.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/jooble.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/linkedin.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/opentoworkremote.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/relocate.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/relocate.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/remote_rocketship.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/remoteok.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/remoteok.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/remotive.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/remotive.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/web3_career.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/wellfound.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/weworkremotely.cpython-312.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/weworkremotely.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/__pycache__/ycomb.cpython-313.pyc +0 -0
- package/scripts/job_scanner/parsers/adzuna.py +81 -0
- package/scripts/job_scanner/parsers/arc.py +65 -0
- package/scripts/job_scanner/parsers/crypto_careers.py +88 -0
- package/scripts/job_scanner/parsers/dailyremote.py +91 -0
- package/scripts/job_scanner/parsers/flexjobs.py +91 -0
- package/scripts/job_scanner/parsers/himalayas.py +47 -0
- package/scripts/job_scanner/parsers/hn_hiring.py +94 -0
- package/scripts/job_scanner/parsers/indeed.py +98 -0
- package/scripts/job_scanner/parsers/jobicy.py +60 -0
- package/scripts/job_scanner/parsers/jooble.py +68 -0
- package/scripts/job_scanner/parsers/linkedin.py +140 -0
- package/scripts/job_scanner/parsers/opentoworkremote.py +53 -0
- package/scripts/job_scanner/parsers/relocate.py +95 -0
- package/scripts/job_scanner/parsers/remote_rocketship.py +90 -0
- package/scripts/job_scanner/parsers/remoteok.py +63 -0
- package/scripts/job_scanner/parsers/remotive.py +57 -0
- package/scripts/job_scanner/parsers/web3_career.py +92 -0
- package/scripts/job_scanner/parsers/wellfound.py +94 -0
- package/scripts/job_scanner/parsers/weworkremotely.py +60 -0
- package/scripts/job_scanner/parsers/ycomb.py +83 -0
- package/scripts/job_scanner/registry.py +65 -0
- package/scripts/job_scanner/scanner.py +162 -0
- package/scripts/question.py +107 -0
- package/scripts/render.py +110 -0
- package/scripts/review.py +89 -0
- package/scripts/setup.py +139 -0
- package/scripts/tailor_brief.py +379 -0
- package/scripts/tailor_cv.py +746 -0
- package/scripts/tailor_prompts.py +101 -0
- package/scripts/tui/__init__.py +1 -0
- package/scripts/tui/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/tui/__pycache__/__init__.cpython-313.pyc +0 -0
- package/scripts/tui/__pycache__/app.cpython-312.pyc +0 -0
- package/scripts/tui/__pycache__/app.cpython-313.pyc +0 -0
- package/scripts/tui/__pycache__/helpers.cpython-312.pyc +0 -0
- package/scripts/tui/__pycache__/helpers.cpython-313.pyc +0 -0
- package/scripts/tui/__pycache__/screens.cpython-312.pyc +0 -0
- package/scripts/tui/__pycache__/screens.cpython-313.pyc +0 -0
- package/scripts/tui/__pycache__/widgets.cpython-312.pyc +0 -0
- package/scripts/tui/__pycache__/widgets.cpython-313.pyc +0 -0
- package/scripts/tui/app.py +1547 -0
- package/scripts/tui/helpers.py +65 -0
- package/scripts/tui/screens.py +642 -0
- package/scripts/tui/widgets.py +276 -0
- package/scripts/tui_app.py +25 -0
- package/scripts/wizard.py +109 -0
- package/tui.bat +50 -0
- package/tui.sh +59 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Reusable widgets: NanoEditor and SettingsPane."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from textual import on
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
7
|
+
from textual.widgets import Button, Input, Select, Static, TextArea
|
|
8
|
+
|
|
9
|
+
from scripts.config import config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
13
|
+
# NANO EDITOR
|
|
14
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
15
|
+
class NanoEditor(Vertical):
|
|
16
|
+
DEFAULT_CSS = """
|
|
17
|
+
NanoEditor { height: 1fr; }
|
|
18
|
+
NanoEditor #editor_text { height: 1fr; border: tall $primary; }
|
|
19
|
+
NanoEditor #editor_status { dock: bottom; height: 1; background: $primary; color: $text; padding: 0 1; }
|
|
20
|
+
NanoEditor #editor_hints { dock: bottom; height: 1; background: $secondary-darken-2; color: $text-muted; padding: 0 1; }
|
|
21
|
+
"""
|
|
22
|
+
_filename: str | None = None
|
|
23
|
+
_original_text: str = ""
|
|
24
|
+
BINDINGS = [
|
|
25
|
+
Binding("ctrl+x", "editor_save_exit", "Save & Exit", show=True),
|
|
26
|
+
Binding("ctrl+g", "editor_goto", "Go To", show=True),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
def compose(self):
|
|
30
|
+
yield TextArea(id="editor_text")
|
|
31
|
+
yield Static(" ^X Save&Exit ^G GoTo", id="editor_hints")
|
|
32
|
+
yield Static(" [bold]New Buffer[/bold] Ln 1, Col 1", id="editor_status")
|
|
33
|
+
|
|
34
|
+
def load_text(self, text: str, filename: str | None = None) -> None:
|
|
35
|
+
try:
|
|
36
|
+
ta = self.query_one("#editor_text", TextArea)
|
|
37
|
+
ta.load_text(text or "")
|
|
38
|
+
self._original_text = text or ""
|
|
39
|
+
if filename:
|
|
40
|
+
self._filename = filename
|
|
41
|
+
self._update_status()
|
|
42
|
+
ta.focus()
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def get_text(self) -> str:
|
|
47
|
+
try:
|
|
48
|
+
return self.query_one("#editor_text", TextArea).text
|
|
49
|
+
except Exception:
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
def is_modified(self) -> bool:
|
|
53
|
+
try:
|
|
54
|
+
return self.query_one("#editor_text", TextArea).text != self._original_text
|
|
55
|
+
except Exception:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def _update_status(self) -> None:
|
|
59
|
+
try:
|
|
60
|
+
ta = self.query_one("#editor_text", TextArea)
|
|
61
|
+
loc = ta.cursor_location
|
|
62
|
+
lines = loc[0] + 1
|
|
63
|
+
col = loc[1] + 1
|
|
64
|
+
modified = " [+]" if self.is_modified() else ""
|
|
65
|
+
name = self._filename or "New Buffer"
|
|
66
|
+
self.query_one("#editor_status", Static).update(
|
|
67
|
+
f" [bold]{name}{modified}[/bold]"
|
|
68
|
+
f"{'':>{max(1, 60 - len(name) - len(modified))}}"
|
|
69
|
+
f"Ln {lines}, Col {col}"
|
|
70
|
+
)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def on_text_area_cursor(self) -> None:
|
|
75
|
+
self._update_status()
|
|
76
|
+
|
|
77
|
+
def on_text_area_changed(self) -> None:
|
|
78
|
+
self._update_status()
|
|
79
|
+
|
|
80
|
+
def action_editor_save_exit(self) -> None:
|
|
81
|
+
from scripts.tui.app import ChameleonTUI
|
|
82
|
+
app = self.app
|
|
83
|
+
if isinstance(app, ChameleonTUI):
|
|
84
|
+
app._save_editor_content()
|
|
85
|
+
try:
|
|
86
|
+
app.query_one("#main_tabs").active = "tab_jobs"
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def action_editor_goto(self) -> None:
|
|
91
|
+
from scripts.tui.screens import GotoLineScreen
|
|
92
|
+
try:
|
|
93
|
+
ta = self.query_one("#editor_text", TextArea)
|
|
94
|
+
self.app.push_screen(GotoLineScreen(ta))
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
100
|
+
# SETTINGS PANE
|
|
101
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
102
|
+
class SettingsPane(VerticalScroll):
|
|
103
|
+
"""Dynamic settings editor — all config keys editable."""
|
|
104
|
+
|
|
105
|
+
DEFAULT_CSS = """
|
|
106
|
+
SettingsPane { height: 1fr; }
|
|
107
|
+
SettingsPane .section-title {
|
|
108
|
+
background: $primary-darken-1; color: $text;
|
|
109
|
+
padding: 0 1; height: 2; content-align: left middle;
|
|
110
|
+
text-style: bold; margin: 1 0 0 0;
|
|
111
|
+
}
|
|
112
|
+
SettingsPane .setting-row { height: 3; padding: 0 1; layout: horizontal; }
|
|
113
|
+
SettingsPane .setting-label { width: 32; content-align: left middle; color: $text-muted; }
|
|
114
|
+
SettingsPane .setting-value { width: 1fr; }
|
|
115
|
+
SettingsPane #settings_actions { height: 4; padding: 1; layout: horizontal; }
|
|
116
|
+
SettingsPane #settings_actions Button { margin: 0 1; }
|
|
117
|
+
SettingsPane #font_preview {
|
|
118
|
+
height: 3; padding: 0 2;
|
|
119
|
+
background: $surface-darken-1; color: $text;
|
|
120
|
+
content-align: center middle;
|
|
121
|
+
margin: 0 1;
|
|
122
|
+
}
|
|
123
|
+
SettingsPane #settings_status {
|
|
124
|
+
height: 1; padding: 0 1; color: $text-muted;
|
|
125
|
+
text-style: italic;
|
|
126
|
+
}
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
_SECTION_ICONS: dict[str, str] = {
|
|
130
|
+
"Paths": "\U0001f4c1",
|
|
131
|
+
"Scanner": "\U0001f50d",
|
|
132
|
+
"Matching": "\u2694",
|
|
133
|
+
"AI Reviewer": "\U0001f916",
|
|
134
|
+
"Telegram Bot": "\U0001f4e2",
|
|
135
|
+
"WhatsApp": "\U0001f4ac",
|
|
136
|
+
"Bridge/RSS": "\U0001f4e1",
|
|
137
|
+
"Colors": "\U0001f3a8",
|
|
138
|
+
"Fonts": "\u270d",
|
|
139
|
+
"Cache": "\U0001f4be",
|
|
140
|
+
"Rate Limits": "\u23f1",
|
|
141
|
+
"ATS Ghost Text": "\U0001f47b",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
def compose(self):
|
|
145
|
+
from scripts.fonts import list_available_fonts, get_current_font
|
|
146
|
+
|
|
147
|
+
all_cfg = config.all()
|
|
148
|
+
# Keys already handled by the explicit font selector
|
|
149
|
+
font_handled_keys = {"cv_font_family"}
|
|
150
|
+
|
|
151
|
+
keys_by_section: dict[str, list[str]] = {
|
|
152
|
+
"Paths": [k for k in all_cfg if k.endswith("_path") or k.endswith("_dir")],
|
|
153
|
+
"Scanner": [k for k in all_cfg if k.startswith("scan_")],
|
|
154
|
+
"Matching": [k for k in all_cfg if k.startswith("score_") or k.startswith("lang_") or k.startswith("fw_") or k.startswith("dom_") or k.startswith("domain_")],
|
|
155
|
+
"AI Reviewer": [k for k in all_cfg if k.startswith("opencode_") or k.startswith("model_")],
|
|
156
|
+
"Telegram Bot": [k for k in all_cfg if k.startswith("telegram_")],
|
|
157
|
+
"WhatsApp": [k for k in all_cfg if k.startswith("evolution_") or k.startswith("whatsapp_")],
|
|
158
|
+
"Bridge/RSS": [k for k in all_cfg if k.startswith("rss_") or k.startswith("webhook_")],
|
|
159
|
+
"Colors": [k for k in all_cfg if k.startswith("color_")],
|
|
160
|
+
"Fonts": [k for k in all_cfg if k.startswith("cv_font_") and k not in font_handled_keys],
|
|
161
|
+
"Cache": [k for k in all_cfg if k.startswith("cache_")],
|
|
162
|
+
"Rate Limits": [k for k in all_cfg if k.startswith("rate_") or k.startswith("max_retry") or k.startswith("retry_")],
|
|
163
|
+
"ATS Ghost Text": [k for k in all_cfg if k.startswith("ats_ghost_")],
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for section, keys in keys_by_section.items():
|
|
167
|
+
if not keys:
|
|
168
|
+
continue
|
|
169
|
+
icon = self._SECTION_ICONS.get(section, "\u25cf")
|
|
170
|
+
yield Static(f" {icon} {section}", classes="section-title")
|
|
171
|
+
for key in keys:
|
|
172
|
+
val = all_cfg[key]
|
|
173
|
+
with Horizontal(classes="setting-row"):
|
|
174
|
+
yield Static(key, classes="setting-label")
|
|
175
|
+
if isinstance(val, bool):
|
|
176
|
+
yield Select(
|
|
177
|
+
[("true", "true"), ("false", "false")],
|
|
178
|
+
value=str(val).lower(),
|
|
179
|
+
id=f"set_{key}",
|
|
180
|
+
classes="setting-value",
|
|
181
|
+
)
|
|
182
|
+
elif isinstance(val, int):
|
|
183
|
+
yield Input(value=str(val), id=f"set_{key}", classes="setting-value", type="integer")
|
|
184
|
+
elif isinstance(val, float):
|
|
185
|
+
yield Input(value=str(val), id=f"set_{key}", classes="setting-value")
|
|
186
|
+
elif isinstance(val, list):
|
|
187
|
+
yield Input(value=",".join(val), id=f"set_{key}", classes="setting-value")
|
|
188
|
+
else:
|
|
189
|
+
yield Input(value=str(val), id=f"set_{key}", classes="setting-value")
|
|
190
|
+
|
|
191
|
+
yield Static(" Font Preview & Selection", classes="section-title")
|
|
192
|
+
yield Static(get_current_font(), id="font_preview")
|
|
193
|
+
fonts = list_available_fonts()
|
|
194
|
+
font_options = [(f["family"], f["family"]) for f in fonts[:50]]
|
|
195
|
+
yield Select(font_options, value=get_current_font(), id="set_cv_font_family", classes="setting-value")
|
|
196
|
+
|
|
197
|
+
with Horizontal(id="settings_actions"):
|
|
198
|
+
yield Button("Save to Disk", id="btn_save_config", variant="primary")
|
|
199
|
+
yield Button("Reload Config", id="btn_reload_config", variant="default")
|
|
200
|
+
yield Button("Reset to Defaults", id="btn_reset_config", variant="error")
|
|
201
|
+
yield Button("Clear Cache", id="btn_clear_cache", variant="default")
|
|
202
|
+
yield Static("[dim]Changes apply in memory instantly. Press Save to persist.[/dim]", id="settings_status")
|
|
203
|
+
|
|
204
|
+
@on(Select.Changed)
|
|
205
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
206
|
+
key = event.select.id
|
|
207
|
+
if key and key.startswith("set_"):
|
|
208
|
+
cfg_key = key[4:]
|
|
209
|
+
val = event.value
|
|
210
|
+
if val == "true":
|
|
211
|
+
config.set(cfg_key, True)
|
|
212
|
+
elif val == "false":
|
|
213
|
+
config.set(cfg_key, False)
|
|
214
|
+
else:
|
|
215
|
+
config.set(cfg_key, val)
|
|
216
|
+
_update_font_preview(self)
|
|
217
|
+
|
|
218
|
+
@on(Input.Changed)
|
|
219
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
220
|
+
key = event.input.id
|
|
221
|
+
if key and key.startswith("set_"):
|
|
222
|
+
cfg_key = key[4:]
|
|
223
|
+
raw = event.value.strip()
|
|
224
|
+
all_cfg = config.all()
|
|
225
|
+
current = all_cfg.get(cfg_key)
|
|
226
|
+
if isinstance(current, bool):
|
|
227
|
+
config.set_nopersist(cfg_key, raw.lower() == "true")
|
|
228
|
+
elif isinstance(current, int):
|
|
229
|
+
try:
|
|
230
|
+
config.set_nopersist(cfg_key, int(raw))
|
|
231
|
+
except (ValueError, TypeError):
|
|
232
|
+
pass
|
|
233
|
+
elif isinstance(current, list):
|
|
234
|
+
config.set_nopersist(cfg_key, [x.strip() for x in raw.split(",") if x.strip()])
|
|
235
|
+
else:
|
|
236
|
+
config.set_nopersist(cfg_key, raw)
|
|
237
|
+
_update_font_preview(self)
|
|
238
|
+
|
|
239
|
+
@on(Button.Pressed, "#btn_save_config")
|
|
240
|
+
def on_save(self) -> None:
|
|
241
|
+
config.persist()
|
|
242
|
+
self.notify("Config saved to disk", timeout=2)
|
|
243
|
+
try:
|
|
244
|
+
self.query_one("#settings_status", Static).update("[green]\u2714 Saved to .chameleon/config.json[/green]")
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
@on(Button.Pressed, "#btn_reload_config")
|
|
249
|
+
def on_reload(self) -> None:
|
|
250
|
+
config.reload()
|
|
251
|
+
_update_font_preview(self)
|
|
252
|
+
self.notify("Config reloaded from disk", timeout=2)
|
|
253
|
+
|
|
254
|
+
@on(Button.Pressed, "#btn_reset_config")
|
|
255
|
+
def on_reset(self) -> None:
|
|
256
|
+
from scripts.config import DEFAULTS
|
|
257
|
+
config.set_many(dict(DEFAULTS))
|
|
258
|
+
config.persist()
|
|
259
|
+
_update_font_preview(self)
|
|
260
|
+
self.notify("Config reset to defaults", timeout=2)
|
|
261
|
+
|
|
262
|
+
@on(Button.Pressed, "#btn_clear_cache")
|
|
263
|
+
def on_clear_cache(self) -> None:
|
|
264
|
+
from scripts.cache import cache_clear
|
|
265
|
+
n = cache_clear()
|
|
266
|
+
self.notify(f"Cleared {n} cached items", timeout=2)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _update_font_preview(pane: SettingsPane) -> None:
|
|
270
|
+
from scripts.fonts import get_current_font
|
|
271
|
+
try:
|
|
272
|
+
st = pane.query_one("#font_preview", Static)
|
|
273
|
+
current = get_current_font()
|
|
274
|
+
st.update(f" Current font: [bold]{current}[/bold] | Sample: The quick brown fox...")
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Chameleon Job TUI — backward-compatible entry point.
|
|
2
|
+
|
|
3
|
+
All logic now lives in scripts/tui/ package.
|
|
4
|
+
This file exists so `python scripts/tui_app.py` and the ./tui launcher keep working.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Ensure project root is on sys.path
|
|
12
|
+
_ROOT = Path(__file__).resolve().parent.parent
|
|
13
|
+
if str(_ROOT) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(_ROOT))
|
|
15
|
+
|
|
16
|
+
from scripts.tui.app import ChameleonTUI # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> None:
|
|
20
|
+
app = ChameleonTUI()
|
|
21
|
+
app.run()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
main()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chameleon Interactive Setup Wizard — guided install, config, and first CV.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
12
|
+
VENV = ROOT / ".venv"
|
|
13
|
+
BIN = VENV / ("Scripts" if sys.platform == "win32" else "bin")
|
|
14
|
+
PYTHON = BIN / ("python.exe" if sys.platform == "win32" else "python3")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def style(text: str, code: str) -> str:
|
|
18
|
+
codes = {"green": "32", "yellow": "33", "cyan": "36", "bold": "1", "dim": "2"}
|
|
19
|
+
parts = ";" .join(codes.get(c, c) for c in code.split())
|
|
20
|
+
return f"\033[{parts}m{text}\033[0m"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def print_header():
|
|
24
|
+
print()
|
|
25
|
+
print(style(" ╔══════════════════════════════════════════╗", "cyan+bold"))
|
|
26
|
+
print(style(" ║ Chameleon Setup Wizard ║", "cyan+bold"))
|
|
27
|
+
print(style(" ║ AI-Powered Resume Tailor v0.1.0 ║", "cyan+bold"))
|
|
28
|
+
print(style(" ╚══════════════════════════════════════════╝", "cyan+bold"))
|
|
29
|
+
print()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def step(num: int, total: int, title: str):
|
|
33
|
+
print(f"\n{style(f'[{num}/{total}]', 'green+bold')} {style(title, 'bold')}")
|
|
34
|
+
print(f" {style('─' * 50, 'dim')}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
print_header()
|
|
39
|
+
|
|
40
|
+
from scripts.setup import (
|
|
41
|
+
ensure_venv, install_core, install_rendercv,
|
|
42
|
+
install_playwright, create_dirs, create_config,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
total = 6
|
|
46
|
+
|
|
47
|
+
step(1, total, "Checking Python")
|
|
48
|
+
try:
|
|
49
|
+
r = subprocess.run([sys.executable, "--version"], capture_output=True, text=True)
|
|
50
|
+
print(f" Found: {r.stdout.strip() or r.stderr.strip()}")
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
print(style(" ✗ Python not found! Install Python 3.10+ from https://python.org", "red+bold"))
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
step(2, total, "Creating Virtual Environment")
|
|
56
|
+
if not ensure_venv(print):
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
step(3, total, "Installing Dependencies (incl. Playwright + Chromium)")
|
|
60
|
+
install_core(print)
|
|
61
|
+
install_rendercv(print)
|
|
62
|
+
install_playwright(print)
|
|
63
|
+
|
|
64
|
+
step(4, total, "Creating Directories & Config")
|
|
65
|
+
create_dirs(print)
|
|
66
|
+
create_config(print)
|
|
67
|
+
|
|
68
|
+
step(5, total, "First CV Setup")
|
|
69
|
+
cvs = list((ROOT / "templates").glob("*_cv.yaml"))
|
|
70
|
+
if cvs:
|
|
71
|
+
print(f" Found {len(cvs)} existing CV(s):")
|
|
72
|
+
for i, cv in enumerate(cvs, 1):
|
|
73
|
+
print(f" {i}. {cv.name}")
|
|
74
|
+
else:
|
|
75
|
+
print(" No CV found in templates/.")
|
|
76
|
+
src = ROOT / "templates" / "master_template.yaml"
|
|
77
|
+
if src.exists():
|
|
78
|
+
dst = ROOT / "templates" / "my_cv.yaml"
|
|
79
|
+
shutil.copy(src, dst)
|
|
80
|
+
print(style(f" ✓ Created templates/my_cv.yaml from master template", "green"))
|
|
81
|
+
else:
|
|
82
|
+
print(" To add a CV later: chameleon init <path>")
|
|
83
|
+
|
|
84
|
+
step(6, total, "Verify Installation")
|
|
85
|
+
print(" Running tests...")
|
|
86
|
+
r = subprocess.run([str(PYTHON), "-m", "pytest", "tests/", "-q", "--tb=short"], cwd=ROOT,
|
|
87
|
+
capture_output=True, text=True, timeout=60)
|
|
88
|
+
if r.returncode == 0:
|
|
89
|
+
print(style(" ✓ All tests passed", "green"))
|
|
90
|
+
else:
|
|
91
|
+
out = r.stdout.strip() + r.stderr.strip()
|
|
92
|
+
for line in out.split("\n")[-3:]:
|
|
93
|
+
print(f" {line}")
|
|
94
|
+
print(style(" ⚠ Some tests failed (check output above)", "yellow"))
|
|
95
|
+
|
|
96
|
+
print()
|
|
97
|
+
print(style(" ╔══════════════════════════════════════════╗", "green+bold"))
|
|
98
|
+
print(style(" ║ Setup Complete! ║", "green+bold"))
|
|
99
|
+
print(style(" ╚══════════════════════════════════════════╝", "green+bold"))
|
|
100
|
+
print()
|
|
101
|
+
print(f" Next steps:")
|
|
102
|
+
print(f" {style('chameleon tui', 'cyan')} — Launch the terminal UI")
|
|
103
|
+
print(f" {style('chameleon help', 'cyan')} — See all commands")
|
|
104
|
+
print(f" {style('chameleon wizard', 'cyan')} — Re-run this wizard")
|
|
105
|
+
print()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
package/tui.bat
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM Chameleon Job TUI — launcher script (Windows)
|
|
3
|
+
setlocal enabledelayedexpansion
|
|
4
|
+
|
|
5
|
+
set "CHAMELEON_DIR=%~dp0"
|
|
6
|
+
set "VENV_PYTHON=%CHAMELEON_DIR%.venv\Scripts\python.exe"
|
|
7
|
+
|
|
8
|
+
if /I "%1"=="--help" goto :help
|
|
9
|
+
if /I "%1"=="-h" goto :help
|
|
10
|
+
if /I "%1"=="--version" goto :version
|
|
11
|
+
|
|
12
|
+
if not exist "%VENV_PYTHON%" (
|
|
13
|
+
echo [!] Virtual environment not found at %VENV_PYTHON%
|
|
14
|
+
echo Run: make install-tools
|
|
15
|
+
echo Or: python -m venv .venv ^&^& .venv\Scripts\pip install textual pyyaml pypdf reportlab httpx beautifulsoup4 lxml markdownify
|
|
16
|
+
pause
|
|
17
|
+
exit /b 1
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
cd /d "%CHAMELEON_DIR%"
|
|
21
|
+
"%VENV_PYTHON%" -m scripts.tui_app %*
|
|
22
|
+
exit /b %errorlevel%
|
|
23
|
+
|
|
24
|
+
:help
|
|
25
|
+
echo Chameleon Job TUI — scan, score, tailor, and diff
|
|
26
|
+
echo.
|
|
27
|
+
echo Usage: tui.bat [options]
|
|
28
|
+
echo.
|
|
29
|
+
echo Options:
|
|
30
|
+
echo --help, -h Show this help message
|
|
31
|
+
echo --version Show version info
|
|
32
|
+
echo.
|
|
33
|
+
echo Keybindings inside the TUI:
|
|
34
|
+
echo Ctrl+S Scan job platforms
|
|
35
|
+
echo Ctrl+N Paste a job description
|
|
36
|
+
echo Ctrl+E Edit selected job
|
|
37
|
+
echo Ctrl+T Tailor CV for selected job
|
|
38
|
+
echo Ctrl+R Rescore all jobs
|
|
39
|
+
echo Ctrl+D Delete selected job
|
|
40
|
+
echo Ctrl+O Open job URL in browser
|
|
41
|
+
echo / Search jobs
|
|
42
|
+
echo F1 Help screen
|
|
43
|
+
echo Ctrl+Q Quit
|
|
44
|
+
echo Ctrl+1-6 Switch tabs
|
|
45
|
+
echo.
|
|
46
|
+
exit /b 0
|
|
47
|
+
|
|
48
|
+
:version
|
|
49
|
+
echo Chameleon Job TUI v1.0
|
|
50
|
+
exit /b 0
|
package/tui.sh
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Chameleon Job TUI — launcher script (Unix/Linux/macOS)
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
CHAMELEON_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
VENV_PYTHON="$CHAMELEON_DIR/.venv/bin/python3"
|
|
7
|
+
|
|
8
|
+
show_help() {
|
|
9
|
+
cat <<EOF
|
|
10
|
+
Chameleon Job TUI — scan, score, tailor, and diff
|
|
11
|
+
|
|
12
|
+
Usage: ./tui.sh [options]
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--help, -h Show this help message
|
|
16
|
+
--version Show version info
|
|
17
|
+
|
|
18
|
+
Keybindings inside the TUI:
|
|
19
|
+
Ctrl+S Scan job platforms
|
|
20
|
+
Ctrl+N Paste a job description
|
|
21
|
+
Ctrl+E Edit selected job
|
|
22
|
+
Ctrl+T Tailor CV for selected job
|
|
23
|
+
Ctrl+R Rescore all jobs
|
|
24
|
+
Ctrl+D Delete selected job
|
|
25
|
+
Ctrl+O Open job URL in browser
|
|
26
|
+
/ Search jobs
|
|
27
|
+
F1 Help screen
|
|
28
|
+
Ctrl+Q Quit
|
|
29
|
+
Ctrl+1-6 Switch tabs
|
|
30
|
+
|
|
31
|
+
EOF
|
|
32
|
+
exit 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
36
|
+
show_help
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [[ "${1:-}" == "--version" ]]; then
|
|
40
|
+
echo "Chameleon Job TUI v1.0"
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Ensure venv exists
|
|
45
|
+
if [[ ! -f "$VENV_PYTHON" ]]; then
|
|
46
|
+
echo "[!] Virtual environment not found at $VENV_PYTHON"
|
|
47
|
+
echo " Run: ./chameleon setup"
|
|
48
|
+
echo " Or: python3 -m venv .venv && .venv/bin/pip install textual pyyaml httpx reportlab beautifulsoup4 lxml markdownify"
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Quick dep check
|
|
53
|
+
$VENV_PYTHON -c "import textual" 2>/dev/null || {
|
|
54
|
+
echo "[!] textual not installed. Run: $VENV_PYTHON -m pip install textual pyyaml httpx"
|
|
55
|
+
exit 1
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
cd "$CHAMELEON_DIR" || exit 1
|
|
59
|
+
exec "$VENV_PYTHON" -m scripts.tui_app "$@"
|