Skip to main content

progit_plugin_sdk/traits/
jobs.rs

1// SPDX-License-Identifier: LSL-1.0
2// Copyright (c) 2026 Markus Maiwald
3
4//! Async job execution contract
5//!
6//! Host-managed background jobs that plugins can spawn and monitor.
7//! The plugin never owns the thread — it requests work from the host,
8//! receives progress events, and may cancel jobs it owns.
9//!
10//! This preserves Doctrine 4 (trait firewall): plugins cannot spawn raw
11//! threads, open arbitrary processes, or block the TUI render loop.
12
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// Opaque job identifier. Plugins treat this as an opaque string.
17pub type JobId = String;
18
19/// What the plugin wants the host to run.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct JobSpawnRequest {
22    /// Human label shown in TUI job lists.
23    pub label: String,
24    /// Command argv[0] — must be a binary the host already trusts
25    /// (e.g. `citadel`, not an arbitrary path).
26    pub binary: String,
27    /// Arguments passed to the binary.
28    pub args: Vec<String>,
29    /// Working directory. Empty = repo root.
30    #[serde(default)]
31    pub cwd: Option<String>,
32    /// Environment variables injected for this job.
33    #[serde(default)]
34    pub env: HashMap<String, String>,
35    /// Max runtime before host auto-cancels (seconds). 0 = host default.
36    #[serde(default)]
37    pub timeout_secs: u64,
38}
39
40/// Events the host emits about a job's lifecycle.
41///
42/// Serialised as `{"type": "...", "job_id": "...", "data": {...}}`
43/// so Lua receives ergonomic tables.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(tag = "type", rename_all = "snake_case")]
46pub enum JobEvent {
47    /// Job has started on a host background thread.
48    JobStarted {
49        job_id: JobId,
50        #[serde(flatten)]
51        data: JobStartedData,
52    },
53    /// Incremental progress (0–100 or arbitrary units).
54    JobProgress {
55        job_id: JobId,
56        #[serde(flatten)]
57        data: JobProgressData,
58    },
59    /// One line of stdout/stderr from the job process.
60    JobLogLine {
61        job_id: JobId,
62        #[serde(flatten)]
63        data: JobLogLineData,
64    },
65    /// Raw output chunk (for non-line-oriented binaries).
66    JobOutputChunk {
67        job_id: JobId,
68        #[serde(flatten)]
69        data: JobOutputChunkData,
70    },
71    /// Job was cancelled by user or timeout.
72    JobCancelled {
73        job_id: JobId,
74        #[serde(flatten)]
75        data: JobCancelledData,
76    },
77    /// Job finished successfully.
78    JobCompleted {
79        job_id: JobId,
80        #[serde(flatten)]
81        data: JobCompletedData,
82    },
83    /// Job failed (non-zero exit, panic, or unhandled error).
84    JobFailed {
85        job_id: JobId,
86        #[serde(flatten)]
87        data: JobFailedData,
88    },
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct JobStartedData {
93    pub pid: Option<u32>,
94    pub started_at: String, // ISO 8601
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct JobProgressData {
99    /// 0–100 when known, otherwise arbitrary step counter.
100    pub percent: Option<u8>,
101    /// Human step description (e.g. "Compiling module 3/7").
102    pub step: Option<String>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct JobLogLineData {
107    pub line: String,
108    /// `stdout` | `stderr` | `system`
109    pub stream: String,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct JobOutputChunkData {
114    pub bytes: Vec<u8>,
115    /// `stdout` | `stderr`
116    pub stream: String,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct JobCancelledData {
121    pub reason: String,
122    pub cancelled_at: String,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct JobCompletedData {
127    pub exit_code: i32,
128    pub completed_at: String,
129    /// Truncated stdout tail (host decides limit, e.g. last 4 KB).
130    pub output_tail: Option<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct JobFailedData {
135    pub exit_code: Option<i32>,
136    pub error: String,
137    pub failed_at: String,
138}
139
140/// Host-side trait for job orchestration.
141///
142/// The ProGit TUI implements this and injects it into the plugin runtime.
143/// Plugins receive a `Box<dyn JobHost>` (or Lua equivalent) and call
144/// `spawn`, `cancel`, and `status` — never `std::process::Command` directly.
145pub trait JobHost: Send + Sync {
146    /// Spawn a background job. Returns the opaque `JobId`.
147    fn spawn(&self, req: JobSpawnRequest) -> JobId;
148
149    /// Cancel a job the plugin previously spawned.
150    /// Returns true if cancellation was sent; false if job already finished.
151    fn cancel(&self, job_id: &JobId) -> bool;
152
153    /// Poll the current status of a job.
154    fn status(&self, job_id: &JobId) -> Option<JobStatus>;
155
156    /// Drain events for jobs owned by this plugin.
157    /// Non-blocking: returns events accumulated since last call.
158    fn drain_events(&self) -> Vec<JobEvent>;
159}
160
161/// Lightweight status snapshot for UI lists.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub enum JobStatus {
164    Pending,
165    Running {
166        started_at: String,
167        percent: Option<u8>,
168    },
169    Cancelled {
170        reason: String,
171    },
172    Completed {
173        exit_code: i32,
174    },
175    Failed {
176        exit_code: Option<i32>,
177        error: String,
178    },
179}