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.
- krita_cli/__init__.py +5 -0
- krita_cli/_shared.py +94 -0
- krita_cli/app.py +149 -0
- krita_cli/cli.py +59 -0
- krita_cli/commands/__init__.py +1 -0
- krita_cli/commands/batch.py +86 -0
- krita_cli/commands/brush.py +58 -0
- krita_cli/commands/call.py +49 -0
- krita_cli/commands/canvas.py +77 -0
- krita_cli/commands/color.py +42 -0
- krita_cli/commands/config.py +48 -0
- krita_cli/commands/file_ops.py +27 -0
- krita_cli/commands/health.py +32 -0
- krita_cli/commands/history_cmd.py +64 -0
- krita_cli/commands/introspect.py +46 -0
- krita_cli/commands/layers.py +112 -0
- krita_cli/commands/navigation.py +33 -0
- krita_cli/commands/replay.py +126 -0
- krita_cli/commands/rollback.py +39 -0
- krita_cli/commands/selection.py +364 -0
- krita_cli/commands/stroke.py +101 -0
- krita_cli/config_cmd.py +68 -0
- krita_cli/history.py +198 -0
- krita_cli-1.0.0.dist-info/METADATA +103 -0
- krita_cli-1.0.0.dist-info/RECORD +35 -0
- krita_cli-1.0.0.dist-info/WHEEL +4 -0
- krita_cli-1.0.0.dist-info/entry_points.txt +2 -0
- krita_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- krita_client/__init__.py +68 -0
- krita_client/client.py +690 -0
- krita_client/config.py +53 -0
- krita_client/models.py +507 -0
- krita_client/schema.py +109 -0
- krita_mcp/__init__.py +1 -0
- krita_mcp/server.py +1022 -0
krita_cli/__init__.py
ADDED
krita_cli/_shared.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Shared CLI utilities used by app.py and command modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from typer import Context
|
|
11
|
+
|
|
12
|
+
from krita_cli.config_cmd import load_config
|
|
13
|
+
from krita_client import (
|
|
14
|
+
ClientConfig,
|
|
15
|
+
ErrorCode,
|
|
16
|
+
KritaClient,
|
|
17
|
+
KritaError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CLIState:
|
|
24
|
+
"""Shared state passed through Typer context."""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self.url: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _handle_error(exc: KritaError) -> None:
|
|
31
|
+
"""Display a Krita client error and exit."""
|
|
32
|
+
console.print(f"[red]Error:[/red] {exc.message}")
|
|
33
|
+
if exc.code:
|
|
34
|
+
console.print(f"[dim]Code: {exc.code}[/dim]")
|
|
35
|
+
|
|
36
|
+
if exc.recoverable:
|
|
37
|
+
if exc.code == ErrorCode.NO_ACTIVE_DOCUMENT:
|
|
38
|
+
console.print("[green]Hint: Open a document or create a new canvas first.[/green]")
|
|
39
|
+
elif exc.code == ErrorCode.INVALID_PARAMETERS:
|
|
40
|
+
console.print("[green]Hint: Check your input values are within allowed ranges.[/green]")
|
|
41
|
+
elif exc.code == ErrorCode.LAYER_NOT_FOUND:
|
|
42
|
+
console.print("[green]Hint: Ensure there is an active paint layer in your document.[/green]")
|
|
43
|
+
elif exc.code == ErrorCode.PLUGIN_UNREACHABLE:
|
|
44
|
+
console.print("[green]Hint: Make sure Krita is running with the MCP plugin enabled.[/green]")
|
|
45
|
+
elif exc.code == ErrorCode.COMMAND_TIMEOUT:
|
|
46
|
+
console.print("[green]Hint: The operation took too long. Try again or check Krita status.[/green]")
|
|
47
|
+
elif exc.code == ErrorCode.BRUSH_NOT_FOUND:
|
|
48
|
+
console.print("[green]Hint: Check the brush preset name or list available brushes first.[/green]")
|
|
49
|
+
elif exc.code == ErrorCode.FILE_NOT_FOUND:
|
|
50
|
+
console.print("[green]Hint: Verify the file path exists and is accessible.[/green]")
|
|
51
|
+
else:
|
|
52
|
+
console.print(
|
|
53
|
+
"[green]Hint: This error appears to be recoverable. Adjust your request and try again.[/green]"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
raise typer.Exit(code=1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@contextmanager
|
|
60
|
+
def _handle_errors() -> Any:
|
|
61
|
+
"""Context manager to handle Krita errors gracefully."""
|
|
62
|
+
try:
|
|
63
|
+
yield
|
|
64
|
+
except KritaError as exc:
|
|
65
|
+
_handle_error(exc)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _get_client(ctx: Context) -> KritaClient:
|
|
69
|
+
"""Create a Krita client from the Typer context."""
|
|
70
|
+
state: CLIState = ctx.obj or CLIState()
|
|
71
|
+
if state.url is not None:
|
|
72
|
+
config = ClientConfig(url=state.url)
|
|
73
|
+
else:
|
|
74
|
+
plugin_config = load_config()
|
|
75
|
+
port = plugin_config.get("port", 5678)
|
|
76
|
+
config = ClientConfig(url=f"http://localhost:{port}")
|
|
77
|
+
return KritaClient(config)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _format_result(result: dict[str, object]) -> None:
|
|
81
|
+
"""Display a command result in a readable format."""
|
|
82
|
+
if "error" in result:
|
|
83
|
+
console.print(f"[red]Error:[/red] {result['error']}")
|
|
84
|
+
raise typer.Exit(code=1)
|
|
85
|
+
for key, value in result.items(): # pragma: no cover - display-only
|
|
86
|
+
if key == "status":
|
|
87
|
+
continue
|
|
88
|
+
console.print(f"[dim]{key}:[/dim] {value}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _print_result(result: dict[str, object], message: str) -> None:
|
|
92
|
+
"""Display a command result with a custom message."""
|
|
93
|
+
console.print(f"[green]{message}[/green]")
|
|
94
|
+
_format_result(result)
|
krita_cli/app.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Krita CLI — Main application composition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from typer import Context
|
|
10
|
+
|
|
11
|
+
from krita_cli import _shared
|
|
12
|
+
from krita_cli.commands import config as _config
|
|
13
|
+
from krita_client import (
|
|
14
|
+
KritaCommandError,
|
|
15
|
+
KritaConnectionError,
|
|
16
|
+
KritaValidationError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
name="krita",
|
|
21
|
+
help="CLI for programmatic painting in Krita via the MCP plugin.",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
add_completion=False,
|
|
24
|
+
)
|
|
25
|
+
app.add_typer(_config.app, name="config")
|
|
26
|
+
console = _shared.console
|
|
27
|
+
|
|
28
|
+
# Re-export shared utilities for backward compatibility
|
|
29
|
+
CLIState = _shared.CLIState
|
|
30
|
+
_handle_error = _shared._handle_error
|
|
31
|
+
_get_client = _shared._get_client
|
|
32
|
+
_format_result = _shared._format_result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# -- Global options -----------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.callback()
|
|
39
|
+
def callback(
|
|
40
|
+
ctx: Context,
|
|
41
|
+
url: Annotated[
|
|
42
|
+
str | None,
|
|
43
|
+
typer.Option("--url", "-u", help="Krita plugin URL (overrides KRITA_URL env var)"),
|
|
44
|
+
] = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Krita CLI — programmatic painting in Krita."""
|
|
47
|
+
ctx.obj = CLIState()
|
|
48
|
+
ctx.obj.url = url
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# -- Register sub-apps --------------------------------------------------------
|
|
52
|
+
# Lazy imports to avoid circular dependency between app.py and command modules.
|
|
53
|
+
from krita_cli.commands import (
|
|
54
|
+
batch as _batch,
|
|
55
|
+
)
|
|
56
|
+
from krita_cli.commands import (
|
|
57
|
+
brush as _brush,
|
|
58
|
+
)
|
|
59
|
+
from krita_cli.commands import (
|
|
60
|
+
call as _call,
|
|
61
|
+
)
|
|
62
|
+
from krita_cli.commands import (
|
|
63
|
+
canvas as _canvas,
|
|
64
|
+
)
|
|
65
|
+
from krita_cli.commands import (
|
|
66
|
+
color as _color,
|
|
67
|
+
)
|
|
68
|
+
from krita_cli.commands import (
|
|
69
|
+
file_ops as _file_ops,
|
|
70
|
+
)
|
|
71
|
+
from krita_cli.commands import (
|
|
72
|
+
health as _health,
|
|
73
|
+
)
|
|
74
|
+
from krita_cli.commands import (
|
|
75
|
+
history_cmd as _history_cmd,
|
|
76
|
+
)
|
|
77
|
+
from krita_cli.commands import (
|
|
78
|
+
introspect as _introspect,
|
|
79
|
+
)
|
|
80
|
+
from krita_cli.commands import (
|
|
81
|
+
layers as _layers,
|
|
82
|
+
)
|
|
83
|
+
from krita_cli.commands import (
|
|
84
|
+
navigation as _navigation,
|
|
85
|
+
)
|
|
86
|
+
from krita_cli.commands import (
|
|
87
|
+
replay as _replay,
|
|
88
|
+
)
|
|
89
|
+
from krita_cli.commands import (
|
|
90
|
+
rollback as _rollback,
|
|
91
|
+
)
|
|
92
|
+
from krita_cli.commands import (
|
|
93
|
+
selection as _selection,
|
|
94
|
+
)
|
|
95
|
+
from krita_cli.commands import (
|
|
96
|
+
stroke as _stroke,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
app.add_typer(_canvas.app, name="canvas")
|
|
100
|
+
app.add_typer(_color.app, name="color")
|
|
101
|
+
app.add_typer(_brush.app, name="brush")
|
|
102
|
+
app.add_typer(_stroke.app, name="stroke")
|
|
103
|
+
app.add_typer(_navigation.app, name="navigation")
|
|
104
|
+
app.add_typer(_file_ops.app, name="file")
|
|
105
|
+
app.add_typer(_health.app)
|
|
106
|
+
app.add_typer(_call.app)
|
|
107
|
+
app.add_typer(_history_cmd.app)
|
|
108
|
+
app.add_typer(_batch.app)
|
|
109
|
+
app.add_typer(_rollback.app)
|
|
110
|
+
app.add_typer(_introspect.app, name="introspect")
|
|
111
|
+
app.add_typer(_layers.app, name="layers")
|
|
112
|
+
app.add_typer(_replay.app)
|
|
113
|
+
app.add_typer(_selection.app, name="selection")
|
|
114
|
+
|
|
115
|
+
# Re-export command functions for backward compatibility (cli.py shim)
|
|
116
|
+
new_canvas = _canvas.new_canvas
|
|
117
|
+
get_canvas = _canvas.get_canvas
|
|
118
|
+
save = _canvas.save
|
|
119
|
+
clear = _canvas.clear
|
|
120
|
+
set_color = _color.set_color
|
|
121
|
+
get_color_at = _color.get_color_at
|
|
122
|
+
set_brush = _brush.set_brush
|
|
123
|
+
list_brushes = _brush.list_brushes
|
|
124
|
+
stroke = _stroke.stroke
|
|
125
|
+
fill = _stroke.fill
|
|
126
|
+
draw_shape = _stroke.draw_shape
|
|
127
|
+
undo = _navigation.undo
|
|
128
|
+
redo = _navigation.redo
|
|
129
|
+
open_file = _file_ops.open_file
|
|
130
|
+
health = _health.health
|
|
131
|
+
call = _call.call
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# -- Entry point --------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main() -> None: # pragma: no cover
|
|
138
|
+
"""Main entry point for the CLI."""
|
|
139
|
+
try:
|
|
140
|
+
app()
|
|
141
|
+
except (KritaConnectionError, KritaCommandError, KritaValidationError) as exc:
|
|
142
|
+
_handle_error(exc)
|
|
143
|
+
except KeyboardInterrupt:
|
|
144
|
+
console.print("\n[dim]Interrupted.[/dim]")
|
|
145
|
+
sys.exit(130)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__": # pragma: no cover
|
|
149
|
+
main()
|
krita_cli/cli.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Krita CLI — Compatibility shim.
|
|
2
|
+
|
|
3
|
+
All functionality has moved to krita_cli.app.
|
|
4
|
+
This module re-exports everything for backward compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from krita_cli.app import (
|
|
8
|
+
CLIState,
|
|
9
|
+
_format_result,
|
|
10
|
+
_get_client,
|
|
11
|
+
_handle_error,
|
|
12
|
+
app,
|
|
13
|
+
call,
|
|
14
|
+
callback,
|
|
15
|
+
clear,
|
|
16
|
+
console,
|
|
17
|
+
draw_shape,
|
|
18
|
+
fill,
|
|
19
|
+
get_canvas,
|
|
20
|
+
get_color_at,
|
|
21
|
+
health,
|
|
22
|
+
list_brushes,
|
|
23
|
+
main,
|
|
24
|
+
new_canvas,
|
|
25
|
+
open_file,
|
|
26
|
+
redo,
|
|
27
|
+
save,
|
|
28
|
+
set_brush,
|
|
29
|
+
set_color,
|
|
30
|
+
stroke,
|
|
31
|
+
undo,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"CLIState",
|
|
36
|
+
"_format_result",
|
|
37
|
+
"_get_client",
|
|
38
|
+
"_handle_error",
|
|
39
|
+
"app",
|
|
40
|
+
"call",
|
|
41
|
+
"callback",
|
|
42
|
+
"clear",
|
|
43
|
+
"console",
|
|
44
|
+
"draw_shape",
|
|
45
|
+
"fill",
|
|
46
|
+
"get_canvas",
|
|
47
|
+
"get_color_at",
|
|
48
|
+
"health",
|
|
49
|
+
"list_brushes",
|
|
50
|
+
"main",
|
|
51
|
+
"new_canvas",
|
|
52
|
+
"open_file",
|
|
53
|
+
"redo",
|
|
54
|
+
"save",
|
|
55
|
+
"set_brush",
|
|
56
|
+
"set_color",
|
|
57
|
+
"stroke",
|
|
58
|
+
"undo",
|
|
59
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command sub-applications."""
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Batch command CLI command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated, Any, cast
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
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 batch(
|
|
23
|
+
ctx: Context,
|
|
24
|
+
file: Annotated[Path, typer.Argument(help="JSON file with batch commands")],
|
|
25
|
+
stop_on_error: Annotated[bool, typer.Option("--stop-on-error", help="Stop on first error")] = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Execute multiple commands from a JSON file.
|
|
28
|
+
|
|
29
|
+
The JSON file should contain an array of command objects, each with
|
|
30
|
+
an "action" and optional "params" key.
|
|
31
|
+
|
|
32
|
+
Example JSON file:
|
|
33
|
+
[
|
|
34
|
+
{"action": "set_color", "params": {"color": "#ff0000"}},
|
|
35
|
+
{"action": "stroke", "params": {"points": [[0, 0], [100, 100]]}},
|
|
36
|
+
{"action": "fill", "params": {"x": 200, "y": 200, "radius": 30}}
|
|
37
|
+
]
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
commands = json.loads(file.read_text())
|
|
41
|
+
except json.JSONDecodeError as exc:
|
|
42
|
+
console.print(f"[red]Error:[/red] Invalid JSON in {file}: {exc}")
|
|
43
|
+
raise typer.Exit(code=1) from exc
|
|
44
|
+
except OSError as exc:
|
|
45
|
+
console.print(f"[red]Error:[/red] Cannot read {file}: {exc}")
|
|
46
|
+
raise typer.Exit(code=1) from exc
|
|
47
|
+
|
|
48
|
+
if not isinstance(commands, list):
|
|
49
|
+
console.print("[red]Error:[/red] JSON file must contain an array of commands.")
|
|
50
|
+
raise typer.Exit(code=1)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
client = _shared._get_client(ctx)
|
|
54
|
+
result = client.batch_execute(commands, stop_on_error=stop_on_error)
|
|
55
|
+
status = result.get("status", "unknown")
|
|
56
|
+
results_raw = result.get("results", [])
|
|
57
|
+
if not isinstance(results_raw, list):
|
|
58
|
+
results_raw = []
|
|
59
|
+
results = cast("list[dict[str, Any]]", results_raw)
|
|
60
|
+
|
|
61
|
+
ok = sum(1 for r in results if isinstance(r, dict) and r.get("status") == "ok")
|
|
62
|
+
errs = sum(1 for r in results if isinstance(r, dict) and r.get("status") == "error")
|
|
63
|
+
count = result.get("count", 0)
|
|
64
|
+
color = "green" if status == "ok" else "red" if status == "error" else "yellow"
|
|
65
|
+
console.print(f"[{color}]Batch: {status}[/{color}]")
|
|
66
|
+
console.print(f" {ok} succeeded, {errs} failed out of {count}")
|
|
67
|
+
batch_id = result.get("batch_id")
|
|
68
|
+
if batch_id:
|
|
69
|
+
console.print(f" Batch ID: [dim]{batch_id}[/dim]")
|
|
70
|
+
if errs > 0:
|
|
71
|
+
console.print("[red]Errors:[/red]")
|
|
72
|
+
for r in results:
|
|
73
|
+
if isinstance(r, dict) and r.get("status") == "error":
|
|
74
|
+
err_msg = r.get("error")
|
|
75
|
+
if not err_msg:
|
|
76
|
+
result_data = r.get("result", {})
|
|
77
|
+
if isinstance(result_data, dict) and "error" in result_data:
|
|
78
|
+
err_info = result_data["error"]
|
|
79
|
+
err_msg = (
|
|
80
|
+
err_info.get("message", str(err_info)) if isinstance(err_info, dict) else str(err_info)
|
|
81
|
+
)
|
|
82
|
+
if not err_msg:
|
|
83
|
+
err_msg = "unknown"
|
|
84
|
+
console.print(f" [red]✗ {r.get('action', 'unknown')}: {err_msg}[/red]")
|
|
85
|
+
except KritaError as exc:
|
|
86
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Brush-related CLI commands: set-brush, list-brushes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from typer import Context
|
|
11
|
+
|
|
12
|
+
from krita_cli import _shared
|
|
13
|
+
from krita_client import KritaError
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
app = typer.Typer()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("set-brush")
|
|
21
|
+
def set_brush(
|
|
22
|
+
ctx: Context,
|
|
23
|
+
preset: Annotated[str | None, typer.Option("--preset", "-p", help="Brush preset name")] = None,
|
|
24
|
+
size: Annotated[int | None, typer.Option("--size", "-s", help="Brush size in pixels")] = None,
|
|
25
|
+
opacity: Annotated[float | None, typer.Option("--opacity", "-o", help="Brush opacity (0.0-1.0)")] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Set brush preset and properties."""
|
|
28
|
+
try:
|
|
29
|
+
client = _shared._get_client(ctx)
|
|
30
|
+
result = client.set_brush(preset=preset, size=size, opacity=opacity)
|
|
31
|
+
_shared._format_result(result)
|
|
32
|
+
except KritaError as exc:
|
|
33
|
+
_shared._handle_error(exc)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command("list-brushes")
|
|
37
|
+
def list_brushes(
|
|
38
|
+
ctx: Context,
|
|
39
|
+
filter: Annotated[str, typer.Option("--filter", "-f", help="Filter by name")] = "", # noqa: A002
|
|
40
|
+
limit: Annotated[int, typer.Option("--limit", "-l", help="Maximum number to return")] = 20,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""List available brush presets."""
|
|
43
|
+
try:
|
|
44
|
+
client = _shared._get_client(ctx)
|
|
45
|
+
result = client.list_brushes(filter=filter, limit=limit)
|
|
46
|
+
brushes_raw = result.get("brushes", [])
|
|
47
|
+
brushes = list(brushes_raw) if isinstance(brushes_raw, list) else []
|
|
48
|
+
if not brushes:
|
|
49
|
+
console.print("No brushes found matching filter.")
|
|
50
|
+
return
|
|
51
|
+
table = Table(title=f"Available Brushes ({len(brushes)})")
|
|
52
|
+
table.add_column("#", style="dim")
|
|
53
|
+
table.add_column("Name")
|
|
54
|
+
for i, name in enumerate(brushes, 1):
|
|
55
|
+
table.add_row(str(i), name)
|
|
56
|
+
console.print(table)
|
|
57
|
+
except KritaError as exc:
|
|
58
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Raw command mode CLI command: call."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from typer import Context
|
|
11
|
+
|
|
12
|
+
from krita_cli import _shared
|
|
13
|
+
from krita_client import KritaError
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
app = typer.Typer()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def call(
|
|
22
|
+
ctx: Context,
|
|
23
|
+
action: Annotated[str, typer.Argument(help="Command action name")],
|
|
24
|
+
params_json: Annotated[str | None, typer.Argument(help="JSON params string")] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Send a raw command to the Krita plugin.
|
|
27
|
+
|
|
28
|
+
Useful for commands not yet exposed as subcommands, or for scripting.
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
Examples:
|
|
32
|
+
krita call new_canvas '{"width": 1920, "height": 1080}'
|
|
33
|
+
krita call set_color '{"color": "#ff0000"}'
|
|
34
|
+
krita call stroke '{"points": [[0,0],[100,100]]}'
|
|
35
|
+
"""
|
|
36
|
+
params: dict[str, object] = {}
|
|
37
|
+
if params_json:
|
|
38
|
+
try:
|
|
39
|
+
params = json.loads(params_json)
|
|
40
|
+
except json.JSONDecodeError as exc:
|
|
41
|
+
console.print(f"[red]Error:[/red] Invalid JSON: {exc}")
|
|
42
|
+
raise typer.Exit(code=1) from exc
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
client = _shared._get_client(ctx)
|
|
46
|
+
result = client.send_command(action, params)
|
|
47
|
+
console.print(json.dumps(result, indent=2, default=str))
|
|
48
|
+
except KritaError as exc:
|
|
49
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Canvas-related CLI commands: new-canvas, get-canvas, save, clear."""
|
|
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("new-canvas")
|
|
20
|
+
def new_canvas(
|
|
21
|
+
ctx: Context,
|
|
22
|
+
width: Annotated[int, typer.Option("--width", "-W", help="Canvas width in pixels")] = 800,
|
|
23
|
+
height: Annotated[int, typer.Option("--height", "-H", help="Canvas height in pixels")] = 600,
|
|
24
|
+
name: Annotated[str, typer.Option("--name", "-n", help="Document name")] = "New Canvas",
|
|
25
|
+
background: Annotated[str, typer.Option("--background", "-b", help="Background color (hex)")] = "#1a1a2e",
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Create a new canvas in Krita."""
|
|
28
|
+
try:
|
|
29
|
+
client = _shared._get_client(ctx)
|
|
30
|
+
result = client.new_canvas(width=width, height=height, name=name, background=background)
|
|
31
|
+
_shared._format_result(result)
|
|
32
|
+
except KritaError as exc:
|
|
33
|
+
_shared._handle_error(exc)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command("get-canvas")
|
|
37
|
+
def get_canvas(
|
|
38
|
+
ctx: Context,
|
|
39
|
+
filename: Annotated[str, typer.Option("--filename", "-f", help="Output filename")] = "canvas.png",
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Export the current canvas to a PNG file."""
|
|
42
|
+
console.print("[dim]Exporting canvas (this may take a while for large canvases)...[/dim]")
|
|
43
|
+
try:
|
|
44
|
+
client = _shared._get_client(ctx)
|
|
45
|
+
result = client.get_canvas(filename=filename)
|
|
46
|
+
_shared._format_result(result)
|
|
47
|
+
except KritaError as exc:
|
|
48
|
+
_shared._handle_error(exc)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command()
|
|
52
|
+
def save(
|
|
53
|
+
ctx: Context,
|
|
54
|
+
path: Annotated[str, typer.Argument(help="Full file path to save to")],
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Save the current canvas to a specific file path."""
|
|
57
|
+
console.print("[dim]Saving canvas (this may take a while for large canvases)...[/dim]")
|
|
58
|
+
try:
|
|
59
|
+
client = _shared._get_client(ctx)
|
|
60
|
+
result = client.save(path=path)
|
|
61
|
+
_shared._format_result(result)
|
|
62
|
+
except KritaError as exc:
|
|
63
|
+
_shared._handle_error(exc)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command()
|
|
67
|
+
def clear(
|
|
68
|
+
ctx: Context,
|
|
69
|
+
color: Annotated[str, typer.Option("--color", "-c", help="Color to fill with")] = "#1a1a2e",
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Clear the canvas to a solid color."""
|
|
72
|
+
try:
|
|
73
|
+
client = _shared._get_client(ctx)
|
|
74
|
+
result = client.clear(color=color)
|
|
75
|
+
_shared._format_result(result)
|
|
76
|
+
except KritaError as exc:
|
|
77
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Color-related CLI commands: set-color, get-color-at."""
|
|
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("set-color")
|
|
17
|
+
def set_color(
|
|
18
|
+
ctx: Context,
|
|
19
|
+
color: Annotated[str, typer.Argument(help="Hex color code (e.g., #ff6b6b)")],
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Set the foreground paint color."""
|
|
22
|
+
try:
|
|
23
|
+
client = _shared._get_client(ctx)
|
|
24
|
+
result = client.set_color(color=color)
|
|
25
|
+
_shared._format_result(result)
|
|
26
|
+
except KritaError as exc:
|
|
27
|
+
_shared._handle_error(exc)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command("get-color-at")
|
|
31
|
+
def get_color_at(
|
|
32
|
+
ctx: Context,
|
|
33
|
+
x: Annotated[int, typer.Argument(help="X coordinate")],
|
|
34
|
+
y: Annotated[int, typer.Argument(help="Y coordinate")],
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Sample the color at a specific pixel (eyedropper)."""
|
|
37
|
+
try:
|
|
38
|
+
client = _shared._get_client(ctx)
|
|
39
|
+
result = client.get_color_at(x=x, y=y)
|
|
40
|
+
_shared._format_result(result)
|
|
41
|
+
except KritaError as exc:
|
|
42
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""CLI commands for plugin configuration: show, set, reset."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from krita_cli import config_cmd
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(name="config", help="Plugin configuration commands.")
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("show")
|
|
18
|
+
def config_show() -> None:
|
|
19
|
+
"""Show current plugin configuration."""
|
|
20
|
+
config = config_cmd.load_config()
|
|
21
|
+
table = Table(title=f"Plugin Config ({config_cmd.CONFIG_FILE})")
|
|
22
|
+
table.add_column("Key", style="cyan")
|
|
23
|
+
table.add_column("Value", style="green")
|
|
24
|
+
for key, value in config.items():
|
|
25
|
+
table.add_row(str(key), str(value))
|
|
26
|
+
console.print(table)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command("set")
|
|
30
|
+
def config_set(
|
|
31
|
+
key: Annotated[str, typer.Argument(help="Configuration key")],
|
|
32
|
+
value: Annotated[str, typer.Argument(help="Configuration value")],
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Set a plugin configuration value."""
|
|
35
|
+
try:
|
|
36
|
+
config_cmd.set_key(key, value)
|
|
37
|
+
except ValueError as exc:
|
|
38
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
39
|
+
raise typer.Exit(1) from exc
|
|
40
|
+
console.print(f"[green]Set {key} = {value}[/green]")
|
|
41
|
+
console.print("[dim]Restart Krita for changes to take effect.[/dim]")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command("reset")
|
|
45
|
+
def config_reset() -> None:
|
|
46
|
+
"""Reset plugin configuration to defaults."""
|
|
47
|
+
config_cmd.reset_config()
|
|
48
|
+
console.print("[green]Configuration reset to defaults.[/green]")
|