1use std::path::PathBuf;
2
3use clap::builder::styling::{AnsiColor, Styles};
4use clap::Parser;
5
6use crate::errors::UsageError;
7use crate::render::{
8 BoardSideSet, GeneratedStyle, GobanRange, MoveNumberOptions, NodeDescription, RenderOptions,
9};
10use crate::text::TileSet;
11
12const CLAP_STYLES: Styles = Styles::styled()
14 .header(AnsiColor::Yellow.on_default())
15 .usage(AnsiColor::Green.on_default())
16 .literal(AnsiColor::Green.on_default())
17 .placeholder(AnsiColor::Green.on_default());
18
19#[derive(Debug, Parser)]
20#[clap(version, about, styles=CLAP_STYLES)]
21pub struct SgfRenderArgs {
22 #[clap(subcommand)]
23 pub command: Option<Command>,
24 #[arg(value_name = "FILE", global = true)]
26 pub infile: Option<PathBuf>,
27 #[arg(short, long, value_name = "FILE")]
29 pub outfile: Option<PathBuf>,
30 #[arg(short = 'f', long = "format", default_value = "svg")]
32 #[cfg_attr(not(feature = "png"), arg(hide = true))]
33 pub output_format: OutputFormat,
34 #[arg(short, long, default_value_t = false)]
36 pub lenient: bool,
37 #[clap(flatten)]
38 pub render_args: RenderArgs,
39}
40
41#[derive(Debug, clap::Subcommand)]
42pub enum Command {
43 Query(QueryArgs),
45}
46
47#[derive(Debug, Parser)]
48pub struct RenderArgs {
49 #[clap(flatten)]
50 node_description: NodeDescription,
51 #[arg(
53 short = 'w',
54 long = "width",
55 value_name = "WIDTH",
56 default_value_t = 800.0
57 )]
58 viewbox_width: f64,
59 #[arg(short, long, conflicts_with = "range")]
61 shrink_wrap: bool,
62 #[arg(short, long)]
64 range: Option<GobanRange>,
65 #[arg(long = "style", value_name = "STYLE", default_value = "simple")]
67 generated_style: GeneratedStyle,
68 #[arg(long, value_name = "FILE", conflicts_with = "generated_style")]
70 custom_style: Option<PathBuf>,
71 #[arg(long, require_equals=true, num_args = 0..=1, value_name = "RANGE", default_missing_value = "1")]
73 move_numbers: Option<MoveNumberRange>,
74 #[arg(
76 long,
77 value_name = "NUM",
78 default_value_t = 1,
79 requires = "move_numbers"
80 )]
81 move_numbers_from: u64,
82 #[arg(long, value_name = "SIDES", default_value = "nw")]
84 label_sides: BoardSideSet,
85 #[arg(long, conflicts_with = "label_sides")]
87 no_board_labels: bool,
88 #[arg(long, default_value = "●○┏┓┗┛┯┠┷┨┼")]
90 tileset: TileSet,
91 #[clap(long = "no-marks", action = clap::ArgAction::SetFalse)]
93 draw_marks: bool,
94 #[clap(long = "no-triangles", action = clap::ArgAction::SetFalse)]
96 draw_triangles: bool,
97 #[clap(long = "no-circles", action = clap::ArgAction::SetFalse)]
99 draw_circles: bool,
100 #[clap(long = "no-squares", action = clap::ArgAction::SetFalse)]
102 draw_squares: bool,
103 #[clap(long = "no-selected", action = clap::ArgAction::SetFalse)]
105 draw_selected: bool,
106 #[clap(long = "no-dimmed", action = clap::ArgAction::SetFalse)]
108 draw_dimmed: bool,
109 #[clap(long = "no-labels", action = clap::ArgAction::SetFalse)]
111 draw_labels: bool,
112 #[clap(long = "no-lines", action = clap::ArgAction::SetFalse)]
114 draw_lines: bool,
115 #[clap(long = "no-arrows", action = clap::ArgAction::SetFalse)]
117 draw_arrows: bool,
118 #[clap(long)]
120 no_point_markup: bool,
121 #[clap(long)]
123 kifu: bool,
124}
125
126impl RenderArgs {
127 pub fn options(&self, output_format: &OutputFormat) -> Result<RenderOptions, UsageError> {
129 let goban_range = if self.shrink_wrap {
130 GobanRange::ShrinkWrap
131 } else if let Some(range) = &self.range {
132 range.clone()
133 } else {
134 GobanRange::FullBoard
135 };
136
137 let style = match &self.custom_style {
138 Some(filename) => {
139 let data = std::fs::read_to_string(filename)
140 .map_err(|e| UsageError::StyleReadError(e.into()))?;
141 toml::from_str(&data).map_err(|e| UsageError::StyleReadError(e.into()))?
142 }
143 None => self.generated_style.style().clone(),
144 };
145
146 let count_from = self.move_numbers_from;
147 let move_number_options = if let Some(range) = self.move_numbers {
148 Some(MoveNumberOptions {
149 start: range.start,
150 end: range.end,
151 count_from,
152 })
153 } else if self.kifu {
154 Some(MoveNumberOptions {
155 start: 1,
156 end: None,
157 count_from,
158 })
159 } else {
160 None
161 };
162
163 let no_point_markup = self.no_point_markup;
164 let label_sides = if self.no_board_labels {
165 BoardSideSet::default()
166 } else {
167 self.label_sides
168 };
169
170 if output_format == &OutputFormat::Text {
171 if self.kifu {
172 return Err(UsageError::InvalidTextOutputOption("Kifu mode".to_owned()));
173 }
174 if move_number_options.is_some() {
175 return Err(UsageError::InvalidTextOutputOption(
176 "Move numbers".to_owned(),
177 ));
178 }
179 }
180
181 Ok(RenderOptions {
182 node_description: self.node_description,
183 goban_range,
184 style,
185 viewbox_width: self.viewbox_width,
186 label_sides,
187 move_number_options,
188 draw_marks: self.draw_marks && !no_point_markup,
189 draw_triangles: self.draw_triangles && !no_point_markup,
190 draw_circles: self.draw_circles && !no_point_markup,
191 draw_squares: self.draw_squares && !no_point_markup,
192 draw_selected: self.draw_selected && !no_point_markup,
193 draw_dimmed: self.draw_dimmed && !no_point_markup,
194 draw_labels: self.draw_labels && !no_point_markup,
195 draw_lines: self.draw_lines && !no_point_markup,
196 draw_arrows: self.draw_arrows && !no_point_markup,
197 kifu_mode: self.kifu,
198 tileset: self.tileset.clone(),
199 })
200 }
201}
202
203#[derive(Debug, Clone, Copy, clap::ValueEnum, Eq, PartialEq)]
204pub enum OutputFormat {
205 Svg,
206 Text,
207 #[cfg(feature = "png")]
208 Png,
209}
210
211#[derive(Debug, Clone, Copy)]
212struct MoveNumberRange {
213 start: u64,
214 end: Option<u64>,
215}
216
217impl std::str::FromStr for MoveNumberRange {
218 type Err = UsageError;
219
220 fn from_str(s: &str) -> Result<Self, Self::Err> {
221 let parts: Vec<_> = s.splitn(2, '-').collect();
222 let start = parts[0]
223 .parse()
224 .map_err(|_| UsageError::InvalidFirstMoveNumber)?;
225 let end = parts
226 .get(1)
227 .map(|end| end.parse())
228 .transpose()
229 .map_err(|_| UsageError::InvalidLastMoveNumber)?;
230 Ok(MoveNumberRange { start, end })
231 }
232}
233
234#[derive(Debug, Parser)]
235pub struct QueryArgs {
236 #[clap(long, group = "mode")]
238 pub last_game: bool,
239 #[clap(long, group = "mode")]
241 pub last_variation: bool,
242 #[clap(long, group = "mode")]
244 pub last_node: bool,
245 #[arg(short, long, default_value_t = 0)]
247 pub game_number: u64,
248 #[arg(short, long, default_value_t = 0)]
250 pub variation: u64,
251}
252
253#[derive(Debug, Clone, Copy)]
254pub enum QueryMode {
255 Default,
256 LastGame,
257 LastVariation,
258 LastNode,
259}
260
261impl QueryArgs {
262 pub fn mode(&self) -> QueryMode {
263 if self.last_game {
264 QueryMode::LastGame
265 } else if self.last_variation {
266 QueryMode::LastVariation
267 } else if self.last_node {
268 QueryMode::LastNode
269 } else {
270 QueryMode::Default
271 }
272 }
273}