Replace content CLI with Textual TUI
This commit is contained in:
1 parent
0d58ddd430
commit
737700a9a8
28 files changed
+2403
-167
No files matched your search
@@ -0,0 +1,147 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
SEPARATOR = "|"
|
||||
|
||||
FIELD_HINTS = {
|
||||
"lines": "one per line",
|
||||
"tags": "comma separated",
|
||||
"images": "one image path per line",
|
||||
"link_map": "one per line: name url",
|
||||
"link_list": "one per line: label | url",
|
||||
"awards": "one per line: track | place",
|
||||
"socials": "one per line: name | icon | url",
|
||||
"skill_map": "one per line: Category: skill, skill",
|
||||
}
|
||||
MULTILINE_KINDS = {"longtext", "lines", "images", "link_map", "link_list", "awards", "socials", "skill_map"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class FieldSpec:
|
||||
path: str
|
||||
label: str
|
||||
kind: str = "text"
|
||||
required: bool = False
|
||||
options: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def hint(self) -> str:
|
||||
return FIELD_HINTS.get(self.kind, "")
|
||||
|
||||
|
||||
def get_path(record: dict, path: str):
|
||||
value = record
|
||||
for part in path.split("."):
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
value = value.get(part)
|
||||
return value
|
||||
|
||||
|
||||
def is_empty(value) -> bool:
|
||||
return value is None or value is False or value == "" or value == [] or value == {}
|
||||
|
||||
|
||||
def set_path(record: dict, path: str, value) -> None:
|
||||
parts = path.split(".")
|
||||
parents = [record]
|
||||
for part in parts[:-1]:
|
||||
child = parents[-1].get(part)
|
||||
if not isinstance(child, dict):
|
||||
child = {}
|
||||
parents[-1][part] = child
|
||||
parents.append(child)
|
||||
|
||||
if is_empty(value):
|
||||
parents[-1].pop(parts[-1], None)
|
||||
else:
|
||||
parents[-1][parts[-1]] = value
|
||||
|
||||
for depth in range(len(parts) - 1, 0, -1):
|
||||
if not parents[depth]:
|
||||
parents[depth - 1].pop(parts[depth - 1], None)
|
||||
|
||||
|
||||
def split_parts(line: str, count: int) -> list[str]:
|
||||
parts = [part.strip() for part in line.split(SEPARATOR, count - 1)]
|
||||
return parts + [""] * (count - len(parts))
|
||||
|
||||
|
||||
def non_empty_lines(text: str) -> list[str]:
|
||||
return [line.strip() for line in text.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def to_form_text(kind: str, value) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if kind in ("lines", "images"):
|
||||
return "\n".join(value)
|
||||
if kind == "tags":
|
||||
return ", ".join(value)
|
||||
if kind == "link_map":
|
||||
return "\n".join(f"{name} {url}" for name, url in value.items())
|
||||
if kind == "link_list":
|
||||
return "\n".join(f"{link.get('label', '')} {SEPARATOR} {link.get('url', '')}" for link in value)
|
||||
if kind == "awards":
|
||||
return "\n".join(f"{award.get('track', '')} {SEPARATOR} {award.get('place', '')}" for award in value)
|
||||
if kind == "socials":
|
||||
return "\n".join(f"{social.get('name', '')} {SEPARATOR} {social.get('icon', '')} {SEPARATOR} {social.get('url', '')}" for social in value)
|
||||
if kind == "skill_map":
|
||||
return "\n".join(f"{category}: {', '.join(skills)}" for category, skills in value.items())
|
||||
return str(value)
|
||||
|
||||
|
||||
def from_form_text(kind: str, text: str):
|
||||
text = text.strip()
|
||||
if kind in ("lines", "images"):
|
||||
return non_empty_lines(text)
|
||||
if kind == "tags":
|
||||
return [tag.strip() for tag in text.split(",") if tag.strip()]
|
||||
if kind == "link_map":
|
||||
links = {}
|
||||
for line in non_empty_lines(text):
|
||||
name, _, url = line.partition(" ")
|
||||
if not url.strip():
|
||||
raise ValueError(f"Link line needs a name and a url: {line}")
|
||||
links[name.strip()] = url.strip()
|
||||
return links
|
||||
if kind == "link_list":
|
||||
links = []
|
||||
for line in non_empty_lines(text):
|
||||
label, url = split_parts(line, 2)
|
||||
if not url:
|
||||
raise ValueError(f"Link line needs label | url: {line}")
|
||||
links.append({"label": label, "url": url})
|
||||
return links
|
||||
if kind == "awards":
|
||||
awards = []
|
||||
for line in non_empty_lines(text):
|
||||
track, place = split_parts(line, 2)
|
||||
awards.append({"track": track, "place": place} if place else {"track": track})
|
||||
return awards
|
||||
if kind == "socials":
|
||||
socials = []
|
||||
for line in non_empty_lines(text):
|
||||
name, icon, url = split_parts(line, 3)
|
||||
if not url:
|
||||
raise ValueError(f"Social line needs name | icon | url: {line}")
|
||||
socials.append({"name": name, "icon": icon, "url": url})
|
||||
return socials
|
||||
if kind == "skill_map":
|
||||
skills = {}
|
||||
for line in non_empty_lines(text):
|
||||
category, _, values = line.partition(":")
|
||||
if not values.strip():
|
||||
raise ValueError(f"Skill line needs Category: skill, skill: {line}")
|
||||
skills[category.strip()] = [skill.strip() for skill in values.split(",") if skill.strip()]
|
||||
return skills
|
||||
return text
|
||||
|
||||
|
||||
def summary_text(value) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, bool):
|
||||
return "yes" if value else ""
|
||||
if isinstance(value, list):
|
||||
return ", ".join(str(item) for item in value if not isinstance(item, dict))
|
||||
return str(value)
|
||||
Reference in new issue
Block a user