krita-cli 1.0.0__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.
@@ -0,0 +1,27 @@
1
+ """File operation CLI commands: open-file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from typer import Context
9
+
10
+ from krita_cli import _shared
11
+ from krita_client import KritaError
12
+
13
+ app = typer.Typer()
14
+
15
+
16
+ @app.command("open-file")
17
+ def open_file(
18
+ ctx: Context,
19
+ path: Annotated[str, typer.Argument(help="Full file path to open")],
20
+ ) -> None:
21
+ """Open an existing file in Krita (.kra, .png, .jpg, etc)."""
22
+ try:
23
+ client = _shared._get_client(ctx)
24
+ result = client.open_file(path=path)
25
+ _shared._format_result(result)
26
+ except KritaError as exc:
27
+ _shared._handle_error(exc)
@@ -0,0 +1,32 @@
1
+ """Health check CLI command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from typer import Context
8
+
9
+ from krita_cli import _shared
10
+ from krita_client import KritaError
11
+
12
+ console = Console()
13
+
14
+ app = typer.Typer()
15
+
16
+
17
+ @app.command()
18
+ def health(ctx: Context) -> None:
19
+ """Check if Krita is running with the MCP plugin active."""
20
+ try:
21
+ client = _shared._get_client(ctx)
22
+ result = client.health()
23
+ plugin = result.get("plugin", "unknown")
24
+ status = result.get("status", "unknown")
25
+ version = result.get("version", "unknown")
26
+ protocol_version = result.get("protocol_version", "unknown")
27
+ console.print(
28
+ f"[green]Krita is running.[/green] Plugin: [bold]{plugin}[/bold] "
29
+ f"v{version} (protocol v{protocol_version}, {status})"
30
+ )
31
+ except KritaError as exc:
32
+ _shared._handle_error(exc)
@@ -0,0 +1,64 @@
1
+ """History command CLI subcommand."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Annotated, Any, cast
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+ from typer import Context
12
+
13
+ from krita_cli import _shared
14
+ from krita_client import KritaError
15
+
16
+ console = Console()
17
+
18
+ app = typer.Typer()
19
+
20
+
21
+ @app.command()
22
+ def history(
23
+ ctx: Context,
24
+ limit: Annotated[int, typer.Option("--limit", "-n", help="Number of history entries to show")] = 20,
25
+ as_json: Annotated[bool, typer.Option("--json", "-j", help="Output as raw JSON")] = False, # noqa: FBT002
26
+ ) -> None:
27
+ """View recent command execution history."""
28
+ try:
29
+ client = _shared._get_client(ctx)
30
+ result = client.get_command_history(limit=limit)
31
+
32
+ if as_json:
33
+ console.print(json.dumps(result, indent=2, default=str))
34
+ return
35
+
36
+ records_raw = result.get("history", [])
37
+ if not isinstance(records_raw, list):
38
+ records_raw = []
39
+ records = cast("list[dict[str, Any]]", records_raw)
40
+
41
+ if not records:
42
+ console.print("[dim]No command history recorded.[/dim]")
43
+ return
44
+
45
+ table = Table(title=f"Command History ({len(records)} entries)")
46
+ table.add_column("#", style="dim")
47
+ table.add_column("Action")
48
+ table.add_column("Status")
49
+ table.add_column("Duration (ms)")
50
+ table.add_column("Error", style="red")
51
+
52
+ for i, rec in enumerate(records, 1):
53
+ table.add_row(
54
+ str(i),
55
+ rec.get("action", "?"),
56
+ rec.get("status", "?"),
57
+ f"{rec.get('duration_ms', 0):.1f}",
58
+ rec.get("error", "") or "",
59
+ )
60
+
61
+ console.print(table)
62
+
63
+ except KritaError as exc:
64
+ _shared._handle_error(exc)
@@ -0,0 +1,46 @@
1
+ """Canvas introspection CLI commands: canvas-info, current-color, current-brush."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from typer import Context
7
+
8
+ from krita_cli import _shared
9
+ from krita_client import KritaError
10
+
11
+ console = _shared.console
12
+
13
+ app = typer.Typer()
14
+
15
+
16
+ @app.command("canvas-info")
17
+ def canvas_info(ctx: Context) -> None:
18
+ """Get information about the current canvas."""
19
+ try:
20
+ client = _shared._get_client(ctx)
21
+ result = client.get_canvas_info()
22
+ _shared._format_result(result)
23
+ except KritaError as exc:
24
+ _shared._handle_error(exc)
25
+
26
+
27
+ @app.command("current-color")
28
+ def current_color(ctx: Context) -> None:
29
+ """Get the current foreground and background colors."""
30
+ try:
31
+ client = _shared._get_client(ctx)
32
+ result = client.get_current_color()
33
+ _shared._format_result(result)
34
+ except KritaError as exc:
35
+ _shared._handle_error(exc)
36
+
37
+
38
+ @app.command("current-brush")
39
+ def current_brush(ctx: Context) -> None:
40
+ """Get the current brush preset and properties."""
41
+ try:
42
+ client = _shared._get_client(ctx)
43
+ result = client.get_current_brush()
44
+ _shared._format_result(result)
45
+ except KritaError as exc:
46
+ _shared._handle_error(exc)
@@ -0,0 +1,112 @@
1
+ """Layer-related CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from typer import Context
9
+
10
+ from krita_cli import _shared
11
+ from krita_client import KritaError
12
+
13
+ app = typer.Typer()
14
+
15
+
16
+ @app.command("list")
17
+ def list_layers(ctx: Context) -> None:
18
+ """List all layers in the current document."""
19
+ try:
20
+ client = _shared._get_client(ctx)
21
+ result = client.list_layers()
22
+ _shared._format_result(result)
23
+ except KritaError as exc:
24
+ _shared._handle_error(exc)
25
+
26
+
27
+ @app.command("create")
28
+ def create_layer(
29
+ ctx: Context,
30
+ name: Annotated[str, typer.Option("--name", "-n", help="Layer name")] = "New Layer",
31
+ layer_type: Annotated[str, typer.Option("--type", "-t", help="Layer type")] = "paintlayer",
32
+ ) -> None:
33
+ """Create a new layer in the current document."""
34
+ try:
35
+ client = _shared._get_client(ctx)
36
+ result = client.create_layer(name=name, layer_type=layer_type)
37
+ _shared._format_result(result)
38
+ except KritaError as exc:
39
+ _shared._handle_error(exc)
40
+
41
+
42
+ @app.command("select")
43
+ def select_layer(
44
+ ctx: Context,
45
+ name: Annotated[str, typer.Argument(help="Layer name to select")],
46
+ ) -> None:
47
+ """Select a layer by name."""
48
+ try:
49
+ client = _shared._get_client(ctx)
50
+ result = client.select_layer(name=name)
51
+ _shared._format_result(result)
52
+ except KritaError as exc:
53
+ _shared._handle_error(exc)
54
+
55
+
56
+ @app.command("delete")
57
+ def delete_layer(
58
+ ctx: Context,
59
+ name: Annotated[str, typer.Argument(help="Layer name to delete")],
60
+ ) -> None:
61
+ """Delete a layer by name."""
62
+ try:
63
+ client = _shared._get_client(ctx)
64
+ result = client.delete_layer(name=name)
65
+ _shared._format_result(result)
66
+ except KritaError as exc:
67
+ _shared._handle_error(exc)
68
+
69
+
70
+ @app.command("rename")
71
+ def rename_layer(
72
+ ctx: Context,
73
+ old_name: Annotated[str, typer.Argument(help="Current layer name")],
74
+ new_name: Annotated[str, typer.Argument(help="New layer name")],
75
+ ) -> None:
76
+ """Rename a layer."""
77
+ try:
78
+ client = _shared._get_client(ctx)
79
+ result = client.rename_layer(old_name=old_name, new_name=new_name)
80
+ _shared._format_result(result)
81
+ except KritaError as exc:
82
+ _shared._handle_error(exc)
83
+
84
+
85
+ @app.command("set-opacity")
86
+ def set_layer_opacity(
87
+ ctx: Context,
88
+ name: Annotated[str, typer.Argument(help="Layer name")],
89
+ opacity: Annotated[float, typer.Option("--opacity", "-o", help="Opacity (0.0 to 1.0)")] = 1.0,
90
+ ) -> None:
91
+ """Set the opacity of a layer."""
92
+ try:
93
+ client = _shared._get_client(ctx)
94
+ result = client.set_layer_opacity(name=name, opacity=opacity)
95
+ _shared._format_result(result)
96
+ except KritaError as exc:
97
+ _shared._handle_error(exc)
98
+
99
+
100
+ @app.command("set-visibility")
101
+ def set_layer_visibility(
102
+ ctx: Context,
103
+ name: Annotated[str, typer.Argument(help="Layer name")],
104
+ visible: Annotated[bool, typer.Option("--visible/--hidden", "-v/-h", help="Layer visibility")] = True,
105
+ ) -> None:
106
+ """Toggle the visibility of a layer."""
107
+ try:
108
+ client = _shared._get_client(ctx)
109
+ result = client.set_layer_visibility(name=name, visible=visible)
110
+ _shared._format_result(result)
111
+ except KritaError as exc:
112
+ _shared._handle_error(exc)
@@ -0,0 +1,33 @@
1
+ """Navigation-related CLI commands: undo, redo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from typer import Context
7
+
8
+ from krita_cli import _shared
9
+ from krita_client import KritaError
10
+
11
+ app = typer.Typer()
12
+
13
+
14
+ @app.command()
15
+ def undo(ctx: Context) -> None:
16
+ """Undo the last action."""
17
+ try:
18
+ client = _shared._get_client(ctx)
19
+ result = client.undo()
20
+ _shared._format_result(result)
21
+ except KritaError as exc:
22
+ _shared._handle_error(exc)
23
+
24
+
25
+ @app.command()
26
+ def redo(ctx: Context) -> None:
27
+ """Redo the last undone action."""
28
+ try:
29
+ client = _shared._get_client(ctx)
30
+ result = client.redo()
31
+ _shared._format_result(result)
32
+ except KritaError as exc:
33
+ _shared._handle_error(exc)
@@ -0,0 +1,126 @@
1
+ """Replay command CLI subcommand."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import pathlib
7
+ import time
8
+ from typing import Annotated, Any, cast
9
+
10
+ import typer
11
+ from rich.console import Console
12
+ from typer import Context
13
+
14
+ from krita_cli import _shared
15
+ from krita_client import KritaError
16
+
17
+ console = Console()
18
+
19
+ app = typer.Typer()
20
+
21
+
22
+ def _validate_records(records: list[dict]) -> None:
23
+ """Validate that all records have required 'action' field."""
24
+ for i, rec in enumerate(records, 1):
25
+ action = rec.get("action")
26
+ if not action:
27
+ console.print(f" [red]#{i}: Missing 'action' field[/red]")
28
+ raise typer.Exit(code=1)
29
+
30
+
31
+ def _execute_replay(
32
+ ctx: Context,
33
+ records: list[dict],
34
+ speed: float,
35
+ ) -> None:
36
+ """Execute recorded commands via the client."""
37
+ client = _shared._get_client(ctx)
38
+ ok_count = 0
39
+ err_count = 0
40
+
41
+ for i, rec in enumerate(records, 1):
42
+ action = rec.get("action")
43
+ params = rec.get("params", {})
44
+
45
+ if not action:
46
+ console.print(f" [red]#{i}: Skipping — missing 'action'[/red]")
47
+ err_count += 1
48
+ continue
49
+
50
+ if speed > 0:
51
+ original_ms = rec.get("duration_ms", 0) / 1000.0
52
+ delay = original_ms / speed
53
+ if delay > 0:
54
+ time.sleep(delay)
55
+
56
+ try:
57
+ result = client.send_command(action, params)
58
+ if "error" in result:
59
+ err_raw = result.get("error", {})
60
+ if isinstance(err_raw, dict):
61
+ err_dict = cast("dict[str, Any]", err_raw)
62
+ err_msg = str(err_dict.get("message", str(err_raw)))
63
+ else:
64
+ err_msg = str(err_raw)
65
+ console.print(f" [red]#{i}: {action} — {err_msg}[/red]")
66
+ err_count += 1
67
+ else:
68
+ ok_count += 1
69
+ except KritaError as exc:
70
+ console.print(f" [red]#{i}: {action} — {exc}[/red]")
71
+ err_count += 1
72
+
73
+ console.print(f"\nReplay complete: {ok_count} succeeded, {err_count} failed out of {len(records)}")
74
+
75
+
76
+ @app.command()
77
+ def replay(
78
+ ctx: Context,
79
+ file: Annotated[
80
+ str,
81
+ typer.Argument(help="JSON file with recorded commands"),
82
+ ],
83
+ speed: Annotated[
84
+ float,
85
+ typer.Option("--speed", "-s", help="Playback speed (0.0=instant, 1.0=original)"),
86
+ ] = 1.0,
87
+ dry_run: Annotated[ # noqa: FBT002
88
+ bool,
89
+ typer.Option("--dry-run", "-n", help="Validate without executing"),
90
+ ] = False,
91
+ ) -> None:
92
+ """Replay commands from a JSON file.
93
+
94
+ The JSON file should contain an array of command records from
95
+ 'krita history --json', each with an "action" and optional "params" key.
96
+
97
+ Example:
98
+ krita history --json > history.json
99
+ krita replay history.json
100
+ krita replay history.json --speed 0.5
101
+ krita replay history.json --dry-run
102
+ """
103
+ path = pathlib.Path(file)
104
+ try:
105
+ records = json.loads(path.read_text())
106
+ except json.JSONDecodeError as exc:
107
+ console.print(f"[red]Error:[/red] Invalid JSON in {path}: {exc}")
108
+ raise typer.Exit(code=1) from exc
109
+ except OSError as exc:
110
+ console.print(f"[red]Error:[/red] Cannot read {path}: {exc}")
111
+ raise typer.Exit(code=1) from exc
112
+
113
+ if not isinstance(records, list):
114
+ console.print("[red]Error:[/red] JSON file must contain an array.")
115
+ raise typer.Exit(code=1)
116
+
117
+ if dry_run:
118
+ console.print(f"[dim]Dry run: validating {len(records)} records[/dim]")
119
+ _validate_records(records)
120
+ console.print(f"[green]All {len(records)} records are valid.[/green]")
121
+ return
122
+
123
+ try:
124
+ _execute_replay(ctx, records, speed)
125
+ except KritaError as exc:
126
+ _shared._handle_error(exc)
@@ -0,0 +1,39 @@
1
+ """Rollback CLI command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from typer import Context
10
+
11
+ from krita_cli import _shared
12
+ from krita_client import KritaError
13
+
14
+ console = Console()
15
+
16
+ app = typer.Typer()
17
+
18
+
19
+ @app.command()
20
+ def rollback(
21
+ ctx: Context,
22
+ batch_id: Annotated[str, typer.Argument(help="ID of the batch to roll back")],
23
+ ) -> None:
24
+ """Roll back a previously executed batch.
25
+
26
+ This restores the canvas to its state before the specified batch was executed.
27
+ Note that snapshots are kept in memory and are lost if the Krita plugin is restarted.
28
+ """
29
+ try:
30
+ client = _shared._get_client(ctx)
31
+ result = client.rollback(batch_id=batch_id)
32
+ status = result.get("status", "unknown")
33
+ msg = result.get("message", "")
34
+ if status == "ok":
35
+ console.print(f"[green]Rollback successful: {msg}[/green]")
36
+ else:
37
+ console.print(f"[red]Rollback failed: {msg}[/red]")
38
+ except KritaError as exc:
39
+ _shared._handle_error(exc)