Skip to content

Commit aa3059b

Browse files
committed
feat(man): bidirectional conflicts with user-facing wording
Build a bidirectional conflict map so conflicts are shown from both sides (e.g. --json-input shows --quantity and --quantity shows --json-input). Use .PP for the conflict paragraph and user-facing wording "Cannot be used with" instead of the developer-facing "Conflicts with". https://claude.ai/code/session_01CrXuWDMVQsiUBoy6ceACsF
1 parent 2486e7e commit aa3059b

2 files changed

Lines changed: 66 additions & 22 deletions

File tree

exports/pdu.1

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ List of files and/or directories
1616
.TP
1717
\fB\-\-json\-input\fR
1818
Read JSON data from stdin
19-
.br
20-
Conflicts with \fB\-\-quantity\fR, \fB\-\-deduplicate\-hardlinks\fR, \fB\-\-one\-file\-system\fR.
19+
.PP
20+
Cannot be used with \fB\-\-deduplicate\-hardlinks\fR, \fB\-\-one\-file\-system\fR, \fB\-\-quantity\fR.
2121
.TP
2222
\fB\-\-json\-output\fR
2323
Print JSON data instead of an ASCII chart
@@ -38,9 +38,13 @@ Use binary scale, i.e. 1K = 1024B, 1M = 1024K, and so on
3838
.TP
3939
\fB\-H\fR, \fB\-\-deduplicate\-hardlinks\fR, \fB\-\-detect\-links\fR, \fB\-\-dedupe\-links\fR
4040
Detect and subtract the sizes of hardlinks from their parent directory totals
41+
.PP
42+
Cannot be used with \fB\-\-json\-input\fR.
4143
.TP
4244
\fB\-x\fR, \fB\-\-one\-file\-system\fR
4345
Skip directories on different filesystems
46+
.PP
47+
Cannot be used with \fB\-\-json\-input\fR.
4448
.TP
4549
\fB\-\-top\-down\fR
4650
Print the tree top\-down instead of bottom\-up
@@ -61,17 +65,21 @@ Measure block sizes (block\-count * 512B)
6165
\fB\-\-quantity block\-count\fR
6266
Count numbers of blocks
6367
.RE
68+
.PP
69+
Cannot be used with \fB\-\-json\-input\fR.
6470
.TP
6571
\fB\-d\fR, \fB\-\-max\-depth\fR, \fB\-\-depth\fR \fI<MAX_DEPTH>\fR [default: 10]
6672
Maximum depth to display the data. Could be either "inf" or a positive integer
6773
.TP
6874
\fB\-w\fR, \fB\-\-total\-width\fR, \fB\-\-width\fR \fI<TOTAL_WIDTH>\fR
6975
Width of the visualization
70-
.br
71-
Conflicts with \fB\-\-column\-width\fR.
76+
.PP
77+
Cannot be used with \fB\-\-column\-width\fR.
7278
.TP
7379
\fB\-\-column\-width\fR \fI<TREE_WIDTH>\fR\fI \fR\fI<BAR_WIDTH>\fR
7480
Maximum widths of the tree column and width of the bar column
81+
.PP
82+
Cannot be used with \fB\-\-total\-width\fR.
7583
.TP
7684
\fB\-m\fR, \fB\-\-min\-ratio\fR \fI<MIN_RATIO>\fR [default: 0.01]
7785
Minimal size proportion required to appear

src/man_page.rs

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,56 @@
11
use crate::args::Args;
22
use clap::{Arg, ArgAction, Command, CommandFactory};
3-
use std::{borrow::Cow, fmt::Write};
3+
use std::{borrow::Cow, collections::BTreeMap, fmt::Write};
4+
5+
/// A map from argument ID to the set of argument IDs it conflicts with (bidirectional).
6+
type ConflictMap = BTreeMap<String, Vec<String>>;
47

58
/// Renders the man page for `pdu` as a string in roff format.
69
pub fn render_man_page() -> String {
710
let mut command = Args::command();
811
command.build();
12+
let conflict_map = build_conflict_map(&command);
913
let mut out = String::new();
1014
render_title(&mut out, &command);
1115
render_name_section(&mut out, &command);
1216
render_synopsis_section(&mut out, &command);
1317
render_description_section(&mut out, &command);
14-
render_options_section(&mut out, &command);
18+
render_options_section(&mut out, &command, &conflict_map);
1519
render_examples_section(&mut out, &command);
1620
render_version_section(&mut out, &command);
1721
out
1822
}
1923

