diff --git a/.gitignore b/.gitignore index 68bc17f..c663839 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +uv.lock diff --git a/pyproject.toml b/pyproject.toml index 47ad13f..1326037 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dependencies = [ "numpy>=1.24.0", "typer[all]>=0.9.0", "rich>=13.0.0", + "textual>=8.2.5", ] [project.optional-dependencies] diff --git a/tui.py b/tui.py index 5517197..7f142f2 100644 --- a/tui.py +++ b/tui.py @@ -11,7 +11,7 @@ from textual.widgets import ( Header, Footer, Static, Button, Input, Label, Select, Switch, TextArea, ProgressBar, TabbedContent, TabPane, - DirectoryTree, Markdown, DataTable, Log + DirectoryTree, Markdown, DataTable, RichLog ) from textual.binding import Binding from textual.screen import Screen @@ -304,8 +304,8 @@ class StegosaurusApp(App): def compose(self) -> ComposeResult: yield Header() - yield Container( - Vertical( + with Container(id="main-container"): + yield Vertical( Static(DINO_ASCII, id="dino-display"), Button("🔐 Encode", id="btn-encode", classes="menu-button"), Button("🔓 Decode", id="btn-decode", classes="menu-button"), @@ -315,20 +315,19 @@ def compose(self) -> ComposeResult: Static("[dim]───────────────────[/dim]", classes="subtitle"), Static(f"[dim]{TAGLINES[0]}[/dim]", classes="subtitle"), id="sidebar", - ), - Vertical( - TabbedContent( - TabPane("Home", self._home_content(), id="tab-home"), - TabPane("Encode", self._encode_content(), id="tab-encode"), - TabPane("Decode", self._decode_content(), id="tab-decode"), - TabPane("Analyze", self._analyze_content(), id="tab-analyze"), - TabPane("Inject", self._inject_content(), id="tab-inject"), - id="main-tabs", - ), - id="content", - ), - id="main-container", - ) + ) + with Vertical(id="content"): + with TabbedContent(id="main-tabs"): + with TabPane("Home", id="tab-home"): + yield from self._home_content() + with TabPane("Encode", id="tab-encode"): + yield from self._encode_content() + with TabPane("Decode", id="tab-decode"): + yield from self._decode_content() + with TabPane("Analyze", id="tab-analyze"): + yield from self._analyze_content() + with TabPane("Inject", id="tab-inject"): + yield from self._inject_content() yield Footer() def _home_content(self) -> ComposeResult: @@ -392,7 +391,7 @@ def _encode_content(self) -> ComposeResult: id="encode-data", ), ) - yield Log(id="encode-log", highlight=True, markup=True) + yield RichLog(id="encode-log", highlight=True, markup=True) def _decode_content(self) -> ComposeResult: yield Static("[cyan]═══ DECODE DATA ═══[/cyan]", classes="panel-title") @@ -430,7 +429,7 @@ def _decode_content(self) -> ComposeResult: ), ), ) - yield Log(id="decode-log", highlight=True, markup=True) + yield RichLog(id="decode-log", highlight=True, markup=True) def _analyze_content(self) -> ComposeResult: yield Static("[magenta]═══ ANALYZE IMAGE ═══[/magenta]", classes="panel-title") @@ -440,7 +439,7 @@ def _analyze_content(self) -> ComposeResult: Button("🔍 Analyze", id="btn-do-analyze"), ), DataTable(id="analyze-results"), - Log(id="analyze-log", highlight=True, markup=True), + RichLog(id="analyze-log", highlight=True, markup=True), ) def _inject_content(self) -> ComposeResult: @@ -506,9 +505,9 @@ def on_button_pressed(self, event: Button.Pressed) -> None: def do_encode(self) -> None: """Perform encoding""" - log = self.query_one("#encode-log", Log) + log = self.query_one("#encode-log", RichLog) log.clear() - log.write_line("[cyan]Starting encode...[/cyan]") + log.write("[cyan]Starting encode...[/cyan]") try: input_path = self.query_one("#encode-input", Input).value @@ -521,46 +520,46 @@ def do_encode(self) -> None: password = self.query_one("#encode-password", Input).value if not input_path: - log.write_line("[red]Error: No input image specified[/red]") + log.write("[red]Error: No input image specified[/red]") return if not text: - log.write_line("[red]Error: No data to encode[/red]") + log.write("[red]Error: No data to encode[/red]") return - log.write_line(f"[dim]Loading image: {input_path}[/dim]") + log.write(f"[dim]Loading image: {input_path}[/dim]") image = Image.open(input_path) - log.write_line(f"[green]✓[/green] Image loaded: {image.width}x{image.height}") + log.write(f"[green]✓[/green] Image loaded: {image.width}x{image.height}") config = create_config(channels=channels, bits=bits, compress=use_compress) capacity = calculate_capacity(image, config) - log.write_line(f"[dim]Capacity: {capacity['human']}[/dim]") + log.write(f"[dim]Capacity: {capacity['human']}[/dim]") payload = text.encode('utf-8') if use_encrypt and password: - log.write_line("[dim]Encrypting payload...[/dim]") + log.write("[dim]Encrypting payload...[/dim]") payload = encrypt(payload, password) - log.write_line("[green]✓[/green] Payload encrypted") + log.write("[green]✓[/green] Payload encrypted") if not output_path: output_path = generate_injection_filename("chatgpt_decoder", channels) - log.write_line("[dim]Encoding...[/dim]") + log.write("[dim]Encoding...[/dim]") result = encode(image, payload, config, output_path) - log.write_line(f"[green]✓ SUCCESS![/green]") - log.write_line(f"[green]Output: {output_path}[/green]") + log.write(f"[green]✓ SUCCESS![/green]") + log.write(f"[green]Output: {output_path}[/green]") self.notify(f"Encoded to {output_path}", title="Success!") except Exception as e: - log.write_line(f"[red]Error: {e}[/red]") + log.write(f"[red]Error: {e}[/red]") self.notify(str(e), title="Error", severity="error") def do_decode(self) -> None: """Perform decoding""" - log = self.query_one("#decode-log", Log) + log = self.query_one("#decode-log", RichLog) log.clear() - log.write_line("[cyan]Starting decode...[/cyan]") + log.write("[cyan]Starting decode...[/cyan]") try: input_path = self.query_one("#decode-input", Input).value @@ -571,40 +570,40 @@ def do_decode(self) -> None: password = self.query_one("#decode-password", Input).value if not input_path: - log.write_line("[red]Error: No input image specified[/red]") + log.write("[red]Error: No input image specified[/red]") return - log.write_line(f"[dim]Loading image: {input_path}[/dim]") + log.write(f"[dim]Loading image: {input_path}[/dim]") image = Image.open(input_path) - log.write_line(f"[green]✓[/green] Image loaded") + log.write(f"[green]✓[/green] Image loaded") config = create_config(channels=channels, bits=bits, compress=use_compress) - log.write_line("[dim]Decoding...[/dim]") + log.write("[dim]Decoding...[/dim]") data = decode(image, config) if use_encrypt and password: - log.write_line("[dim]Decrypting...[/dim]") + log.write("[dim]Decrypting...[/dim]") data = decrypt(data, password) - log.write_line("[green]✓[/green] Decrypted") + log.write("[green]✓[/green] Decrypted") try: text = data.decode('utf-8') self.query_one("#decode-output", TextArea).text = text - log.write_line(f"[green]✓ SUCCESS! Extracted {len(data)} bytes[/green]") + log.write(f"[green]✓ SUCCESS! Extracted {len(data)} bytes[/green]") except UnicodeDecodeError: self.query_one("#decode-output", TextArea).text = data.hex() - log.write_line("[yellow]Binary data - showing hex[/yellow]") + log.write("[yellow]Binary data - showing hex[/yellow]") self.notify("Decode complete!", title="Success!") except Exception as e: - log.write_line(f"[red]Error: {e}[/red]") + log.write(f"[red]Error: {e}[/red]") self.notify(str(e), title="Error", severity="error") def do_analyze(self) -> None: """Perform analysis""" - log = self.query_one("#analyze-log", Log) + log = self.query_one("#analyze-log", RichLog) table = self.query_one("#analyze-results", DataTable) log.clear() table.clear() @@ -612,14 +611,14 @@ def do_analyze(self) -> None: try: input_path = self.query_one("#analyze-input", Input).value if not input_path: - log.write_line("[red]Error: No input image specified[/red]") + log.write("[red]Error: No input image specified[/red]") return - log.write_line(f"[dim]Analyzing: {input_path}[/dim]") + log.write(f"[dim]Analyzing: {input_path}[/dim]") image = Image.open(input_path) analysis = analyze_image(image) - log.write_line(f"[green]✓[/green] {image.width}x{image.height} - {analysis['total_pixels']:,} pixels") + log.write(f"[green]✓[/green] {image.width}x{image.height} - {analysis['total_pixels']:,} pixels") for ch_name, ch_data in analysis['channels'].items(): lsb = ch_data['lsb_ratio'] @@ -643,14 +642,14 @@ def do_analyze(self) -> None: max_ind = max(ch['lsb_ratio']['chi_square_indicator'] for ch in analysis['channels'].values()) if max_ind > 0.3: - log.write_line("[red]⚠ HIGH PROBABILITY OF HIDDEN DATA[/red]") + log.write("[red]⚠ HIGH PROBABILITY OF HIDDEN DATA[/red]") elif max_ind > 0.1: - log.write_line("[yellow]⚠ Possible hidden data[/yellow]") + log.write("[yellow]⚠ Possible hidden data[/yellow]") else: - log.write_line("[green]✓ No obvious steganographic indicators[/green]") + log.write("[green]✓ No obvious steganographic indicators[/green]") except Exception as e: - log.write_line(f"[red]Error: {e}[/red]") + log.write(f"[red]Error: {e}[/red]") def gen_filename(self) -> None: """Generate injection filename"""