1 Commits
Author SHA1 Message Date
james.maesandClaude Opus 5.5 f0f1579555 ci: least-privilege GITHUB_TOKEN permissions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 16:12:21 -05:00
15 changed files with 479 additions and 603 deletions

No files matched your search

+6
View File
@@ -9,8 +9,14 @@ on:
env:
CARGO_TERM_COLOR: always
permissions:
contents: read
jobs:
test:
permissions:
contents: read
checks: write
name: Test
runs-on: ubuntu-latest
strategy:
+6
View File
@@ -4,8 +4,14 @@ on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
jobs:
dependabot:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
+7
View File
@@ -8,8 +8,13 @@ on:
env:
CARGO_TERM_COLOR: always
permissions:
contents: read
jobs:
create-release:
permissions:
contents: write
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
@@ -29,6 +34,8 @@ jobs:
prerelease: false
build:
permissions:
contents: write
needs: create-release
strategy:
matrix:
+1
View File
@@ -1,6 +1,7 @@
# Rust
/target/
**/*.rs.bk
Cargo.lock
# IDE
.vscode/
+56 -11
View File
@@ -1,13 +1,58 @@
# KofTwentyTwo rustfmt configuration (K22-CODE-RS). Stable-channel options only, so
# `cargo fmt --check` behaves the same on every toolchain.
edition = "2021"
# Kingsrook: 3-space indentation (tab_spaces is a stable option).
# Rustfmt configuration file
# Based on your .clang-format preferences
# Configured for nightly Rust with Allman brace style
# Indentation - matches your IndentWidth: 3, TabWidth: 3, UseTab: Never
tab_spaces = 3
hard_tabs = false
newline_style = "Unix"
# Kingsrook: no practical line limit. 200 keeps rustfmt's wrapping heuristics sane.
max_width = 200
use_field_init_shorthand = true
# Line length - matches your ColumnLimit: 100
max_width = 100
# Edition
edition = "2021"
# Brace style - matches your BreakBeforeBraces: Allman
brace_style = "AlwaysNextLine" # Opening braces on new lines
control_brace_style = "AlwaysNextLine" # Control structures with braces on new lines
# Comments - matches your AlignTrailingComments: true
wrap_comments = true
comment_width = 100
normalize_comments = false # ReflowComments: false
# Imports and modules
imports_granularity = "Crate"
imports_layout = "Mixed"
group_imports = "StdExternalCrate"
# Functions and expressions
fn_single_line = false # AllowShortFunctionsOnASingleLine: None
where_single_line = false
# Structs and enums
struct_field_align_threshold = 0
enum_discrim_align_threshold = 0
# Spacing
space_before_colon = false
space_after_colon = true
spaces_around_ranges = false
# Trailing commas
trailing_comma = "Vertical"
# Empty lines
empty_item_single_line = true
# max_empty_lines_to_keep = 3 # Not available in current nightly
# String formatting
format_strings = true
format_code_in_doc_comments = true
# Misc
remove_nested_parens = true
use_try_shorthand = true
# Not set: brace_style = "AlwaysNextLine" and blank_lines_upper_bound are nightly-only.
# Rust therefore keeps rustfmt's same-line braces (profile rule K22-CODE-RS-02).
use_field_init_shorthand = true
force_explicit_abi = true
# This configuration enforces your C++ Allman brace style in Rust
Generated
-344
View File
@@ -1,344 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "clap"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys",
]
[[package]]
name = "equivalent"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "kfmtbox"
version = "0.1.0"
dependencies = [
"clap",
"dirs",
"serde",
"serde_yaml",
]
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "libredox"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d"
dependencies = [
"libc",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_users"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0"
dependencies = [
"libredox",
"thiserror",
]
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
+31 -33
View File
@@ -1,34 +1,32 @@
# Clippy configuration file
# This file should be in the project root
# Allow explicit return statements
# Note: needless-return is not a valid clippy.toml option
# Instead, we use the Cargo.toml configuration
# Other valid clippy.toml options:
cognitive-complexity-threshold = 10
too-many-arguments-threshold = 7
too-many-lines-threshold = 100
type-complexity-threshold = 200
large-error-threshold = 128
# Bytes (clippy default 512000). 256 cannot be met by ordinary frames: measured debug-build frames are 297-1633 bytes
# (largest: render_flower_box at 1633, extract_content_from_box at 1551). 2048 keeps a gate just above them.
stack-size-threshold = 2048
enum-variant-size-threshold = 200
array-size-threshold = 512
max-fn-params-bools = 3
max-struct-bools = 3
max-trait-bounds = 3
literal-representation-threshold = 10
min-ident-chars-threshold = 3
single-char-binding-names-threshold = 2
missing-docs-in-crate-items = true
check-private-items = true
arithmetic-side-effects-allowed = []
allow-unwrap-in-tests = false
allow-expect-in-tests = false
allow-panic-in-tests = false
allow-dbg-in-tests = false
allow-print-in-tests = false
semicolon-inside-block-ignore-singleline = false
# Clippy configuration file
# This file should be in the project root
# Allow explicit return statements
# Note: needless-return is not a valid clippy.toml option
# Instead, we use the Cargo.toml configuration
# Other valid clippy.toml options:
cognitive-complexity-threshold = 10
too-many-arguments-threshold = 7
too-many-lines-threshold = 100
type-complexity-threshold = 200
large-error-threshold = 128
stack-size-threshold = 256
enum-variant-size-threshold = 200
array-size-threshold = 512
max-fn-params-bools = 3
max-struct-bools = 3
max-trait-bounds = 3
literal-representation-threshold = 10
min-ident-chars-threshold = 3
single-char-binding-names-threshold = 2
missing-docs-in-crate-items = true
check-private-items = true
arithmetic-side-effects-allowed = []
allow-unwrap-in-tests = false
allow-expect-in-tests = false
allow-panic-in-tests = false
allow-dbg-in-tests = false
allow-print-in-tests = false
semicolon-inside-block-ignore-singleline = false
semicolon-outside-block-ignore-multiline = false
+56 -11
View File
@@ -1,13 +1,58 @@
# KofTwentyTwo rustfmt configuration (K22-CODE-RS). Stable-channel options only, so
# `cargo fmt --check` behaves the same on every toolchain.
edition = "2021"
# Kingsrook: 3-space indentation (tab_spaces is a stable option).
# Rustfmt configuration file
# Based on your .clang-format preferences
# Configured for nightly Rust with Allman brace style
# Indentation - matches your IndentWidth: 3, TabWidth: 3, UseTab: Never
tab_spaces = 3
hard_tabs = false
newline_style = "Unix"
# Kingsrook: no practical line limit. 200 keeps rustfmt's wrapping heuristics sane.
max_width = 200
use_field_init_shorthand = true
# Line length - matches your ColumnLimit: 100
max_width = 100
# Edition
edition = "2021"
# Brace style - matches your BreakBeforeBraces: Allman
brace_style = "AlwaysNextLine" # Opening braces on new lines
control_brace_style = "AlwaysNextLine" # Control structures with braces on new lines
# Comments - matches your AlignTrailingComments: true
wrap_comments = true
comment_width = 100
normalize_comments = false # ReflowComments: false
# Imports and modules
imports_granularity = "Crate"
imports_layout = "Mixed"
group_imports = "StdExternalCrate"
# Functions and expressions
fn_single_line = false # AllowShortFunctionsOnASingleLine: None
where_single_line = false
# Structs and enums
struct_field_align_threshold = 0
enum_discrim_align_threshold = 0
# Spacing
space_before_colon = false
space_after_colon = true
spaces_around_ranges = false
# Trailing commas
trailing_comma = "Vertical"
# Empty lines
empty_item_single_line = true
# max_empty_lines_to_keep = 3 # Not available in current nightly
# String formatting
format_strings = true
format_code_in_doc_comments = true
# Misc
remove_nested_parens = true
use_try_shorthand = true
# Not set: brace_style = "AlwaysNextLine" and blank_lines_upper_bound are nightly-only.
# Rust therefore keeps rustfmt's same-line braces (profile rule K22-CODE-RS-02).
use_field_init_shorthand = true
force_explicit_abi = true
# This configuration enforces your C++ Allman brace style in Rust
+102 -51
View File
@@ -1,13 +1,17 @@
use crate::{comment_detector::CommentStyle, text_wrapper::TextWrapper};
pub struct BoxRenderer {
pub struct BoxRenderer
{
comment_style: CommentStyle,
text_wrapper: TextWrapper,
}
impl BoxRenderer {
impl BoxRenderer
{
#[must_use]
pub const fn new(comment_style: CommentStyle, max_width: usize) -> Self {
pub fn new(comment_style: CommentStyle, max_width: usize) -> Self
{
#[allow(clippy::needless_return)]
return Self {
comment_style,
@@ -16,7 +20,8 @@ impl BoxRenderer {
}
#[must_use]
pub fn render_flower_box(&self, content: &str) -> String {
pub fn render_flower_box(&self, content: &str) -> String
{
// Check if content is already in a flower box
let clean_content = self.extract_content_from_box(content);
@@ -24,82 +29,130 @@ impl BoxRenderer {
let wrapped_lines = self.text_wrapper.wrap_text(&clean_content);
let balanced_lines = self.text_wrapper.balance_lines(wrapped_lines);
// Find the widest line, in characters, for the box width
let max_line_len = balanced_lines.iter().map(|line| return line.chars().count()).max().unwrap_or(0);
// Find the maximum line length for box width
let max_line_len = balanced_lines
.iter()
.map(|line| line.len())
.max()
.unwrap_or(0);
// Inner width: one space either side of the text, minimum of 10
let inner_width = max_line_len.saturating_add(2).max(10);
let box_width = (max_line_len + 4).max(10); // Minimum width of 10
let prefix = &self.comment_style.prefix;
let border = "─".repeat(inner_width);
// Build the flower box
let mut result = Vec::new();
// Top border
result.push(format!("{prefix} ╭{border}╮"));
result.push(format!(
"{prefix} {}",
"/".repeat(box_width)
));
// Content lines
for line in balanced_lines {
let padding = inner_width.saturating_sub(line.chars().count()).saturating_sub(2);
result.push(format!("{prefix} │ {line}{} │", " ".repeat(padding)));
for line in balanced_lines
{
let padding = box_width - line.len() - 2;
let padded_line = format!(
"{prefix} {line}{} {prefix}",
" ".repeat(padding)
);
result.push(padded_line);
}
// Bottom border
result.push(format!("{prefix} ╰{border}╯"));
result.push(format!(
"{prefix} {}",
"/".repeat(box_width)
));
return result.join("\n");
result.join("\n")
}
/// Extracts content from an existing flower box comment, or returns the input unchanged.
fn extract_content_from_box(&self, content: &str) -> String {
/// Extracts content from an existing flower box comment.
///
/// # Panics
///
/// This function may panic if the input contains malformed flower box syntax
/// or if string slicing operations fail due to UTF-8 boundary issues.
fn extract_content_from_box(&self, content: &str) -> String
{
let lines: Vec<&str> = content.lines().collect();
let (Some(first), Some(last)) = (lines.first(), lines.last()) else {
return content.to_string();
};
if lines.len() < 3 {
if lines.len() < 3
{
return content.to_string();
}
// Check if this looks like a flower box
let (first_line, last_line) = (first.trim(), last.trim());
if !(first_line.contains('╭') && first_line.contains('╮') && last_line.contains('╰') && last_line.contains('╯')) {
return content.to_string();
}
let prefix = self.comment_style.prefix.as_str();
let prefix = &self.comment_style.prefix;
let mut content_lines = Vec::new();
// Extract content from the middle lines
for line in lines.iter().skip(1).take(lines.len().saturating_sub(2)) {
// Remove the comment prefix first
let clean_line = if line.trim_start().starts_with(prefix) {
line.split_once(prefix).map_or(*line, |(_, rest)| return rest)
} else {
line
};
// Check if this looks like a flower box
let first_line = lines[0].trim();
let last_line = lines[lines.len() - 1].trim();
// The content sits between the first two pipe characters
let mut parts = clean_line.split('│').skip(1);
if let (Some(inner), Some(_)) = (parts.next(), parts.next()) {
if !inner.is_empty() {
content_lines.push(inner.trim().to_string());
if first_line.contains("╭")
&& first_line.contains("╮")
&& last_line.contains("╰")
&& last_line.contains("╯")
{
// Extract content from middle lines
for line in lines.iter().skip(1).take(lines.len() - 2)
{
// Remove the comment prefix first
let clean_line = if line.trim_start().starts_with(prefix)
{
&line[line.find(prefix).unwrap() + prefix.len()..]
}
else
{
line
};
// Find the content between the pipe characters using char-based approach
let chars: Vec<char> = clean_line.chars().collect();
let mut start_idx = None;
let mut end_idx = None;
for (i, ch) in chars.iter().enumerate()
{
if *ch == '│'
{
if start_idx.is_none()
{
start_idx = Some(i + 1);
}
else
{
end_idx = Some(i);
break;
}
}
}
if let (Some(start), Some(end)) = (start_idx, end_idx)
{
if end > start
{
let content_part: String = chars[start..end].iter().collect();
content_lines.push(content_part.trim().to_string());
}
}
}
return content_lines.join(" ");
}
return content_lines.join(" ");
#[allow(clippy::needless_return)]
return content.to_string();
}
}
#[cfg(test)]
mod tests {
mod tests
{
use super::*;
use crate::comment_detector::CommentStyle;
/// # Panics
///
/// Panics if an assertion fails.
#[test]
fn test_render_flower_box() {
fn test_render_flower_box()
{
let comment_style = CommentStyle::new("//".to_string());
let renderer = BoxRenderer::new(comment_style, 80);
let content = "Hello, world!";
@@ -112,11 +165,9 @@ mod tests {
assert!(result.contains("Hello, world!"));
}
/// # Panics
///
/// Panics if an assertion fails.
#[test]
fn test_extract_content_from_box() {
fn test_extract_content_from_box()
{
let comment_style = CommentStyle::new("//".to_string());
let renderer = BoxRenderer::new(comment_style, 80);
+2 -1
View File
@@ -4,7 +4,8 @@ use clap::Parser;
#[command(name = "kfmtbox")]
#[command(about = "A command-line utility that wraps text in flower box style comments")]
#[command(version)]
pub struct Cli {
pub struct Cli
{
/// Input filename for file type detection
#[arg(short = 'f', long = "filename")]
pub filename: Option<String>,
+72 -64
View File
@@ -1,93 +1,101 @@
use std::collections::HashMap;
use crate::config::Config;
#[derive(Debug, Clone)]
pub struct CommentStyle {
pub struct CommentStyle
{
pub prefix: String,
pub is_block: bool,
}
impl CommentStyle {
#[must_use]
pub const fn new(prefix: String) -> Self {
return Self { prefix, is_block: false };
impl CommentStyle
{
pub fn new(prefix: String) -> Self
{
return Self {
prefix,
is_block: false,
};
}
}
/// Default comment prefixes for common file types.
const DEFAULT_STYLES: &[(&str, &str)] = &[
("rs", "//"),
("py", "#"),
("js", "//"),
("ts", "//"),
("go", "//"),
("java", "//"),
("cpp", "//"),
("c", "//"),
("h", "//"),
("hpp", "//"),
("cs", "//"),
("php", "//"),
("rb", "#"),
("sh", "#"),
("bash", "#"),
("zsh", "#"),
("fish", "#"),
("vim", "\""),
("lua", "--"),
("sql", "--"),
("yaml", "#"),
("yml", "#"),
("toml", "#"),
("ini", ";"),
("cfg", ";"),
("conf", "#"),
("dockerfile", "#"),
("makefile", "#"),
("cmake", "#"),
];
pub struct CommentDetector {
pub struct CommentDetector
{
pub default_style: CommentStyle,
style_map: HashMap<String, CommentStyle>,
}
impl CommentDetector {
#[must_use]
pub fn new() -> Self {
let style_map = DEFAULT_STYLES
.iter()
.map(|&(filetype, prefix)| return (filetype.to_string(), CommentStyle::new(prefix.to_string())))
.collect();
impl CommentDetector
{
pub fn new() -> Self
{
let mut style_map = HashMap::new();
return Self {
// Default comment styles for common file types
style_map.insert("rs".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("py".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("js".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("ts".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("go".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("java".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("cpp".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("c".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("h".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("hpp".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("cs".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("php".to_string(), CommentStyle::new("//".to_string()));
style_map.insert("rb".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("sh".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("bash".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("zsh".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("fish".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("vim".to_string(), CommentStyle::new("\"".to_string()));
style_map.insert("lua".to_string(), CommentStyle::new("--".to_string()));
style_map.insert("sql".to_string(), CommentStyle::new("--".to_string()));
style_map.insert("yaml".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("yml".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("toml".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("ini".to_string(), CommentStyle::new(";".to_string()));
style_map.insert("cfg".to_string(), CommentStyle::new(";".to_string()));
style_map.insert("conf".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("dockerfile".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("makefile".to_string(), CommentStyle::new("#".to_string()));
style_map.insert("cmake".to_string(), CommentStyle::new("#".to_string()));
Self {
default_style: CommentStyle::new("#".to_string()),
style_map,
};
}
}
#[must_use]
pub fn detect_from_filename(&self, filename: &str) -> &CommentStyle {
let ext = filename.rsplit('.').next().unwrap_or_default().to_lowercase();
return self.style_map.get(&ext).unwrap_or(&self.default_style);
pub fn detect_from_filename(&self, filename: &str) -> &CommentStyle
{
if let Some(ext) = filename.split('.').last()
{
let ext = ext.to_lowercase();
self.style_map.get(&ext).unwrap_or(&self.default_style)
}
else
{
&self.default_style
}
}
#[must_use]
pub fn detect_from_filetype(&self, filetype: &str) -> &CommentStyle {
pub fn detect_from_filetype(&self, filetype: &str) -> &CommentStyle
{
let filetype = filetype.to_lowercase();
return self.style_map.get(&filetype).unwrap_or(&self.default_style);
self.style_map.get(&filetype).unwrap_or(&self.default_style)
}
pub fn load_config(&mut self, config: &Config) {
for (filetype, prefix) in &config.comment_styles {
self.style_map.insert(filetype.clone(), CommentStyle::new(prefix.clone()));
pub fn load_config(&mut self, config: &Config)
{
for (filetype, prefix) in &config.comment_styles
{
self
.style_map
.insert(filetype.clone(), CommentStyle::new(prefix.clone()));
}
}
}
impl Default for CommentDetector {
fn default() -> Self {
return Self::new();
}
}
+31 -28
View File
@@ -1,60 +1,63 @@
use std::{collections::HashMap, path::PathBuf};
use dirs;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub struct Config
{
pub comment_styles: HashMap<String, String>,
}
impl Default for Config {
fn default() -> Self {
return Self { comment_styles: HashMap::new() };
impl Default for Config
{
fn default() -> Self
{
Self {
comment_styles: HashMap::new(),
}
}
}
impl Config {
/// Loads the configuration from the user's config directory, or returns the default when no file exists.
///
/// # Errors
///
/// Returns an error if the config file cannot be read or is not valid YAML.
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
impl Config
{
pub fn load() -> Result<Self, Box<dyn std::error::Error>>
{
let config_path = Self::get_config_path();
if config_path.exists() {
if config_path.exists()
{
let content = std::fs::read_to_string(&config_path)?;
let config: Self = serde_yaml::from_str(&content)?;
return Ok(config);
} else {
return Ok(Self::default());
let config: Config = serde_yaml::from_str(&content)?;
Ok(config)
}
else
{
Ok(Config::default())
}
}
#[must_use]
pub fn get_config_path() -> PathBuf {
let mut config_dir = dirs::config_dir().unwrap_or_else(|| return PathBuf::from("."));
pub fn get_config_path() -> PathBuf
{
let mut config_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
config_dir.push("kfmtbox");
config_dir.push("comments.yaml");
return config_dir;
config_dir
}
/// Writes the configuration to the user's config directory, creating it if needed.
///
/// # Errors
///
/// Returns an error if the directory or file cannot be written, or the config cannot be serialized.
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>>
{
let config_path = Self::get_config_path();
// Create config directory if it doesn't exist
if let Some(parent) = config_path.parent() {
if let Some(parent) = config_path.parent()
{
std::fs::create_dir_all(parent)?;
}
let content = serde_yaml::to_string(self)?;
std::fs::write(&config_path, content)?;
return Ok(());
Ok(())
}
}
+17 -10
View File
@@ -1,8 +1,8 @@
use std::io::{self, Read};
use clap::Parser;
use kfmtbox::verbose::{set_verbose, verbose_msg};
use kfmtbox::{BoxRenderer, Cli, CommentDetector, Config};
use kfmtbox::verbose::{set_verbose, verbose_msg};
/// Main entry point for the kfmtbox application.
///
@@ -15,14 +15,15 @@ use kfmtbox::{BoxRenderer, Cli, CommentDetector, Config};
/// - Input cannot be read from stdin
/// - Configuration file cannot be loaded or parsed
/// - Output cannot be written to stdout
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn main() -> Result<(), Box<dyn std::error::Error>>
{
let cli = Cli::parse();
set_verbose(cli.verbose);
// Load configuration
verbose_msg("Loading Config");
let config = Config::load().unwrap_or_else(|_| return Config::default());
let config = Config::load().unwrap_or_else(|_| Config::default());
// Initialize comment detector
verbose_msg("Detecting Comment Type");
@@ -30,11 +31,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
detector.load_config(&config);
// Determine comment style
let comment_style = if let Some(filename) = &cli.filename {
let comment_style = if let Some(filename) = &cli.filename
{
detector.detect_from_filename(filename).clone()
} else if let Some(filetype) = &cli.filetype {
}
else if let Some(filetype) = &cli.filetype
{
detector.detect_from_filetype(filetype).clone()
} else {
}
else
{
detector.default_style.clone()
};
@@ -46,9 +52,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Remove trailing newline if present and normalize line endings
verbose_msg("Cleaning Comment (Removing Newlines and Returns");
let input = input.trim_end();
let input = input.replace("\r\n", "\n").replace("\n\r", "\n").replace('\r', "\n");
let input = input.replace("\r\n", "\n").replace("\n\r", "\n").replace("\r", "\n");
if input.is_empty() {
if input.is_empty()
{
verbose_msg("Input was empty - returning nothing successfully");
return Ok(());
}
@@ -60,7 +67,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Output to stdout
verbose_msg("Outputting rendered comment");
println!("{result}");
println!("{}", result);
return Ok(());
Ok(())
}
+71 -40
View File
@@ -1,84 +1,113 @@
pub struct TextWrapper {
#[allow(clippy::needless_return)]
pub struct TextWrapper
{
max_width: usize,
}
impl TextWrapper {
#[must_use]
pub const fn new(max_width: usize) -> Self {
impl TextWrapper
{
pub fn new(max_width: usize) -> Self
{
return Self { max_width };
}
#[must_use]
pub fn wrap_text(&self, text: &str) -> Vec<String> {
pub fn wrap_text(&self, text: &str) -> Vec<String>
{
// First, split by newlines to preserve existing line breaks
let input_lines: Vec<&str> = text.split('\n').collect();
let mut all_lines = Vec::new();
for line in input_lines {
for line in input_lines
{
let words: Vec<&str> = line.split_whitespace().collect();
if words.is_empty() {
if words.is_empty()
{
all_lines.push(String::new());
continue;
}
let mut current_line = String::new();
for word in words {
let test_line = if current_line.is_empty() { word.to_string() } else { format!("{current_line} {word}") };
for word in words
{
let test_line = if current_line.is_empty()
{
word.to_string()
}
else
{
format!("{} {}", current_line, word)
};
// Use a smaller effective width for better wrapping
let effective_width = 20; // Force wrapping at 20 characters
if test_line.len() <= effective_width {
if test_line.len() <= effective_width
{
current_line = test_line;
} else {
if !current_line.is_empty() {
}
else
{
if !current_line.is_empty()
{
all_lines.push(current_line);
}
current_line = word.to_string();
}
}
if !current_line.is_empty() {
if !current_line.is_empty()
{
all_lines.push(current_line);
}
}
if all_lines.is_empty() {
if all_lines.is_empty()
{
return vec![String::new()];
}
return all_lines;
}
#[must_use]
pub fn balance_lines(&self, lines: Vec<String>) -> Vec<String> {
if lines.len() <= 1 {
pub fn balance_lines(&self, lines: Vec<String>) -> Vec<String>
{
if lines.len() <= 1
{
return lines;
}
let total_chars: usize = lines.iter().map(|line| return line.len()).sum();
let avg_chars_per_line = total_chars.checked_div(lines.len()).unwrap_or(0);
let target_width = usize::midpoint(avg_chars_per_line, self.max_width);
let total_chars: usize = lines.iter().map(|line| line.len()).sum();
let avg_chars_per_line = total_chars / lines.len();
let target_width = (avg_chars_per_line + self.max_width) / 2;
let max_combined_width = 25; // Don't combine lines if result would be longer than 25 chars
let mut balanced = Vec::new();
let mut current_line = String::new();
for line in lines {
if current_line.is_empty() {
for line in lines
{
if current_line.is_empty()
{
current_line = line;
} else {
let test_line = format!("{current_line} {line}");
if test_line.len() <= target_width && test_line.len() <= max_combined_width {
}
else
{
let test_line = format!("{} {}", current_line, line);
if test_line.len() <= target_width && test_line.len() <= max_combined_width
{
current_line = test_line;
} else {
}
else
{
balanced.push(current_line);
current_line = line;
}
}
}
if !current_line.is_empty() {
if !current_line.is_empty()
{
balanced.push(current_line);
}
@@ -87,33 +116,35 @@ impl TextWrapper {
}
#[cfg(test)]
mod tests {
mod tests
{
use super::*;
/// # Panics
///
/// Panics if an assertion fails.
#[test]
fn test_wrap_text() {
fn test_wrap_text()
{
let wrapper = TextWrapper::new(20);
let text = "This is a very long line that should be wrapped into multiple lines";
let result = wrapper.wrap_text(text);
assert!(result.len() > 1);
for line in &result {
for line in &result
{
assert!(line.len() <= 20);
}
}
/// # Panics
///
/// Panics if an assertion fails.
#[test]
fn test_balance_lines() {
fn test_balance_lines()
{
let wrapper = TextWrapper::new(20);
let lines = vec!["Short".to_string(), "Very long line that exceeds".to_string(), "Another short".to_string()];
let lines = vec![
"Short".to_string(),
"Very long line that exceeds".to_string(),
"Another short".to_string(),
];
let result = wrapper.balance_lines(lines);
assert_eq!(result.len(), 3);
assert!(result.len() >= 1);
}
}
+21 -10
View File
@@ -1,30 +1,41 @@
use std::sync::atomic::{AtomicBool, Ordering};
// global flag
pub static IS_VERBOSE: AtomicBool = AtomicBool::new(false);
// set from anywhere
pub fn set_verbose(on: bool) {
pub fn set_verbose(on: bool)
{
IS_VERBOSE.store(on, Ordering::Relaxed);
}
// read from anywhere
pub fn is_verbose() -> bool {
return IS_VERBOSE.load(Ordering::Relaxed);
pub fn is_verbose() -> bool
{
IS_VERBOSE.load(Ordering::Relaxed)
}
pub fn verbose_msg(msg: &str) {
if !is_verbose() {
pub fn verbose_msg(msg: &str)
{
if!is_verbose()
{
return;
}
println!("[VERBOSE] {msg}");
println!("[VERBOSE] {}", msg);
}
pub fn verbose_value<T: std::fmt::Debug>(label: &str, val: T) {
if !is_verbose() {
pub fn verbose_value<T: std::fmt::Debug>(label: &str, val: T)
{
if!is_verbose()
{
return;
}
println!("[VERBOSE] {label} = {val:?}");
}
println!("[VERBOSE] {} = {:?}", label, val);
}