24+
/// Builds a bidirectional conflict map from clap's one-directional conflict declarations.
25+
fn build_conflict_map(command: &Command) -> ConflictMap {
26+
let mut map = ConflictMap::new();
27+
for arg in command.get_arguments() {
28+
let arg_id = arg.get_id().to_string();
29+
for conflict in command.get_arg_conflicts_with(arg) {
30+
let conflict_id = conflict.get_id().to_string();
31+
map.entry(arg_id.clone())
32+
.or_default()
33+
.push(conflict_id.clone());
34+
map.entry(conflict_id).or_default().push(arg_id.clone());
35+
}
36+
}
37+
// Deduplicate each entry
38+
for conflicts in map.values_mut() {
39+
conflicts.sort();
40+
conflicts.dedup();
41+
}
42+
map
43+
}
44+
45+
/// Resolves an argument ID to its `--long` flag name for display.
46+
fn resolve_flag_name(command: &Command, arg_id: &str) -> Option<String> {
47+
command
48+
.get_arguments()
49+
.find(|arg| arg.get_id().as_str() == arg_id)
50+
.and_then(|arg| arg.get_long())
51+
.map(|long| format!("\\fB\\-\\-{}\\fR", roff_escape(long)))
52+
}
53+
2054
/// Escapes a string for roff by replacing hyphens with `\-`.
2155
fn roff_escape(text: &str) -> String {
2256
text.replace('-', r"\-")
@@ -128,17 +162,17 @@ fn render_paragraph_text(out: &mut String, text: &str) {
128162
}
129163
}
130164

131-
fn render_options_section(out: &mut String, command: &Command) {
165+
fn render_options_section(out: &mut String, command: &Command, conflict_map: &ConflictMap) {
132166
out.push_str(".SH OPTIONS\n");
133167
for arg in command.get_arguments() {
134168
if arg.is_hide_set() {
135169
continue;
136170
}
137-
render_option_entry(out, command, arg);
171+
render_option_entry(out, command, arg, conflict_map);
138172
}
139173
}
140174

141-
fn render_option_entry(out: &mut String, command: &Command, arg: &Arg) {
175+
fn render_option_entry(out: &mut String, command: &Command, arg: &Arg, conflict_map: &ConflictMap) {
142176
out.push_str(".TP\n");
143177
if arg.is_positional() {
144178
render_option_header_positional(out, arg);
@@ -152,7 +186,7 @@ fn render_option_entry(out: &mut String, command: &Command, arg: &Arg) {
152186
.unwrap_or_default();
153187
writeln!(out, "{}", roff_escape(&help)).unwrap();
154188
render_possible_values(out, arg);
155-
render_conflicts(out, command, arg);
189+
render_conflicts(out, command, arg, conflict_map);
156190
}
157191

158192
fn render_option_header_positional(out: &mut String, arg: &Arg) {
@@ -254,23 +288,25 @@ fn render_possible_values(out: &mut String, arg: &Arg) {
254288
out.push_str(".RE\n");
255289
}
256290

257-
fn render_conflicts(out: &mut String, command: &Command, arg: &Arg) {
258-
let conflicts = command.get_arg_conflicts_with(arg);
259-
if conflicts.is_empty() {
260-
return;
261-
}
262-
let conflict_names: Vec<_> = conflicts
291+
fn render_conflicts(out: &mut String, command: &Command, arg: &Arg, conflict_map: &ConflictMap) {
292+
let arg_id = arg.get_id().as_str();
293+
let conflict_ids = match conflict_map.get(arg_id) {
294+
Some(ids) if !ids.is_empty() => ids,
295+
_ => return,
296+
};
297+
let conflict_names: Vec<_> = conflict_ids
263298
.iter()
264-
.filter_map(|conflict_arg| {
265-
conflict_arg
266-
.get_long()
267-
.map(|long| format!("\\fB\\-\\-{}\\fR", roff_escape(long)))
268-
})
299+
.filter_map(|conflict_id| resolve_flag_name(command, conflict_id))
269300
.collect();
270301
if conflict_names.is_empty() {
271302
return;
272303
}
273-
writeln!(out, ".br\nConflicts with {}.", conflict_names.join(", ")).unwrap();
304+
writeln!(
305+
out,
306+
".PP\nCannot be used with {}.",
307+
conflict_names.join(", ")
308+
)
309+
.unwrap();
274310
}
275311

276312
fn render_examples_section(out: &mut String, command: &Command) {

0 commit comments

Comments
 (0)