tailwhip 0.9a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tailwhip/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Tailwhip - Sort Tailwind CSS classes in HTML and CSS files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import metadata
6
+
7
+ from tailwhip.cli import main
8
+
9
+ __all__ = ["main"]
10
+ __version__ = metadata.version("tailwhip")
11
+ __author__ = "Martin Mahner"
tailwhip/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Entry point for python -m tailwhip."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from tailwhip.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
tailwhip/cli.py ADDED
@@ -0,0 +1,148 @@
1
+ """CLI entrypoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import time
7
+ from importlib import metadata
8
+ from pathlib import Path # noqa: TC003
9
+ from typing import Annotated
10
+
11
+ import typer
12
+ from rich.console import Console
13
+
14
+ from tailwhip.constants import CONSOLE_THEME, GLOBS, SKIP_EXPRESSIONS, VERBOSITY_LOUD
15
+ from tailwhip.datatypes import Config
16
+ from tailwhip.files import apply_changes, find_files
17
+
18
+
19
+ def version_callback(value: bool) -> None:
20
+ """Print version and exit."""
21
+ if value:
22
+ version = metadata.version("tailwhip")
23
+ typer.echo(f"tailwhip {version}")
24
+ raise typer.Exit
25
+
26
+
27
+ app = typer.Typer(
28
+ help="Sort Tailwind CSS classes in HTML and CSS files",
29
+ add_completion=False,
30
+ )
31
+
32
+
33
+ def main() -> None:
34
+ """Entrypoint for the CLI."""
35
+ app()
36
+
37
+
38
+ @app.command()
39
+ def run( # noqa: PLR0913
40
+ paths: Annotated[
41
+ list[Path],
42
+ typer.Argument(
43
+ help=(
44
+ f"File or directory paths to process. Plain directories "
45
+ f"(e.g., 'templates/') use default patterns ({', '.join(GLOBS)}). "
46
+ f"Glob patterns (e.g., 'templates/**/*.postcss') are matched as-is."
47
+ )
48
+ ),
49
+ ],
50
+ version: Annotated[ # noqa: ARG001
51
+ bool,
52
+ typer.Option(
53
+ "--version",
54
+ callback=version_callback,
55
+ is_eager=True,
56
+ help="Show version and exit.",
57
+ ),
58
+ ] = False,
59
+ write_mode: Annotated[
60
+ bool,
61
+ typer.Option(
62
+ "--write",
63
+ help="Apply sorting changes to files. Without this flag, runs in check-only mode to preview changes.",
64
+ ),
65
+ ] = False,
66
+ skip_expressions: Annotated[
67
+ str,
68
+ typer.Option(
69
+ "--skip-expressions",
70
+ help="Template syntax markers that indicate code blocks to skip sorting. Comma-separated list (e.g., '{{,{%,<%'). Prevents sorting dynamic template expressions.",
71
+ ),
72
+ ] = ",".join(SKIP_EXPRESSIONS),
73
+ quiet: Annotated[
74
+ bool,
75
+ typer.Option(
76
+ "--quiet",
77
+ "-q",
78
+ help="Minimal output mode. Only displays errors and warnings.",
79
+ ),
80
+ ] = False,
81
+ verbosity: Annotated[
82
+ int,
83
+ typer.Option(
84
+ "--verbose",
85
+ "-v",
86
+ count=True,
87
+ help="Increase output detail level. Use -v for changes, -vv for file processing, -vvv for debug info.",
88
+ ),
89
+ ] = 0,
90
+ custom_colors: Annotated[
91
+ str,
92
+ typer.Option(
93
+ "--custom-colors",
94
+ help=(
95
+ "Comma-separated list of custom color names from your Tailwind config "
96
+ "(e.g., 'brand,company,accent'). These colors will be recognized and sorted "
97
+ "alongside standard Tailwind colors. WARNING: Classes using these custom colors "
98
+ "will sort differently if you run tailwhip again without providing the same "
99
+ "--custom-colors values. Omitting previously-used custom colors will cause those "
100
+ "utilities to be treated as non-color classes and sorted into different positions."
101
+ ),
102
+ ),
103
+ ] = "",
104
+ ) -> None:
105
+ """Sort Tailwind CSS classes in HTML and CSS files."""
106
+ console = Console(quiet=quiet, theme=CONSOLE_THEME)
107
+ skip_expressions = [
108
+ expr.strip() for expr in skip_expressions.split(",") if expr.strip()
109
+ ]
110
+ custom_colors_list = {
111
+ color.strip() for color in custom_colors.split(",") if color.strip()
112
+ }
113
+
114
+ config = Config(
115
+ console=console,
116
+ paths=paths,
117
+ write=write_mode,
118
+ skip_expressions=skip_expressions,
119
+ verbosity=verbosity + 1,
120
+ custom_colors=custom_colors_list,
121
+ )
122
+
123
+ console.print("")
124
+
125
+ start_time = time.time()
126
+ targets = find_files(config=config)
127
+ found_any, skipped, changed = apply_changes(targets=targets, config=config)
128
+ duration = time.time() - start_time
129
+
130
+ if not found_any:
131
+ config.console.print("[red]Error: No files found[/red]")
132
+ sys.exit(1)
133
+
134
+ if config.verbosity < VERBOSITY_LOUD:
135
+ console.print(
136
+ "\nUse [important] -v [/important] (show unchanged files) or [important] -vv [/important] (show diff preview) for more detail."
137
+ )
138
+
139
+ if not config.write:
140
+ console.print(
141
+ "\n:warning: Dry Run. No files were actually written. "
142
+ "Use [important] --write [/important] to write changes."
143
+ )
144
+
145
+ console.print(
146
+ f"⏱ Completed in [bold]{duration:.3f}s[/bold] for {changed} files. [dim]({skipped} skipped)[/dim]",
147
+ highlight=False,
148
+ )
tailwhip/constants.py ADDED
@@ -0,0 +1,311 @@
1
+ """Constants used by tailwhip."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from rich.theme import Theme
8
+
9
+ # File glob(s) to process
10
+ GLOBS = ["**/*.html", "**/*.css"]
11
+
12
+ # Skip if a template expression appears inside the class attribute
13
+ # List of strings that indicate template expressions to skip
14
+ SKIP_EXPRESSIONS = ["{{", "{%", "<%"]
15
+
16
+ # Recognize Tailwind variants (order doesn't matter; we sort lexicographically)
17
+ # e.g. "sm:hover:focus:text-blue-500" => variants ["focus", "hover", "sm"]
18
+ VARIANT_SEP = ":"
19
+
20
+ # A lightweight utility ranking derived from common Tailwind "best practice" ordering.
21
+ # This is not exhaustive, but it's practical. Unknown utilities fall to the end.
22
+ #
23
+ # SORTING MECHANISM: ------------------
24
+ #
25
+ # Each regex pattern in this list defines a GROUP. Groups are matched in order (top
26
+ # to bottom). When a utility class is encountered, it's tested against each pattern
27
+ # until a match is found. The matched group's index becomes the utility's rank (lower
28
+ # index = sorts earlier).
29
+ #
30
+ # WHY MULTIPLE PATTERNS IN ONE GROUP?
31
+ #
32
+ # Some logical categories (e.g., "Layout & display") contain multiple related patterns.
33
+ # These are kept as separate regex entries rather than one mega-pattern because:
34
+ #
35
+ # 1. Readability: Easier to see what utilities belong together
36
+ # 2. Maintainability: Simpler to add/remove/modify individual patterns
37
+ # 3. Order control: You can fine-tune the sort order within a category
38
+ # (e.g., "flex" before "grid" before "table")
39
+ #
40
+ # WHY SINGLE PATTERNS?
41
+ #
42
+ # Single patterns are used when:
43
+ #
44
+ # - The utility is standalone or unique (e.g., "container")
45
+ # - It needs precise positioning in the sort order
46
+ # - It's a catch-all for a prefix (e.g., "text-" matches text-sm, text-lg, etc.)
47
+ #
48
+ # WITHIN-GROUP SORTING:
49
+ #
50
+ # Classes matching the SAME group index are then sorted:
51
+ #
52
+ # 1. Non-color utilities before color utilities
53
+ # 2. Alphabetically by the full class name
54
+ #
55
+ # Example: "flex", "flex-col", "flex-row" all match r"(flex|inline-flex)" and sort alphabetically.
56
+ #
57
+ GROUPS = [
58
+ #
59
+ # 0) Components (Tailwind "components" layer comes before utilities)
60
+ r"(^| )(container)( |$)",
61
+ #
62
+ # 1) Layout & display
63
+ r"(hidden|visible|invisible)",
64
+ r"(z)-",
65
+ r"(block|inline-block|inline|flow-root|contents)",
66
+ r"(flex|inline-flex)",
67
+ r"(grid|inline-grid)",
68
+ r"(table|inline-table)",
69
+ r"(box-border|box-content|box-decoration-(slice|clone))",
70
+ r"(isolate|isolation-auto)",
71
+ r"(static|fixed|absolute|relative|sticky)",
72
+ r"(inset|top|right|bottom|left|z)-",
73
+ r"(float|clear)-(left|right|none|start|end)",
74
+ #
75
+ # floats/clears
76
+ r"(columns)-",
77
+ #
78
+ # CSS multi-column
79
+ r"(break)-(inside|before|after)-",
80
+ #
81
+ # page/column breaks
82
+ r"(overflow|overscroll)-",
83
+ #
84
+ # scrolling/overflow
85
+ r"(scroll|scroll-m|scroll-p|scroll-smooth)",
86
+ #
87
+ # scroll behavior & margins
88
+ r"(snap)-",
89
+ #
90
+ # 2) Flexbox & Grid
91
+ r"(order|grow|shrink|basis)-",
92
+ r"(grid-cols|grid-rows|grid-flow|auto-cols|auto-rows)-",
93
+ r"(col-(span|start|end)|row-(span|start|end))-",
94
+ r"(gap|space-[xy])-",
95
+ #
96
+ # 3) Alignment & distribution (box alignment)
97
+ r"(place|content|items|self|justify|align)-",
98
+ #
99
+ # 4) Sizing
100
+ r"(size)-",
101
+ r"(w)-",
102
+ r"(min-w)-",
103
+ r"(max-w)-",
104
+ r"(h)-",
105
+ r"(min-h)-",
106
+ r"(max-h)-",
107
+ r"(aspect)-",
108
+ #
109
+ # 5) Spacing (x,y,t,r,b,l)
110
+ r"(m)-",
111
+ r"(mx)-",
112
+ r"(my)-",
113
+ r"(mt)-",
114
+ r"(mr)-",
115
+ r"(mb)-",
116
+ r"(ml)-",
117
+ r"(p)-",
118
+ r"(px)-",
119
+ r"(py)-",
120
+ r"(pt)-",
121
+ r"(pr)-",
122
+ r"(pb)-",
123
+ r"(pl)-",
124
+ #
125
+ # 6) Typography
126
+ r"(font)-",
127
+ r"(text-(balance|pretty|wrap))",
128
+ r"(text)-",
129
+ r"(tracking)-",
130
+ r"(leading)-",
131
+ r"(list)-",
132
+ r"(indent)-",
133
+ r"(placeholder)-",
134
+ r"(whitespace|break|hyphens)-",
135
+ r"(tab-size)-",
136
+ r"(line-clamp)-",
137
+ #
138
+ # official plugin utility
139
+ r"(antialiased|subpixel-antialiased|truncate)",
140
+ #
141
+ # single-token type utils
142
+ r"(tabular-nums|slashed-zero|lining-nums|oldstyle-nums|proportional-nums|ordinal)",
143
+ #
144
+ # content-['…'] utilities
145
+ r"(content)-",
146
+ #
147
+ # 7) Backgrounds
148
+ # includes bg-clip, -origin, -repeat, -size, -position, -attachment, colors
149
+ r"(bg)-",
150
+ #
151
+ # gradients
152
+ r"(from)-",
153
+ r"(to)-",
154
+ r"(via)-",
155
+ #
156
+ #
157
+ # 8) Borders & outlines & rings & divide
158
+ r"(rounded)-",
159
+ r"(border)-",
160
+ r"(divide)-",
161
+ r"(ring)-",
162
+ r"(outline)-",
163
+ #
164
+ # 9) Effects
165
+ r"(shadow)-",
166
+ r"(opacity)-",
167
+ r"(mix-blend)-",
168
+ r"(backdrop|filter|blur|brightness|contrast|grayscale|hue-rotate|invert|saturate|sepia|drop-shadow)-",
169
+ #
170
+ # 10) Transforms
171
+ r"(transform|scale|rotate|translate|skew|origin)-",
172
+ #
173
+ # 11) Transitions & animation
174
+ r"(transition|duration|ease|delay|animate)-",
175
+ #
176
+ # 12) Interactivity & misc
177
+ r"(cursor|select|pointer-events|resize|appearance|accent|caret|touch)-",
178
+ r"(object)-",
179
+ #
180
+ # object-fit/position
181
+ r"(outline-none)",
182
+ #
183
+ # single token
184
+ r"(stroke|fill)-",
185
+ #
186
+ # SVG
187
+ #
188
+ # 13) Accessibility helpers (kept near the end so utility groups dominate)
189
+ r"(sr-only|not-sr-only)",
190
+ #
191
+ # 14) Group/peer & attribute base selectors (as raw classes, not variants)
192
+ r"(peer|group|aria-|data-)(:|$)",
193
+ ]
194
+
195
+ VARIANT_PREFIX_ORDER = [
196
+ #
197
+ # Container queries (most specific, come first)
198
+ r"@container:",
199
+ r"@\w+:",
200
+ #
201
+ # Direction & theme
202
+ r"(rtl|ltr):",
203
+ r"dark:",
204
+ #
205
+ # Media-like variants
206
+ r"(portrait|landscape|print):",
207
+ r"(motion-safe|motion-reduce):",
208
+ r"(contrast-more|contrast-less):",
209
+ #
210
+ # Container & size queries (if using v4 container queries)
211
+ r"(supports-\[.*\]|supports-.*:)",
212
+ #
213
+ # supports: variant patterns
214
+ r"(size-.*:|cq-.*:)",
215
+ #
216
+ # Breakpoints (mobile-first)
217
+ r"min-\[.*\]:",
218
+ r"max-\[.*\]:",
219
+ r"sm:",
220
+ r"md:",
221
+ r"lg:",
222
+ r"xl:",
223
+ r"2xl:",
224
+ #
225
+ # Structural/state
226
+ r"(first):",
227
+ r"(last):",
228
+ r"(only):",
229
+ r"(odd):",
230
+ r"(even):",
231
+ r"(first-of-type|last-of-type|only-of-type):",
232
+ r"(visited|target|open|empty):",
233
+ r"(enabled|disabled|read-only|required|optional):",
234
+ r"(checked|indeterminate|default):",
235
+ r"(valid|invalid):",
236
+ r"(in-range|out-of-range):",
237
+ r"(placeholder-shown):",
238
+ r"(autofill):",
239
+ r"(focus-within):",
240
+ r"(hover|focus|focus-visible|focus-within|active):",
241
+ r"(pressed|selected):",
242
+ r"(current):",
243
+ r"(aria-.*:|data-.*:)",
244
+ #
245
+ # attribute variants
246
+ r"(group(-\[.*\])?:.*:|group-.*:)",
247
+ #
248
+ # group/compound (e.g. group-hover:)
249
+ r"(peer(-\[.*\])?:.*:|peer-.*:)",
250
+ #
251
+ # peer/compound (e.g. peer-checked:)
252
+ r"(marker:|selection:|file:|placeholder:|backdrop:|first-letter:|first-line:)",
253
+ ]
254
+
255
+
256
+ # Standard Tailwind color names (extracted from the palette)
257
+ TAILWIND_COLORS = {
258
+ "transparent",
259
+ "current",
260
+ "black",
261
+ "white",
262
+ "amber",
263
+ "blue",
264
+ "blue-gray",
265
+ "cool-gray",
266
+ "cyan",
267
+ "emerald",
268
+ "fuchsia",
269
+ "gray",
270
+ "green",
271
+ "indigo",
272
+ "light-blue",
273
+ "lime",
274
+ "orange",
275
+ "pink",
276
+ "purple",
277
+ "red",
278
+ "rose",
279
+ "sky",
280
+ "teal",
281
+ "true-gray",
282
+ "violet",
283
+ "warm-gray",
284
+ "yellow",
285
+ }
286
+
287
+
288
+ GROUP_PATTERNS = [re.compile("^" + g) for g in GROUPS]
289
+
290
+ CLASS_ATTR_RE = re.compile(
291
+ r"""(?P<full>\bclass\s*=\s*(?P<quote>["'])(?P<val>.*?)(?P=quote))""",
292
+ re.IGNORECASE | re.DOTALL,
293
+ )
294
+
295
+ APPLY_RE = re.compile(
296
+ r"""@apply\s+(?P<classes>[^;]+);""",
297
+ re.MULTILINE,
298
+ )
299
+
300
+ CONSOLE_THEME = Theme(
301
+ {
302
+ "important": "white on deep_pink4",
303
+ "highlight": "yellow1",
304
+ "filename": "white",
305
+ "bold": "sky_blue1",
306
+ }
307
+ )
308
+
309
+ VERBOSITY_NONE = 0
310
+ VERBOSITY_LOUD = 2
311
+ VERBOSITY_ALL = 3
tailwhip/datatypes.py ADDED
@@ -0,0 +1,23 @@
1
+ """Datatypes for tailwhip."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
10
+
11
+ from rich.console import Console
12
+
13
+
14
+ @dataclass
15
+ class Config:
16
+ """Argument parser config."""
17
+
18
+ console: Console
19
+ paths: list[Path]
20
+ write: bool
21
+ skip_expressions: list[str]
22
+ verbosity: int
23
+ custom_colors: set[str]
tailwhip/files.py ADDED
@@ -0,0 +1,170 @@
1
+ """File-related utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import difflib
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rich.padding import Padding
10
+ from rich.syntax import Syntax
11
+
12
+ from tailwhip.constants import GLOBS, VERBOSITY_ALL, VERBOSITY_LOUD, VERBOSITY_NONE
13
+ from tailwhip.process import process_text
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Generator, Iterable
17
+
18
+ from tailwhip.datatypes import Config
19
+
20
+
21
+ def find_files(*, config: Config) -> Generator[Path]: # noqa: C901
22
+ """Find all HTML/CSS files from a list of paths.
23
+
24
+ Processes multiple path inputs (files, directories, or glob patterns), expands each
25
+ one, and returns a deduplicated generator of all matching HTML/CSS files. If no paths
26
+ are provided, defaults to scanning the current directory.
27
+
28
+ Args:
29
+ config: Argument configuration object
30
+
31
+ Yields:
32
+ Path objects for all HTML/CSS files found (deduplicated and sorted)
33
+
34
+ Examples:
35
+ >>> config.paths = [Path('templates/'), Path('styles/')]
36
+ >>> list(find_files(config))
37
+ [PosixPath('templates/index.html'), PosixPath('styles/main.css')]
38
+
39
+ >>> config.paths = [Path('src/**/*.html'), Path('components/**/*.html')]
40
+ >>> list(find_files(config))
41
+ [PosixPath('src/pages/home.html'), PosixPath('components/nav.html')]
42
+
43
+ >>> config.paths = []
44
+ >>> list(find_files(config)) # Defaults to current directory
45
+ [PosixPath('./index.html'), PosixPath('./styles.css')]
46
+
47
+ >>> config.paths = [Path('index.html'), Path('about.html'), Path('index.html')]
48
+ >>> list(find_files(config))
49
+ [PosixPath('index.html'), PosixPath('about.html')] # Deduplication
50
+
51
+ >>> config.paths = [Path('*.html'), Path('static/*.css')]
52
+ >>> list(find_files(config))
53
+ [PosixPath('home.html'), PosixPath('static/app.css')]
54
+
55
+ """
56
+ seen = set()
57
+
58
+ for entry in config.paths:
59
+ p = Path(entry)
60
+
61
+ # Case 1: Existing directory - search within it using configured glob patterns
62
+ # Example: entry="src/" with config.globs=["**/*.html"]
63
+ # → yields: src/index.html, src/pages/about.html
64
+ if p.is_dir():
65
+ for pattern in GLOBS:
66
+ for match in p.glob(pattern):
67
+ resolved = match.resolve()
68
+ if resolved not in seen:
69
+ seen.add(resolved)
70
+ yield resolved
71
+
72
+ # Case 2: Existing file - yield it directly without pattern matching
73
+ # Example: entry="index.html" (file exists)
74
+ # → yields: /absolute/path/to/index.html
75
+ elif p.is_file():
76
+ resolved = p.resolve()
77
+ if resolved not in seen:
78
+ seen.add(resolved)
79
+ yield resolved
80
+
81
+ # Case 3: Non-existent path - interpret as recursive glob pattern from cwd
82
+ # Example: entry="**/*.css" or entry="templates/*.html" (path doesn't exist as literal)
83
+ # → yields: styles/main.css, components/button.css (all matching files from cwd)
84
+ else:
85
+ for match in Path().rglob(str(entry)):
86
+ if match.is_file():
87
+ resolved = match.resolve()
88
+ if resolved not in seen:
89
+ seen.add(resolved)
90
+ yield resolved
91
+
92
+
93
+ def get_diff(path: Path, old_text: str, new_text: str) -> Syntax:
94
+ """Show a nice diff using Rich."""
95
+ # Create a text diff between old and new text
96
+ diff = difflib.unified_diff(
97
+ old_text.splitlines(),
98
+ new_text.splitlines(),
99
+ fromfile=str(path),
100
+ tofile=str(path),
101
+ n=1,
102
+ )
103
+
104
+ # Remove blank lines
105
+ code = "\n".join([line.strip() for line in diff])
106
+ return Syntax(code, "diff", theme="ansi_dark", background_color="default")
107
+
108
+
109
+ def apply_changes(*, targets: Iterable[Path], config: Config) -> tuple[bool, int, int]:
110
+ """Process target files and apply Tailwind class sorting changes.
111
+
112
+ Reads each file, processes it to sort Tailwind classes (skipping any with
113
+ template syntax), and either writes the changes back or reports what would be
114
+ changed. Provides detailed diff output at higher verbosity levels.
115
+
116
+ Args:
117
+ targets: List of Path objects for files to process
118
+ config: Argument configuration object
119
+
120
+ Returns:
121
+ A tuple of (skipped_count, changed_count) where:
122
+ - skipped_count: Number of files with no changes needed
123
+ - changed_count: Number of files that were modified or would be modified
124
+
125
+ Examples:
126
+ >>> apply_changes(targets=[Path('index.html')], config=config)
127
+ (0, 1) # 0 skipped, 1 changed
128
+
129
+ >>> apply_changes(targets=[Path('a.html'), Path('b.html')], config=config)
130
+ (1, 1) # 1 skipped (no changes), 1 changed
131
+
132
+ """
133
+ skipped = 0
134
+ changed = 0
135
+ found_any = False
136
+
137
+ for f in targets:
138
+ found_any = True
139
+ old_text = f.read_text(encoding="utf-8")
140
+ new_text = process_text(old_text, config)
141
+
142
+ # Skip files that don't need changes
143
+ if old_text == new_text:
144
+ if config.verbosity >= VERBOSITY_LOUD:
145
+ config.console.print(
146
+ f"[grey30]Already sorted {f}[/grey30]", highlight=False
147
+ )
148
+ skipped += 1
149
+ continue
150
+
151
+ changed += 1
152
+
153
+ # Write changes if in write mode, otherwise just report
154
+ if config.write:
155
+ f.write_text(new_text, encoding="utf-8")
156
+
157
+ # No report if verbosity is low
158
+ if config.verbosity == VERBOSITY_NONE:
159
+ continue
160
+
161
+ if config.write:
162
+ config.console.print(f"[dim]Updated[/dim] [filename]{f}[/filename]")
163
+ else:
164
+ config.console.print(f"[dim]Would update[/dim] [filename]{f}[/filename]")
165
+
166
+ if config.verbosity >= VERBOSITY_ALL:
167
+ diff = get_diff(f, old_text, new_text)
168
+ config.console.print(Padding(diff, (1, 0, 1, 4)))
169
+
170
+ return found_any, skipped, changed