Compare commits

...
2 Commits
Author SHA1 Message Date
luxick a8b1d41398 Update nn
Exclude hidden files per default. Use `-a` to include them
2026-08-24 10:54:58 +02:00
luxick 2dc08013b7 Update nn
Direct input into Y/N questions
2026-08-24 10:52:48 +02:00
+52 -11
View File
@@ -6,6 +6,8 @@ import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import termios
import tty
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -24,6 +26,9 @@ TELEGRAM_PATTERN = re.compile(
NORMALIZED = re.compile(r'^\d{4}-\d{2}-\d{2} .+') NORMALIZED = re.compile(r'^\d{4}-\d{2}-\d{2} .+')
# Windows hidden-file attribute, mirrored from the stat module.
FILE_ATTRIBUTE_HIDDEN = 0x2
STATUS_RENAME = 'rename' STATUS_RENAME = 'rename'
STATUS_ALREADY = 'skip (already normalized)' STATUS_ALREADY = 'skip (already normalized)'
STATUS_NO_MATCH = 'skip (no rule matched)' STATUS_NO_MATCH = 'skip (no rule matched)'
@@ -146,7 +151,19 @@ def get_date_for_file(filepath, parse_mode):
return None return None
def collect_files(paths): def is_hidden(filepath):
"""Check whether a file is hidden (dotfile, or hidden attribute on Windows)."""
if filepath.name.startswith('.'):
return True
try:
attrs = filepath.stat().st_file_attributes
except (AttributeError, OSError):
return False
return bool(attrs & FILE_ATTRIBUTE_HIDDEN)
def collect_files(paths, include_hidden=False):
"""Resolve the list of files to process.""" """Resolve the list of files to process."""
if not paths: if not paths:
paths = ['.'] paths = ['.']
@@ -155,8 +172,12 @@ def collect_files(paths):
for path in paths: for path in paths:
candidate = Path(path) candidate = Path(path)
if candidate.is_dir(): if candidate.is_dir():
files.extend(sorted(file for file in candidate.iterdir() if file.is_file())) files.extend(sorted(
file for file in candidate.iterdir()
if file.is_file() and (include_hidden or not is_hidden(file))
))
elif candidate.is_file(): elif candidate.is_file():
# Explicitly named files are always processed.
files.append(candidate) files.append(candidate)
return files return files
@@ -273,13 +294,23 @@ def apply_renames(plan):
def confirm(prompt='Apply renames? [y/n] '): def confirm(prompt='Apply renames? [y/n] '):
"""Ask the user for confirmation.""" """Ask the user for confirmation, reacting to a single keypress."""
while True: print(prompt, end='', flush=True)
answer = input(prompt).strip().lower() if not sys.stdin.isatty():
if answer == 'y': return sys.stdin.read(1).strip().lower() == 'y'
return True
if answer == 'n': fd = sys.stdin.fileno()
return False old_settings = termios.tcgetattr(fd)
try:
# cbreak (not raw) keeps Ctrl-C as SIGINT and newlines well behaved.
tty.setcbreak(fd)
while (key := sys.stdin.read(1).lower()) not in ('y', 'n', ''):
pass
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
print(key)
return key == 'y'
def main(): def main():
@@ -306,6 +337,12 @@ def main():
action='store_true', action='store_true',
help='Force numbering even for already-normalized files (used with -n).', help='Force numbering even for already-normalized files (used with -n).',
) )
parser.add_argument(
'-a',
'--all',
action='store_true',
help='Include hidden files when scanning directories.',
)
parser.add_argument( parser.add_argument(
'files', 'files',
nargs='*', nargs='*',
@@ -314,7 +351,7 @@ def main():
args = parser.parse_args() args = parser.parse_args()
files = collect_files(args.files) files = collect_files(args.files, args.all)
if not files: if not files:
print('No files found.') print('No files found.')
sys.exit(0) sys.exit(0)
@@ -337,4 +374,8 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
main() try:
main()
except KeyboardInterrupt:
print('\nAborted.')
sys.exit(1)