2# +==== BEGIN polyguard =================+
21# CREATION DATE: 21-03-2026
22# LAST Modified: 19:47:9 21-03-2026
24# A module that provides a set of swearwords to listen to when filtering while allowing to toggle on and off different languages.
26# COPYRIGHT: (c) Henry Letellier
27# PURPOSE: This is the code in charge of simulating a pseudo tty for the ones that start the module without piping content into it.
28# TTY-friendly CLI wrapper for PolyGuard.
30# Provides a simple entrypoint that accepts a path to the DB and a test
31# word to check. The CLI is intentionally minimal to be easy to extend.
33# +==== END polyguard =================+
36from typing
import Iterable, Optional, List
40from .
import constants
as POLY_CONST
41from .polyguard
import PolyGuard
45 """Iterate over lines from a stream, yielding non-empty lines.
47 Strips trailing newlines from each line before yielding.
50 stream: Iterable of strings (e.g., from sys.stdin).
53 str: Non-empty lines with trailing newlines removed.
56 text = line.rstrip(
"\n")
62 """Resolve a user-provided token to a `POLY_CONST.Langs` member.
64 Accepts either the enum value ('en_uk') or the enum name ('EN_UK').
65 Returns None if no match is found.
70 norm = token.lower().replace(
"-",
"_")
72 return POLY_CONST.Langs(norm)
75 name = token.upper().replace(
"-",
"_")
76 if hasattr(POLY_CONST.Langs, name):
77 return getattr(POLY_CONST.Langs, name)
82 """Simple CLI class for PolyGuard interactive and batch usage.
84 Kept at module level to allow import and unit testing.
91 """Check a single word and print the result.
94 word: The word to check.
97 int: Always returns 0.
99 is_swear = self.
guard.is_a_swearword(word)
100 status = POLY_CONST.STATUS_OK
102 status = POLY_CONST.STATUS_BLOCKED
103 print(f
"{word}: {status}")
107 """Read lines from stdin and check each for profanity.
109 Outputs 'BLOCKED' or 'OK' for each line.
112 int: Always returns 0.
115 result = self.
guard.is_a_swearword(line)
117 print(POLY_CONST.STATUS_BLOCKED)
119 print(POLY_CONST.STATUS_OK)
123 """Toggle logging output on or off.
125 Parses 'on' or 'off' from the second token and updates the guard and
126 sqlite handler's logging flags.
129 tokens: Command tokens where tokens[1] should be 'on' or 'off'.
132 bool: Always returns True (command processed).
135 print(f
"Usage: {POLY_CONST.COMMAND_TOKEN}log <on|off>")
137 val = tokens[1].lower()
in (
"on",
"1",
"true",
"yes")
139 if self.
guard.sqlite
is not None:
140 self.
guard.sqlite.log = val
142 print(
"Logging enabled")
144 print(
"Logging disabled")
148 """Enable or disable a specific language in the PolyGuard configuration.
150 Parses language code from tokens[1] and 'on'/'off' status from tokens[2],
151 then updates the guard's language configuration.
154 tokens: Command tokens where tokens[1] is language and tokens[2]
158 bool: Always returns True (command processed).
161 print(f
"Usage: {POLY_CONST.COMMAND_TOKEN}langopt <lang> <on|off>")
163 lang_token = tokens[1]
166 if lang_enum
is None:
167 print(f
"Unknown language: {lang_token}")
170 val = tokens[2].lower()
in (
"on",
"1",
"true",
"yes")
172 setattr(self.
guard.default_choice, lang_enum.value, val)
176 print(f
"Set {lang_enum.value} {status}")
177 except Exception
as exc:
178 print(f
"Failed to set language option: {exc}")
182 """List all available languages in the database with word counts.
184 Displays languages compactly on lines of ~80 characters, marking
185 enabled languages with [enabled] tag.
188 tokens: Command tokens (not used).
191 bool: Always returns True (command processed).
193 if not self.
guard.ensure_connection():
194 print(
"No DB connection available")
198 mapping = self.
guard.sqlite.list_languages()
199 except Exception
as exc:
200 print(f
"Failed to query DB: {exc}")
204 print(
"No languages found in DB")
209 for lang_code, count
in sorted(mapping.items()):
212 enabled = bool(getattr(self.
guard.default_choice, lang_code))
218 entries.append(f
"{lang_code}({count}){mark}")
227 if cur_len + add_len > max_width
and line:
228 print(
", ".join(line))
240 print(
", ".join(line))
245 """Display the current enabled/disabled status of all languages.
247 Lists each language code with its current 'on' or 'off' status as
248 determined by the guard's default language configuration.
251 tokens: Command tokens (not used).
254 bool: Always returns True (command processed).
256 for lang
in POLY_CONST.Langs:
258 enabled = bool(getattr(self.
guard.default_choice, lang.value))
264 print(f
"{lang.value}: {status}")
268 """Check a word, optionally for a specific language.
270 Supports multi-word phrases. If the final token matches a known language,
271 it is treated as the language filter. Returns the match status and any
272 matched words from the database (if a specific language is given).
275 tokens: Command tokens where tokens[1:] is the word/phrase and
276 tokens[-1] may be a language code.
279 bool: Always returns True (command processed).
282 print(f
"Usage: {POLY_CONST.COMMAND_TOKEN}word <word> [<lang>]")
289 possible_lang = tokens[-1]
291 if lang_enum
is not None:
292 word =
" ".join(tokens[1:-1])
294 word =
" ".join(tokens[1:])
300 if lang_enum
is not None:
301 if not self.
guard.ensure_connection():
302 print(
"No DB connection available")
305 found = self.
guard.sqlite.has_word(lang_enum, word)
307 print(POLY_CONST.STATUS_BLOCKED)
309 print(POLY_CONST.STATUS_OK)
310 except Exception
as exc:
311 print(f
"DB query failed: {exc}")
315 result = self.
guard.is_a_swearword(word)
317 print(POLY_CONST.STATUS_BLOCKED)
319 print(POLY_CONST.STATUS_OK)
323 """Run the interactive CLI REPL loop.
325 Accepts user input for words to check or colon-prefixed commands.
326 Supports batch processing from stdin and interactive commands like
327 ':log', ':langopt', ':langs', ':langstatus', ':word', ':help', ':man'.
330 int: Always returns 0 on normal exit or interrupt.
332 print(POLY_CONST.POLY_BOOT_MSG)
337 text = input(POLY_CONST.POLY_PROMPT)
347 if not text.startswith(POLY_CONST.COMMAND_TOKEN):
349 result = self.
guard.is_a_swearword(text)
351 print(POLY_CONST.STATUS_BLOCKED)
353 print(POLY_CONST.STATUS_OK)
357 cmd_body = text[POLY_CONST.COMMAND_TOKEN_LENGTH:]
358 tokens = cmd_body.split()
361 base = tokens[0].lower()
363 if base
in (
"quit",
"exit"):
367 print(POLY_CONST.POLY_HELP_TEXT)
371 print(POLY_CONST.POLY_MAN_TEXT)
376 POLY_CONST.DB_PATH_FMT.format(
377 path=self.
guard.db_path
387 if base ==
"langopt":
395 if base
in (
"langstatus",
"langsstatus"):
403 print(f
"Unknown command: {base}")
405 except KeyboardInterrupt:
411def main(argv: Optional[List[str]] =
None) -> int:
412 """CLI entrypoint for PolyGuard.
414 Dispatches to single-word check, stdin batch processing, or interactive
415 REPL based on arguments and whether stdin is a TTY.
418 argv: Optional list of command-line arguments. If None, uses sys.argv.
421 int: Exit code (0 for success, non-zero for error).
423 parser = argparse.ArgumentParser(
424 prog=
"polyguard", description=
"Run PolyGuard checks from the TTY or stdin")
429 help=
"Path to SQLite DB (overrides package default)"
434 help=
"A single word to test for being a swearword"
437 args = parser.parse_args(argv)
439 conf = POLY_CONST.LangConfig()
441 guard =
PolyGuard(conf, db_path=args.db_path)
446 return cli.run_single(args.word)
449 if not sys.stdin.isatty():
450 return cli.run_stdin()
456if __name__ ==
"__main__":
457 raise SystemExit(main())
bool cmd_langs(self, list[str] tokens)
bool cmd_langopt(self, list[str] tokens)
bool cmd_word(self, list[str] tokens)
__init__(self, PolyGuard guard)
bool cmd_log(self, list[str] tokens)
int run_single(self, str word)
bool cmd_langstatus(self, list[str] tokens)
_iter_input_lines(Iterable[str] stream)
int main(Optional[List[str]] argv=None)