Coverage for src / rendercv / cli / new_command / new_command.py: 100%

64 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-25 13:49 +0000

1import pathlib 

2from collections.abc import Callable 

3from typing import Annotated 

4 

5import rich.panel 

6import typer 

7from rich import print 

8 

9from rendercv.exception import RenderCVUserError 

10from rendercv.schema.models.design.built_in_design import available_themes 

11from rendercv.schema.models.locale.locale import available_locales 

12from rendercv.schema.sample_generator import create_sample_yaml_input_file 

13 

14from ..app import app 

15from ..copy_templates import copy_templates 

16from ..error_handler import handle_user_errors 

17from .print_welcome import print_welcome 

18 

19 

20@app.command( 

21 name="new", 

22 help=( 

23 "Generate a YAML input file to get started. Example: [yellow]rendercv new" 

24 ' "John Doe"[/yellow]. Details: [cyan]rendercv new --help[/cyan]' 

25 ), 

26) 

27@handle_user_errors 

28def cli_command_new( 

29 full_name: Annotated[str, typer.Argument(help="Your full name")], 

30 theme: Annotated[ 

31 str, 

32 typer.Option( 

33 help=( 

34 "The name of the theme (available themes are:" 

35 f" {', '.join(available_themes)})" 

36 ) 

37 ), 

38 ] = "classic", 

39 locale: Annotated[ 

40 str, 

41 typer.Option( 

42 help=( 

43 "The name of the locale (available locales are:" 

44 f" {', '.join(available_locales)}). You can also continue with" 

45 " `English`, and then write your own `locale` field for your own" 

46 " language." 

47 ) 

48 ), 

49 ] = "english", 

50 create_typst_templates: Annotated[ 

51 bool, 

52 typer.Option( 

53 "--create-typst-templates", 

54 help="Create Typst templates", 

55 ), 

56 ] = False, 

57 create_markdown_templates: Annotated[ 

58 bool, 

59 typer.Option( 

60 "--create-markdown-templates", 

61 help="Create Markdown templates", 

62 ), 

63 ] = False, 

64): 

65 if theme not in available_themes: 

66 message = ( 

67 f"Theme {theme} is not available. Available themes are:" 

68 f" {', '.join(available_themes)}" 

69 ) 

70 raise RenderCVUserError(message) 

71 

72 if locale not in available_locales: 

73 message = ( 

74 f"Locale {locale} is not available. Available locales are:" 

75 f" {', '.join(available_locales)}" 

76 ) 

77 raise RenderCVUserError(message) 

78 

79 print_welcome() 

80 

81 input_file_path = pathlib.Path(f"{full_name.replace(' ', '_')}_CV.yaml") 

82 typst_templates_folder = pathlib.Path(theme) 

83 markdown_folder = pathlib.Path("markdown") 

84 

85 # Define all items to create: (description, path, creator, skip) 

86 items_to_create: list[tuple[str, pathlib.Path, Callable[[], object], bool]] = [ 

87 ( 

88 "Your YAML input file", 

89 input_file_path, 

90 lambda: create_sample_yaml_input_file( 

91 file_path=input_file_path, name=full_name, theme=theme, locale=locale 

92 ), 

93 True, # never skip the input file 

94 ), 

95 ( 

96 "Typst templates", 

97 typst_templates_folder, 

98 lambda: copy_templates("typst", typst_templates_folder), 

99 create_typst_templates, 

100 ), 

101 ( 

102 "Markdown templates", 

103 markdown_folder, 

104 lambda: copy_templates("markdown", markdown_folder), 

105 create_markdown_templates, 

106 ), 

107 ] 

108 

109 # Process items 

110 created_items: list[tuple[str, pathlib.Path]] = [] 

111 existing_items: list[tuple[str, pathlib.Path]] = [] 

112 for description, path, creator, create in items_to_create: 

113 if not create: 

114 continue 

115 if path.exists(): 

116 existing_items.append((description, path)) 

117 else: 

118 creator() 

119 created_items.append((description, path)) 

120 

121 print(build_creation_panel(input_file_path, created_items, existing_items)) 

122 

123 

124def build_creation_panel( 

125 input_file_path: pathlib.Path, 

126 created_items: list[tuple[str, pathlib.Path]], 

127 existing_items: list[tuple[str, pathlib.Path]], 

128) -> rich.panel.Panel: 

129 """Build Rich panel summarizing created and existing files. 

130 

131 Why: 

132 The ``new`` command output is a structured panel with file statuses, 

133 next steps, and template info. Extracting this keeps the command 

134 handler focused on orchestration while this function owns presentation. 

135 

136 Args: 

137 input_file_path: Path to the generated YAML input file. 

138 created_items: Items that were newly created (description, path). 

139 existing_items: Items that already existed (description, path). 

140 

141 Returns: 

142 Rich Panel ready for printing. 

143 """ 

144 lines: list[str] = [] 

145 

146 # Input file status (always first) 

147 input_file_created = any( 

148 desc == "Your YAML input file" for desc, _ in created_items 

149 ) 

150 if input_file_created: 

151 lines.append( 

152 "[green]✓[/green] Created your YAML input file: " 

153 f"[purple]./{input_file_path}[/purple]" 

154 ) 

155 else: 

156 lines.append( 

157 f"Your YAML input file already exists: [purple]./{input_file_path}[/purple]" 

158 ) 

159 

160 # Next steps (always visible) 

161 lines.append("") 

162 lines.append("Next steps:") 

163 lines.append(" 1. Edit the YAML input file with your information") 

164 lines.append(f" 2. Run: [cyan]rendercv render {input_file_path}[/cyan]") 

165 

166 # Templates (exclude input file from these lists) 

167 created_templates = [ 

168 (d, p) for d, p in created_items if d != "Your YAML input file" 

169 ] 

170 existing_templates = [ 

171 (d, p) for d, p in existing_items if d != "Your YAML input file" 

172 ] 

173 

174 if created_templates: 

175 lines.append("") 

176 lines.append("Also created:") 

177 for desc, path in created_templates: 

178 lines.append(f" ○ {desc}: ./{path}") 

179 

180 if existing_templates: 

181 lines.append("") 

182 lines.append("Not modified (already exist):") 

183 for desc, path in existing_templates: 

184 lines.append(f" - {desc}: ./{path}") 

185 

186 if created_templates or existing_templates: 

187 lines.append("") 

188 lines.append( 

189 "Templates are for advanced design customization. You can ignore or" 

190 " delete them." 

191 ) 

192 

193 return rich.panel.Panel( 

194 "\n".join(lines), 

195 title="Get started", 

196 title_align="left", 

197 border_style="bright_black", 

198 )