taskdog-ui 0.18.1__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.
Files changed (184) hide show
  1. taskdog/__init__.py +8 -0
  2. taskdog/cli/__init__.py +1 -0
  3. taskdog/cli/commands/__init__.py +1 -0
  4. taskdog/cli/commands/add.py +148 -0
  5. taskdog/cli/commands/add_dependency.py +40 -0
  6. taskdog/cli/commands/audit_logs.py +226 -0
  7. taskdog/cli/commands/cancel.py +29 -0
  8. taskdog/cli/commands/common_options.py +116 -0
  9. taskdog/cli/commands/done.py +30 -0
  10. taskdog/cli/commands/export.py +155 -0
  11. taskdog/cli/commands/fix_actual.py +187 -0
  12. taskdog/cli/commands/gantt.py +153 -0
  13. taskdog/cli/commands/note.py +218 -0
  14. taskdog/cli/commands/optimize.py +152 -0
  15. taskdog/cli/commands/pause.py +36 -0
  16. taskdog/cli/commands/remove_dependency.py +40 -0
  17. taskdog/cli/commands/reopen.py +40 -0
  18. taskdog/cli/commands/restore.py +29 -0
  19. taskdog/cli/commands/rm.py +56 -0
  20. taskdog/cli/commands/show.py +31 -0
  21. taskdog/cli/commands/start.py +32 -0
  22. taskdog/cli/commands/stats.py +65 -0
  23. taskdog/cli/commands/table.py +96 -0
  24. taskdog/cli/commands/table_helpers.py +27 -0
  25. taskdog/cli/commands/tags.py +95 -0
  26. taskdog/cli/commands/timeline.py +90 -0
  27. taskdog/cli/commands/tui.py +64 -0
  28. taskdog/cli/commands/update.py +128 -0
  29. taskdog/cli/commands/update_helpers.py +45 -0
  30. taskdog/cli/context.py +34 -0
  31. taskdog/cli/error_handler.py +98 -0
  32. taskdog/cli_main.py +187 -0
  33. taskdog/console/__init__.py +1 -0
  34. taskdog/console/console_writer.py +139 -0
  35. taskdog/console/rich_console_writer.py +212 -0
  36. taskdog/constants/__init__.py +102 -0
  37. taskdog/constants/ascii_art.py +13 -0
  38. taskdog/constants/audit_log.py +41 -0
  39. taskdog/constants/colors.py +33 -0
  40. taskdog/constants/common.py +39 -0
  41. taskdog/constants/formatting.py +15 -0
  42. taskdog/constants/gantt.py +33 -0
  43. taskdog/constants/icons.py +7 -0
  44. taskdog/constants/symbols.py +18 -0
  45. taskdog/constants/task_table.py +59 -0
  46. taskdog/constants/timeline.py +14 -0
  47. taskdog/exporters/__init__.py +13 -0
  48. taskdog/exporters/csv_task_exporter.py +54 -0
  49. taskdog/exporters/json_task_exporter.py +22 -0
  50. taskdog/exporters/markdown_table_exporter.py +154 -0
  51. taskdog/exporters/task_exporter.py +47 -0
  52. taskdog/formatters/__init__.py +1 -0
  53. taskdog/formatters/date_time_formatter.py +195 -0
  54. taskdog/formatters/duration_formatter.py +76 -0
  55. taskdog/infrastructure/__init__.py +12 -0
  56. taskdog/infrastructure/cli_config_manager.py +234 -0
  57. taskdog/presenters/__init__.py +18 -0
  58. taskdog/presenters/gantt_presenter.py +87 -0
  59. taskdog/presenters/statistics_presenter.py +137 -0
  60. taskdog/presenters/table_presenter.py +59 -0
  61. taskdog/presenters/timeline_presenter.py +191 -0
  62. taskdog/py.typed +0 -0
  63. taskdog/renderers/__init__.py +1 -0
  64. taskdog/renderers/gantt_cell_formatter.py +749 -0
  65. taskdog/renderers/rich_detail_renderer.py +148 -0
  66. taskdog/renderers/rich_gantt_renderer.py +361 -0
  67. taskdog/renderers/rich_renderer_base.py +23 -0
  68. taskdog/renderers/rich_statistics_renderer.py +413 -0
  69. taskdog/renderers/rich_table_renderer.py +415 -0
  70. taskdog/renderers/rich_timeline_renderer.py +248 -0
  71. taskdog/renderers/timeline_cell_formatter.py +131 -0
  72. taskdog/services/__init__.py +1 -0
  73. taskdog/services/task_data_loader.py +109 -0
  74. taskdog/shared/__init__.py +0 -0
  75. taskdog/shared/click_types/__init__.py +5 -0
  76. taskdog/shared/click_types/field_list.py +87 -0
  77. taskdog/shared/click_types/positive_number.py +95 -0
  78. taskdog/tui/__init__.py +1 -0
  79. taskdog/tui/app.py +523 -0
  80. taskdog/tui/commands/__init__.py +44 -0
  81. taskdog/tui/commands/add.py +65 -0
  82. taskdog/tui/commands/audit.py +15 -0
  83. taskdog/tui/commands/base.py +235 -0
  84. taskdog/tui/commands/batch_command_base.py +141 -0
  85. taskdog/tui/commands/cancel.py +24 -0
  86. taskdog/tui/commands/decorators.py +37 -0
  87. taskdog/tui/commands/done.py +16 -0
  88. taskdog/tui/commands/edit.py +188 -0
  89. taskdog/tui/commands/export.py +93 -0
  90. taskdog/tui/commands/factory.py +74 -0
  91. taskdog/tui/commands/fix_actual.py +53 -0
  92. taskdog/tui/commands/hard_delete.py +28 -0
  93. taskdog/tui/commands/help.py +14 -0
  94. taskdog/tui/commands/note.py +69 -0
  95. taskdog/tui/commands/optimize.py +121 -0
  96. taskdog/tui/commands/pause.py +16 -0
  97. taskdog/tui/commands/refresh.py +12 -0
  98. taskdog/tui/commands/reopen.py +24 -0
  99. taskdog/tui/commands/rm.py +28 -0
  100. taskdog/tui/commands/show.py +74 -0
  101. taskdog/tui/commands/start.py +16 -0
  102. taskdog/tui/commands/stats.py +13 -0
  103. taskdog/tui/constants/__init__.py +1 -0
  104. taskdog/tui/constants/command_mapping.py +21 -0
  105. taskdog/tui/constants/export_config.py +9 -0
  106. taskdog/tui/constants/keybindings.py +92 -0
  107. taskdog/tui/constants/ui_settings.py +45 -0
  108. taskdog/tui/context.py +35 -0
  109. taskdog/tui/dialogs/__init__.py +11 -0
  110. taskdog/tui/dialogs/algorithm_selection_dialog.py +217 -0
  111. taskdog/tui/dialogs/base_dialog.py +119 -0
  112. taskdog/tui/dialogs/confirmation_dialog.py +73 -0
  113. taskdog/tui/dialogs/fix_actual_dialog.py +248 -0
  114. taskdog/tui/dialogs/form_dialog.py +88 -0
  115. taskdog/tui/dialogs/help_dialog.py +171 -0
  116. taskdog/tui/dialogs/scrollable_dialog.py +86 -0
  117. taskdog/tui/dialogs/stats_dialog.py +528 -0
  118. taskdog/tui/dialogs/task_detail_dialog.py +397 -0
  119. taskdog/tui/dialogs/task_form_dialog.py +176 -0
  120. taskdog/tui/events.py +135 -0
  121. taskdog/tui/forms/__init__.py +3 -0
  122. taskdog/tui/forms/suggesters/__init__.py +9 -0
  123. taskdog/tui/forms/suggesters/comma_separated_suggester.py +58 -0
  124. taskdog/tui/forms/task_form_fields.py +224 -0
  125. taskdog/tui/forms/validators/__init__.py +11 -0
  126. taskdog/tui/forms/validators/datetime_validator.py +87 -0
  127. taskdog/tui/forms/validators/optimization_validators.py +66 -0
  128. taskdog/tui/messages.py +143 -0
  129. taskdog/tui/palette/__init__.py +13 -0
  130. taskdog/tui/palette/providers/__init__.py +27 -0
  131. taskdog/tui/palette/providers/audit_provider.py +13 -0
  132. taskdog/tui/palette/providers/base.py +122 -0
  133. taskdog/tui/palette/providers/export_providers.py +54 -0
  134. taskdog/tui/palette/providers/help_provider.py +13 -0
  135. taskdog/tui/palette/providers/optimize_providers.py +13 -0
  136. taskdog/tui/palette/providers/sort_providers.py +57 -0
  137. taskdog/tui/screens/__init__.py +1 -0
  138. taskdog/tui/screens/audit_log_screen.py +66 -0
  139. taskdog/tui/screens/main_screen.py +218 -0
  140. taskdog/tui/selection.py +69 -0
  141. taskdog/tui/services/__init__.py +7 -0
  142. taskdog/tui/services/connection_monitor.py +42 -0
  143. taskdog/tui/services/event_handler_registry.py +212 -0
  144. taskdog/tui/services/task_ui_manager.py +245 -0
  145. taskdog/tui/services/websocket_handler.py +37 -0
  146. taskdog/tui/state/__init__.py +7 -0
  147. taskdog/tui/state/connection_status.py +19 -0
  148. taskdog/tui/state/connection_status_manager.py +84 -0
  149. taskdog/tui/state/tui_state.py +257 -0
  150. taskdog/tui/styles/components.tcss +149 -0
  151. taskdog/tui/styles/dialogs.tcss +254 -0
  152. taskdog/tui/styles/main.tcss +74 -0
  153. taskdog/tui/styles/theme.tcss +8 -0
  154. taskdog/tui/utils/__init__.py +1 -0
  155. taskdog/tui/utils/css_loader.py +32 -0
  156. taskdog/tui/widgets/__init__.py +15 -0
  157. taskdog/tui/widgets/audit_log_entry_builder.py +184 -0
  158. taskdog/tui/widgets/audit_log_table.py +153 -0
  159. taskdog/tui/widgets/base_widget.py +60 -0
  160. taskdog/tui/widgets/custom_footer.py +251 -0
  161. taskdog/tui/widgets/gantt_data_table.py +652 -0
  162. taskdog/tui/widgets/gantt_widget.py +412 -0
  163. taskdog/tui/widgets/search_query_parser.py +127 -0
  164. taskdog/tui/widgets/task_search_filter.py +238 -0
  165. taskdog/tui/widgets/task_table.py +423 -0
  166. taskdog/tui/widgets/task_table_row_builder.py +229 -0
  167. taskdog/tui/widgets/vi_navigation_mixin.py +134 -0
  168. taskdog/tui/widgets/vi_select.py +77 -0
  169. taskdog/utils/__init__.py +0 -0
  170. taskdog/utils/editor.py +38 -0
  171. taskdog/utils/note_editor.py +211 -0
  172. taskdog/utils/notes_template.py +57 -0
  173. taskdog/utils/template_loader.py +86 -0
  174. taskdog/view_models/__init__.py +26 -0
  175. taskdog/view_models/base.py +25 -0
  176. taskdog/view_models/gantt_view_model.py +87 -0
  177. taskdog/view_models/statistics_view_model.py +108 -0
  178. taskdog/view_models/task_view_model.py +60 -0
  179. taskdog/view_models/timeline_view_model.py +68 -0
  180. taskdog_ui-0.18.1.dist-info/METADATA +190 -0
  181. taskdog_ui-0.18.1.dist-info/RECORD +184 -0
  182. taskdog_ui-0.18.1.dist-info/WHEEL +5 -0
  183. taskdog_ui-0.18.1.dist-info/entry_points.txt +2 -0
  184. taskdog_ui-0.18.1.dist-info/top_level.txt +1 -0
