sgf_render/
args.rs

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
12// clap v3 styling
13const 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    /// SGF file to read from [default: read from stdin].
25    #[arg(value_name = "FILE", global = true)]
26    pub infile: Option<PathBuf>,
27    /// Output file [default: write to stdout].
28    #[arg(short, long, value_name = "FILE")]
29    pub outfile: Option<PathBuf>,
30    /// Output format.
31    #[arg(short = 'f', long = "format", default_value = "svg")]
32    #[cfg_attr(not(feature = "png"), arg(hide = true))]
33    pub output_format: OutputFormat,
34    /// Parse the SGF file even if it contains errors (may drop invalid data)
35    #[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    /// Print a tree of the SGF's variations
44    Query(QueryArgs),
45}
46
47#[derive(Debug, Parser)]
48pub struct RenderArgs {
49    #[clap(flatten)]
50    node_description: NodeDescription,
51    /// Width of the output image in pixels.
52    #[arg(
53        short = 'w',
54        long = "width",
55        value_name = "WIDTH",
56        default_value_t = 800.0
57    )]
58    viewbox_width: f64,
59    /// Draw only enough of the board to hold all the stones (with 1 space padding).
60    #[arg(short, long, conflicts_with = "range")]
61    shrink_wrap: bool,
62    /// Range to draw as a pair of corners (e.g. 'cc-ff').
63    #[arg(short, long)]
64    range: Option<GobanRange>,
65    /// Style to use.
66    #[arg(long = "style", value_name = "STYLE", default_value = "simple")]
67    generated_style: GeneratedStyle,
68    /// Custom style `toml` file. Conflicts with '--style'. See the README for details.
69    #[arg(long, value_name = "FILE", conflicts_with = "generated_style")]
70    custom_style: Option<PathBuf>,
71    /// Draw move numbers (may replace other markup).
72    #[arg(long, require_equals=true, num_args = 0..=1, value_name = "RANGE", default_missing_value = "1")]
73    move_numbers: Option<MoveNumberRange>,
74    /// Number to start counting move numbers from (requires --move-numbers).
75    #[arg(
76        long,
77        value_name = "NUM",
78        default_value_t = 1,
79        requires = "move_numbers"
80    )]
81    move_numbers_from: u64,
82    /// Sides to draw position labels on.
83    #[arg(long, value_name = "SIDES", default_value = "nw")]
84    label_sides: BoardSideSet,
85    /// Don't draw position labels.
86    #[arg(long, conflicts_with = "label_sides")]
87    no_board_labels: bool,
88    /// Tileset to use for text rendering (11 characters)
89    #[arg(long, default_value = "●○┏┓┗┛┯┠┷┨┼")]
90    tileset: TileSet,
91    /// Don't draw SGF marks.
92    #[clap(long = "no-marks", action = clap::ArgAction::SetFalse)]
93    draw_marks: bool,
94    /// Don't draw SGF triangles.
95    #[clap(long = "no-triangles", action = clap::ArgAction::SetFalse)]
96    draw_triangles: bool,
97    /// Don't draw SGF circles.
98    #[clap(long = "no-circles", action = clap::ArgAction::SetFalse)]
99    draw_circles: bool,
100    /// Don't draw SGF squares.
101    #[clap(long = "no-squares", action = clap::ArgAction::SetFalse)]
102    draw_squares: bool,
103    /// Don't draw SGF selected.
104    #[clap(long = "no-selected", action = clap::ArgAction::SetFalse)]
105    draw_selected: bool,
106    /// Don't draw SGF dimmed.
107    #[clap(long = "no-dimmed", action = clap::ArgAction::SetFalse)]
108    draw_dimmed: bool,
109    /// Don't draw SGF labels.
110    #[clap(long = "no-labels", action = clap::ArgAction::SetFalse)]
111    draw_labels: bool,
112    /// Don't draw SGF lines.
113    #[clap(long = "no-lines", action = clap::ArgAction::SetFalse)]
114    draw_lines: bool,
115    /// Don't draw SGF arrows.
116    #[clap(long = "no-arrows", action = clap::ArgAction::SetFalse)]
117    draw_arrows: bool,
118    /// Don't draw any markup on points.
119    #[clap(long)]
120    no_point_markup: bool,
121    /// Generate a kifu.
122    #[clap(long)]
123    kifu: bool,
124}
125
126impl RenderArgs {
127    /// Map RenderArgs to RenderOptions.
128    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    /// Print the index of the last game.
237    #[clap(long, group = "mode")]
238    pub last_game: bool,
239    /// Print the index of the last variation in the selected game.
240    #[clap(long, group = "mode")]
241    pub last_variation: bool,
242    /// Print the index of the last node in the selected variation.
243    #[clap(long, group = "mode")]
244    pub last_node: bool,
245    /// Game number to query.
246    #[arg(short, long, default_value_t = 0)]
247    pub game_number: u64,
248    /// Variation number to query.
249    #[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}