taskdog/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """taskdog - CLI task management tool"""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("taskdog-ui")
7
+ except PackageNotFoundError:
8
+ __version__ = "unknown"
@@ -0,0 +1 @@
1
+ """CLI interface."""
@@ -0,0 +1 @@
1
+ """CLI commands."""
@@ -0,0 +1,148 @@
1
+ """Add command - Add a new task."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import click
8
+
9
+ from taskdog.cli.error_handler import handle_task_errors
10
+
11
+ if TYPE_CHECKING:
12
+ from datetime import datetime
13
+
14
+ from taskdog.cli.context import CliContext
15
+ from taskdog_core.domain.exceptions.task_exceptions import TaskValidationError
16
+
17
+
18
+ @click.command(name="add", help="Add a new task.")
19
+ @click.argument("name", type=str)
20
+ @click.option(
21
+ "--priority",
22
+ "-p",
23
+ type=int,
24
+ default=None,
25
+ help="Task priority (default: from config or 5, higher value = higher priority)",
26
+ )
27
+ @click.option(
28
+ "--fixed",
29
+ "-f",
30
+ is_flag=True,
31
+ help="Mark task as fixed (won't be rescheduled by optimizer)",
32
+ )
33
+ @click.option(
34
+ "--depends-on",
35
+ "-d",
36
+ multiple=True,
37
+ type=int,
38
+ help="Task IDs this task depends on (can be specified multiple times)",
39
+ )
40
+ @click.option(
41
+ "--tag",
42
+ "-t",
43
+ multiple=True,
44
+ type=str,
45
+ help="Tags for categorization and filtering (can be specified multiple times)",
46
+ )
47
+ @click.option(
48
+ "--deadline",
49
+ "-D",
50
+ type=click.DateTime(formats=["%Y-%m-%d %H:%M:%S"]),
51
+ default=None,
52
+ help="Task deadline (format: YYYY-MM-DD HH:MM:SS)",
53
+ )
54
+ @click.option(
55
+ "--estimate",
56
+ "-e",
57
+ type=float,
58
+ default=None,
59
+ help="Estimated duration in hours (must be > 0)",
60
+ )
61
+ @click.option(
62
+ "--start",
63
+ "-s",
64
+ type=click.DateTime(formats=["%Y-%m-%d %H:%M:%S"]),
65
+ default=None,
66
+ help="Planned start time (format: YYYY-MM-DD HH:MM:SS)",
67
+ )
68
+ @click.option(
69
+ "--end",
70
+ "-E",
71
+ type=click.DateTime(formats=["%Y-%m-%d %H:%M:%S"]),
72
+ default=None,
73
+ help="Planned end time (format: YYYY-MM-DD HH:MM:SS)",
74
+ )
75
+ @click.pass_context
76
+ @handle_task_errors("adding task")
77
+ def add_command(
78
+ ctx: click.Context,
79
+ name: str,
80
+ priority: int,
81
+ fixed: bool,
82
+ depends_on: tuple[int, ...],
83
+ tag: tuple[str, ...],
84
+ deadline: datetime | None,
85
+ estimate: float | None,
86
+ start: datetime | None,
87
+ end: datetime | None,
88
+ ) -> None:
89
+ """Add a new task.
90
+
91
+ You can set all task properties at creation time, or use dedicated commands
92
+ after creation (deadline, estimate, schedule) for updates.
93
+
94
+ Usage:
95
+ # Basic task
96
+ taskdog add "Task name"
97
+
98
+ # With priority and tags
99
+ taskdog add "Task name" --priority 3 --tag backend
100
+
101
+ # Full task with all properties
102
+ taskdog add "Implement auth" -p 5 -D "2025-12-01" -e 8 -s "2025-11-20" -t backend
103
+
104
+ Examples:
105
+ # Simple task
106
+ taskdog add "Implement authentication"
107
+
108
+ # With priority
109
+ taskdog add "Fix login bug" -p 5
110
+
111
+ # With deadline and estimate
112
+ taskdog add "Add unit tests" --deadline "2025-12-15" --estimate 4
113
+
114
+ # Complete task with schedule
115
+ taskdog add "Code review" -p 3 -s "2025-11-20 09:00" -e "2025-11-20 12:00"
116
+
117
+ # With dependencies and tags
118
+ taskdog add "Deploy feature" -d 123 -t deployment -p 5
119
+ """
120
+ ctx_obj: CliContext = ctx.obj
121
+ console_writer = ctx_obj.console_writer
122
+
123
+ # Create task via API client
124
+ task = ctx_obj.api_client.create_task(
125
+ name=name,
126
+ priority=priority,
127
+ is_fixed=fixed,
128
+ tags=list(tag) if tag else None,
129
+ deadline=deadline,
130
+ estimated_duration=estimate,
131
+ planned_start=start,
132
+ planned_end=end,
133
+ )
134
+
135
+ # Add dependencies if specified
136
+ if depends_on:
137
+ for dep_id in depends_on:
138
+ try:
139
+ task = ctx_obj.api_client.add_dependency(task.id, dep_id)
140
+ except TaskValidationError as e:
141
+ console_writer.validation_error(str(e))
142
+ # Continue adding other dependencies even if one fails
143
+
144
+ console_writer.task_success("Added", task)
145
+ if task.depends_on:
146
+ console_writer.info(f"Dependencies: {task.depends_on}")
147
+ if task.tags:
148
+ console_writer.info(f"Tags: {', '.join(task.tags)}")
@@ -0,0 +1,40 @@
1
+ """Add-dependency command - Add a task dependency."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import click
8
+
9
+ from taskdog.cli.error_handler import handle_task_errors
10
+
11
+ if TYPE_CHECKING:
12
+ from taskdog.cli.context import CliContext
13
+
14
+
15
+ @click.command(name="add-dependency", help="Add a dependency to a task.")
16
+ @click.argument("task_id", type=int)
17
+ @click.argument("depends_on_id", type=int)
18
+ @click.pass_context
19
+ @handle_task_errors("adding dependency")
20
+ def add_dependency_command(
21
+ ctx: click.Context, task_id: int, depends_on_id: int
22
+ ) -> None:
23
+ """Add a dependency to a task.
24
+
25
+ Usage:
26
+ taskdog add-dependency <TASK_ID> <DEPENDS_ON_ID>
27
+
28
+ Examples:
29
+ taskdog add-dependency 5 3 # Task 5 depends on task 3
30
+ """
31
+ ctx_obj: CliContext = ctx.obj
32
+ console_writer = ctx_obj.console_writer
33
+
34
+ # Add dependency via API client
35
+ task = ctx_obj.api_client.add_dependency(task_id, depends_on_id)
36
+
37
+ console_writer.success(
38
+ f"Added dependency: Task {task_id} now depends on task {depends_on_id}"
39
+ )
40
+ console_writer.info(f"Task {task_id} dependencies: {task.depends_on}")
@@ -0,0 +1,226 @@
1
+ """Audit logs command - Display operation history."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING
7
+
8
+ import click
9
+ from rich.table import Table
10
+
11
+ from taskdog.cli.error_handler import handle_command_errors
12
+ from taskdog.constants.audit_log import (
13
+ AUDIT_CHANGES_WIDTH,
14
+ AUDIT_CLIENT_WIDTH,
15
+ AUDIT_ID_WIDTH,
16
+ AUDIT_OPERATION_WIDTH,
17
+ AUDIT_STATUS_WIDTH,
18
+ AUDIT_TIMESTAMP_WIDTH,
19
+ COLUMN_AUDIT_ID_STYLE,
20
+ COLUMN_AUDIT_STATUS_FAIL_STYLE,
21
+ COLUMN_AUDIT_STATUS_OK_STYLE,
22
+ HEADER_AUDIT_CHANGES,
23
+ HEADER_AUDIT_CLIENT,
24
+ HEADER_AUDIT_OPERATION,
25
+ HEADER_AUDIT_RESOURCE,
26
+ HEADER_AUDIT_STATUS,
27
+ HEADER_AUDIT_TIMESTAMP,
28
+ JUSTIFY_AUDIT_CHANGES,
29
+ )
30
+ from taskdog.constants.common import HEADER_ID, TABLE_HEADER_STYLE
31
+ from taskdog.constants.formatting import format_table_title
32
+ from taskdog.tui.widgets.audit_log_entry_builder import format_audit_changes
33
+
34
+ if TYPE_CHECKING:
35
+ from taskdog.cli.context import CliContext
36
+ from taskdog_core.application.dto.audit_log_dto import AuditLogOutput
37
+
38
+
39
+ def _parse_date_filter(date_str: str, end_of_day: bool = False) -> datetime:
40
+ """Parse a date filter string to datetime.
41
+
42
+ Args:
43
+ date_str: ISO format date or datetime string
44
+ end_of_day: If True and only date provided, use end of day
45
+
46
+ Returns:
47
+ Parsed datetime object
48
+ """
49
+ try:
50
+ return datetime.fromisoformat(date_str)
51
+ except ValueError:
52
+ suffix = "T23:59:59" if end_of_day else "T00:00:00"
53
+ return datetime.fromisoformat(f"{date_str}{suffix}")
54
+
55
+
56
+ def _format_resource(log: AuditLogOutput) -> str:
57
+ """Format resource display for audit log entry."""
58
+ if log.resource_id and log.resource_name:
59
+ return f"[cyan]#{log.resource_id}[/cyan] {log.resource_name}"
60
+ if log.resource_id:
61
+ return f"[cyan]#{log.resource_id}[/cyan]"
62
+ if log.resource_name:
63
+ return log.resource_name
64
+ return "[dim]-[/dim]"
65
+
66
+
67
+ def _format_changes(log: AuditLogOutput) -> str:
68
+ """Format changes display for audit log entry."""
69
+ if not log.success and log.error_message:
70
+ error_msg = log.error_message[:AUDIT_CHANGES_WIDTH]
71
+ if len(log.error_message) > AUDIT_CHANGES_WIDTH:
72
+ error_msg += "..."
73
+ return f"[red]{error_msg}[/red]"
74
+ return format_audit_changes(
75
+ log.old_values, log.new_values, max_length=AUDIT_CHANGES_WIDTH
76
+ )
77
+
78
+
79
+ @click.command(
80
+ name="audit-logs",
81
+ help="""Display operation history (audit logs).
82
+
83
+ Shows a history of operations performed via the API, including task creates,
84
+ updates, status changes, and other modifications.
85
+
86
+ Use filters to narrow down the logs by client name, operation type, task ID, etc.
87
+
88
+ Examples:
89
+ taskdog audit-logs # Show latest 100 logs
90
+ taskdog audit-logs --client claude-code # Filter by client
91
+ taskdog audit-logs --task 123 # Filter by task ID
92
+ taskdog audit-logs --operation complete_task # Filter by operation
93
+ taskdog audit-logs --since 2025-12-01 # Filter by date
94
+ """,
95
+ )
96
+ @click.option(
97
+ "--client",
98
+ "-c",
99
+ "client_filter",
100
+ type=str,
101
+ default=None,
102
+ help="Filter by client name (e.g., 'claude-code')",
103
+ )
104
+ @click.option(
105
+ "--operation",
106
+ "-o",
107
+ type=str,
108
+ default=None,
109
+ help="Filter by operation type (e.g., 'create_task', 'complete_task')",
110
+ )
111
+ @click.option(
112
+ "--task",
113
+ "-t",
114
+ "task_id",
115
+ type=int,
116
+ default=None,
117
+ help="Filter by task ID",
118
+ )
119
+ @click.option(
120
+ "--since",
121
+ type=str,
122
+ default=None,
123
+ help="Show logs since this date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)",
124
+ )
125
+ @click.option(
126
+ "--until",
127
+ type=str,
128
+ default=None,
129
+ help="Show logs until this date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)",
130
+ )
131
+ @click.option(
132
+ "--limit",
133
+ "-n",
134
+ type=click.IntRange(1, 1000),
135
+ default=100,
136
+ help="Maximum number of logs to show (default: 100, max: 1000)",
137
+ )
138
+ @click.option(
139
+ "--failed",
140
+ is_flag=True,
141
+ default=False,
142
+ help="Show only failed operations",
143
+ )
144
+ @click.pass_context
145
+ @handle_command_errors("fetching audit logs")
146
+ def audit_logs_command(
147
+ ctx: click.Context,
148
+ client_filter: str | None,
149
+ operation: str | None,
150
+ task_id: int | None,
151
+ since: str | None,
152
+ until: str | None,
153
+ limit: int,
154
+ failed: bool,
155
+ ) -> None:
156
+ """Display operation history (audit logs)."""
157
+ ctx_obj: CliContext = ctx.obj
158
+ console_writer = ctx_obj.console_writer
159
+ api_client = ctx_obj.api_client
160
+
161
+ # Parse date filters
162
+ start_date = _parse_date_filter(since) if since else None
163
+ end_date = _parse_date_filter(until, end_of_day=True) if until else None
164
+
165
+ # Fetch audit logs via API
166
+ result = api_client.list_audit_logs(
167
+ client_filter=client_filter,
168
+ operation=operation,
169
+ resource_type="task" if task_id else None,
170
+ resource_id=task_id,
171
+ success=False if failed else None,
172
+ start_date=start_date,
173
+ end_date=end_date,
174
+ limit=limit,
175
+ )
176
+
177
+ if not result.logs:
178
+ console_writer.info("No audit logs found matching the criteria.")
179
+ return
180
+
181
+ # Create Rich table
182
+ table = Table(
183
+ title=format_table_title(
184
+ f"Audit Logs ({len(result.logs)} of {result.total_count})"
185
+ ),
186
+ show_header=True,
187
+ header_style=TABLE_HEADER_STYLE,
188
+ )
189
+ table.add_column(HEADER_ID, style=COLUMN_AUDIT_ID_STYLE, width=AUDIT_ID_WIDTH)
190
+ table.add_column(HEADER_AUDIT_TIMESTAMP, width=AUDIT_TIMESTAMP_WIDTH)
191
+ table.add_column(HEADER_AUDIT_RESOURCE)
192
+ table.add_column(HEADER_AUDIT_OPERATION, width=AUDIT_OPERATION_WIDTH)
193
+ table.add_column(
194
+ HEADER_AUDIT_CHANGES,
195
+ width=AUDIT_CHANGES_WIDTH,
196
+ justify=JUSTIFY_AUDIT_CHANGES,
197
+ )
198
+ table.add_column(HEADER_AUDIT_CLIENT, width=AUDIT_CLIENT_WIDTH)
199
+ table.add_column(HEADER_AUDIT_STATUS, width=AUDIT_STATUS_WIDTH)
200
+
201
+ for log in result.logs:
202
+ status_style = (
203
+ COLUMN_AUDIT_STATUS_OK_STYLE
204
+ if log.success
205
+ else COLUMN_AUDIT_STATUS_FAIL_STYLE
206
+ )
207
+ status_label = "OK" if log.success else "FAILED"
208
+ changes = _format_changes(log)
209
+ table.add_row(
210
+ str(log.id),
211
+ log.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
212
+ _format_resource(log),
213
+ log.operation,
214
+ changes,
215
+ log.client_name or "[dim]anonymous[/dim]",
216
+ f"[{status_style}]{status_label}[/{status_style}]",
217
+ )
218
+
219
+ console_writer.print(table)
220
+
221
+ # Show pagination info if there are more logs
222
+ if result.total_count > len(result.logs):
223
+ console_writer.info(
224
+ f"Showing {len(result.logs)} of {result.total_count} logs. "
225
+ f"Use --limit to see more."
226
+ )
@@ -0,0 +1,29 @@
1
+ """Cancel command - Mark a task as canceled."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import click
8
+
9
+ if TYPE_CHECKING:
10
+ from taskdog.cli.context import CliContext
11
+ from taskdog_core.shared.constants import StatusVerbs
12
+
13
+
14
+ @click.command(name="cancel", help="Mark task(s) as canceled.")
15
+ @click.argument("task_ids", nargs=-1, type=int, required=True)
16
+ @click.pass_context
17
+ def cancel_command(ctx: click.Context, task_ids: tuple[int, ...]) -> None:
18
+ """Mark task(s) as canceled."""
19
+ ctx_obj: CliContext = ctx.obj
20
+ console_writer = ctx_obj.console_writer
21
+
22
+ results = ctx_obj.api_client.bulk_cancel(list(task_ids))
23
+ for result in results.results:
24
+ if result.success and result.task is not None:
25
+ console_writer.task_success(StatusVerbs.CANCELED, result.task)
26
+ elif result.error is not None:
27
+ console_writer.validation_error(result.error)
28
+ if len(task_ids) > 1:
29
+ console_writer.empty_line()
@@ -0,0 +1,116 @@
1
+ """Common CLI option decorators for reuse across commands."""
2
+
3
+ from collections.abc import Callable
4
+ from functools import wraps
5
+ from typing import Any
6
+
7
+ import click
8
+
9
+
10
+ def filter_options() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
11
+ """Add common filter options (--all, --status) to a command.
12
+
13
+ Usage:
14
+ @click.command()
15
+ @filter_options()
16
+ def my_command(ctx, include_archived, status, ...):
17
+ pass
18
+ """
19
+
20
+ def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
21
+ @click.option(
22
+ "--status",
23
+ type=click.Choice(
24
+ ["pending", "in_progress", "completed", "canceled"],
25
+ case_sensitive=False,
26
+ ),
27
+ default=None,
28
+ help="Filter tasks by status (overrides --all). Note: archived tasks are controlled by the --all flag, not --status.",
29
+ )
30
+ @click.option(
31
+ "--all",
32
+ "-a",
33
+ "include_archived",
34
+ is_flag=True,
35
+ help="Show all tasks including completed, canceled, and archived",
36
+ )
37
+ @wraps(f)
38
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
39
+ return f(*args, **kwargs)
40
+
41
+ return wrapper
42
+
43
+ return decorator
44
+
45
+
46
+ def sort_options(
47
+ default_sort: str = "id",
48
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
49
+ """Add common sort options (--sort, --reverse) to a command.
50
+
51
+ Args:
52
+ default_sort: Default field to sort by (default: "id")
53
+
54
+ Usage:
55
+ @click.command()
56
+ @sort_options(default_sort="deadline")
57
+ def my_command(ctx, sort, reverse, ...):
58
+ pass
59
+ """
60
+
61
+ def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
62
+ @click.option(
63
+ "--reverse",
64
+ "-r",
65
+ is_flag=True,
66
+ help="Reverse sort order",
67
+ )
68
+ @click.option(
69
+ "--sort",
70
+ type=click.Choice(
71
+ ["id", "priority", "deadline", "name", "status", "planned_start"]
72
+ ),
73
+ default=default_sort,
74
+ help=f"Sort tasks by specified field (default: {default_sort})",
75
+ )
76
+ @wraps(f)
77
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
78
+ return f(*args, **kwargs)
79
+
80
+ return wrapper
81
+
82
+ return decorator
83
+
84
+
85
+ def date_range_options() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
86
+ """Add common date range options (--start-date, --end-date) to a command.
87
+
88
+ Usage:
89
+ @click.command()
90
+ @date_range_options()
91
+ def my_command(ctx, start_date, end_date, ...):
92
+ pass
93
+ """
94
+
95
+ def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
96
+ @click.option(
97
+ "--end-date",
98
+ "-e",
99
+ type=click.DateTime(),
100
+ help="End date for filtering (YYYY-MM-DD). "
101
+ "Shows tasks with any date field <= end date.",
102
+ )
103
+ @click.option(
104
+ "--start-date",
105
+ "-s",
106
+ type=click.DateTime(),
107
+ help="Start date for filtering (YYYY-MM-DD). "
108
+ "Shows tasks with any date field >= start date.",
109
+ )
110
+ @wraps(f)
111
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
112
+ return f(*args, **kwargs)
113
+
114
+ return wrapper
115
+
116
+ return decorator
@@ -0,0 +1,30 @@
1
+ """Done command - Mark a task as completed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import click
8
+
9
+ if TYPE_CHECKING:
10
+ from taskdog.cli.context import CliContext
11
+ from taskdog_core.shared.constants import StatusVerbs
12
+
13
+
14
+ @click.command(name="done", help="Mark task(s) as completed.")
15
+ @click.argument("task_ids", nargs=-1, type=int, required=True)
16
+ @click.pass_context
17
+ def done_command(ctx: click.Context, task_ids: tuple[int, ...]) -> None:
18
+ """Mark task(s) as completed."""
19
+ ctx_obj: CliContext = ctx.obj
20
+ console_writer = ctx_obj.console_writer
21
+
22
+ results = ctx_obj.api_client.bulk_complete(list(task_ids))
23
+ for result in results.results:
24
+ if result.success and result.task is not None:
25
+ console_writer.task_success(StatusVerbs.COMPLETED, result.task)
26
+ console_writer.task_completion_details(result.task)
27
+ elif result.error is not None:
28
+ console_writer.validation_error(result.error)
29
+ if len(task_ids) > 1:
30
+ console_writer.empty_line()