Skip to main content

zsh/
jobs.rs

1//! Job control for zshrs
2//!
3//! Port from zsh/Src/jobs.c
4//!
5//! Provides job control, process management, and signal handling for jobs.
6
7use std::process::Child;
8use std::time::{Duration, Instant};
9
10/// Job status flags
11pub mod stat {
12    pub const STOPPED: u32 = 1 << 0; // Job is stopped
13    pub const DONE: u32 = 1 << 1; // Job is finished
14    pub const SUBJOB: u32 = 1 << 2; // Job is a subjob
15    pub const CURSH: u32 = 1 << 3; // Last pipeline elem in current shell
16    pub const SUPERJOB: u32 = 1 << 4; // Job is a superjob
17    pub const WASSUPER: u32 = 1 << 5; // Was a superjob
18    pub const INUSE: u32 = 1 << 6; // Entry in use
19    pub const BUILTIN: u32 = 1 << 7; // Job has builtin
20    pub const DISOWN: u32 = 1 << 8; // Disowned
21    pub const NOTIFY: u32 = 1 << 9; // Notify when done
22    pub const ATTACH: u32 = 1 << 10; // Attached to tty
23}
24
25/// Special process status values
26pub const SP_RUNNING: i32 = -1;
27
28/// Maximum pipestats
29pub const MAX_PIPESTATS: usize = 256;
30
31/// Process timing information
32#[derive(Clone, Debug, Default)]
33pub struct TimeInfo {
34    pub user_time: Duration,
35    pub sys_time: Duration,
36}
37
38/// A single process in a pipeline
39#[derive(Clone, Debug)]
40pub struct Process {
41    pub pid: i32,
42    pub status: i32,
43    pub start_time: Option<Instant>,
44    pub end_time: Option<Instant>,
45    pub ti: TimeInfo,
46    pub text: String,
47}
48
49impl Process {
50    pub fn new(pid: i32) -> Self {
51        Process {
52            pid,
53            status: SP_RUNNING,
54            start_time: Some(Instant::now()),
55            end_time: None,
56            ti: TimeInfo::default(),
57            text: String::new(),
58        }
59    }
60
61    pub fn is_running(&self) -> bool {
62        self.status == SP_RUNNING
63    }
64
65    pub fn is_stopped(&self) -> bool {
66        // WIFSTOPPED equivalent
67        self.status & 0xff == 0x7f
68    }
69
70    pub fn is_signaled(&self) -> bool {
71        // WIFSIGNALED equivalent
72        (self.status & 0x7f) > 0 && (self.status & 0x7f) < 0x7f
73    }
74
75    pub fn exit_status(&self) -> i32 {
76        // WEXITSTATUS equivalent
77        (self.status >> 8) & 0xff
78    }
79
80    pub fn term_sig(&self) -> i32 {
81        // WTERMSIG equivalent
82        self.status & 0x7f
83    }
84
85    pub fn stop_sig(&self) -> i32 {
86        // WSTOPSIG equivalent
87        (self.status >> 8) & 0xff
88    }
89}
90
91/// A job (pipeline)
92#[derive(Clone, Debug)]
93pub struct Job {
94    pub stat: u32,
95    pub gleader: i32,           // Process group leader
96    pub procs: Vec<Process>,    // Processes in job
97    pub auxprocs: Vec<Process>, // Auxiliary processes
98    pub other: usize,           // For superjobs: subjob index
99    pub filelist: Vec<String>,  // Temp files to delete
100    pub text: String,           // Job text for display
101}
102
103impl Job {
104    pub fn new() -> Self {
105        Job {
106            stat: 0,
107            gleader: 0,
108            procs: Vec::new(),
109            auxprocs: Vec::new(),
110            other: 0,
111            filelist: Vec::new(),
112            text: String::new(),
113        }
114    }
115
116    pub fn is_done(&self) -> bool {
117        (self.stat & stat::DONE) != 0
118    }
119
120    pub fn is_stopped(&self) -> bool {
121        (self.stat & stat::STOPPED) != 0
122    }
123
124    pub fn is_superjob(&self) -> bool {
125        (self.stat & stat::SUPERJOB) != 0
126    }
127
128    pub fn is_subjob(&self) -> bool {
129        (self.stat & stat::SUBJOB) != 0
130    }
131
132    pub fn is_inuse(&self) -> bool {
133        (self.stat & stat::INUSE) != 0
134    }
135
136    pub fn has_procs(&self) -> bool {
137        !self.procs.is_empty() || !self.auxprocs.is_empty()
138    }
139
140    pub fn make_running(&mut self) {
141        self.stat &= !stat::STOPPED;
142        for proc in &mut self.procs {
143            if proc.is_stopped() {
144                proc.status = SP_RUNNING;
145            }
146        }
147    }
148}
149
150impl Default for Job {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156/// Simple job info for exec.rs compatibility
157#[derive(Debug)]
158pub struct JobInfo {
159    pub id: usize,
160    pub pid: i32,
161    pub child: Option<Child>,
162    pub command: String,
163    pub state: JobState,
164    pub is_current: bool,
165}
166
167/// Job table compatible with exec.rs
168pub struct JobTable {
169    jobs: Vec<Option<JobInfo>>,
170    current_id: Option<usize>,
171    next_id: usize,
172}
173
174impl Default for JobTable {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180impl JobTable {
181    pub fn new() -> Self {
182        JobTable {
183            jobs: Vec::with_capacity(16),
184            current_id: None,
185            next_id: 1,
186        }
187    }
188
189    /// Peek at the next id that would be assigned by `add_job`/`add_pid`.
190    /// Used by `wait %N` to distinguish a never-issued id (clear user
191    /// error) from a job that was issued and already reaped (silent
192    /// success in zshrs to keep the `cmd & wait %1` idiom working
193    /// across the races introduced by the threaded job table).
194    pub fn peek_next_id(&self) -> usize {
195        self.next_id
196    }
197
198    /// Add a job with a Child process
199    pub fn add_job(&mut self, child: Child, command: String, state: JobState) -> usize {
200        let id = self.next_id;
201        self.next_id += 1;
202
203        let pid = child.id() as i32;
204        let job = JobInfo {
205            id,
206            pid,
207            child: Some(child),
208            command,
209            state,
210            is_current: true,
211        };
212
213        // Mark previous current as not current
214        if let Some(cur_id) = self.current_id {
215            if let Some(j) = self.get_mut_internal(cur_id) {
216                j.is_current = false;
217            }
218        }
219
220        // Add new job
221        let slot = self.get_free_slot();
222        if slot >= self.jobs.len() {
223            self.jobs.resize_with(slot + 1, || None);
224        }
225        self.jobs[slot] = Some(job);
226        self.current_id = Some(id);
227
228        id
229    }
230
231    fn get_free_slot(&self) -> usize {
232        for (i, slot) in self.jobs.iter().enumerate() {
233            if slot.is_none() {
234                return i;
235            }
236        }
237        self.jobs.len()
238    }
239
240    fn get_mut_internal(&mut self, id: usize) -> Option<&mut JobInfo> {
241        for job in self.jobs.iter_mut().flatten() {
242            if job.id == id {
243                return Some(job);
244            }
245        }
246        None
247    }
248
249    /// Get a job by ID
250    pub fn get(&self, id: usize) -> Option<&JobInfo> {
251        self.jobs
252            .iter()
253            .flatten()
254            .find(|&job| job.id == id)
255            .map(|v| v as _)
256    }
257
258    /// Get a mutable job by ID
259    pub fn get_mut(&mut self, id: usize) -> Option<&mut JobInfo> {
260        self.get_mut_internal(id)
261    }
262
263    /// Remove a job by ID
264    pub fn remove(&mut self, id: usize) -> Option<JobInfo> {
265        for slot in self.jobs.iter_mut() {
266            if slot.as_ref().map(|j| j.id == id).unwrap_or(false) {
267                let job = slot.take();
268                if self.current_id == Some(id) {
269                    self.current_id = None;
270                }
271                return job;
272            }
273        }
274        None
275    }
276
277    /// List all active jobs
278    pub fn list(&self) -> Vec<&JobInfo> {
279        self.jobs.iter().filter_map(|j| j.as_ref()).collect()
280    }
281
282    /// Iterate over jobs with their IDs (for compatibility)
283    pub fn iter(&self) -> impl Iterator<Item = (usize, &JobInfo)> {
284        self.jobs
285            .iter()
286            .filter_map(|j| j.as_ref().map(|job| (job.id, job)))
287    }
288
289    /// Count number of active jobs
290    pub fn count(&self) -> usize {
291        self.jobs.iter().filter(|j| j.is_some()).count()
292    }
293
294    /// Check if there are any jobs
295    pub fn is_empty(&self) -> bool {
296        self.count() == 0
297    }
298
299    /// Get current job
300    pub fn current(&self) -> Option<&JobInfo> {
301        self.current_id.and_then(|id| self.get(id))
302    }
303
304    /// Reap finished jobs (check for completed processes)
305    pub fn reap_finished(&mut self) -> Vec<JobInfo> {
306        let mut finished = Vec::new();
307
308        for slot in self.jobs.iter_mut() {
309            if let Some(job) = slot {
310                if let Some(ref mut child) = job.child {
311                    // Try to check if child has finished without blocking
312                    match child.try_wait() {
313                        Ok(Some(_status)) => {
314                            // Child finished
315                            job.state = JobState::Done;
316                        }
317                        Ok(None) => {
318                            // Still running
319                        }
320                        Err(_) => {
321                            // Error checking, assume done
322                            job.state = JobState::Done;
323                        }
324                    }
325                }
326            }
327        }
328
329        // Remove done jobs
330        for slot in self.jobs.iter_mut() {
331            if slot
332                .as_ref()
333                .map(|j| j.state == JobState::Done)
334                .unwrap_or(false)
335            {
336                if let Some(job) = slot.take() {
337                    finished.push(job);
338                }
339            }
340        }
341
342        finished
343    }
344}
345
346/// Format a job for display
347pub fn format_job(
348    job: &Job,
349    job_num: usize,
350    cur_job: Option<usize>,
351    prev_job: Option<usize>,
352) -> String {
353    let marker = if Some(job_num) == cur_job {
354        '+'
355    } else if Some(job_num) == prev_job {
356        '-'
357    } else {
358        ' '
359    };
360
361    let status = if job.is_done() {
362        "done"
363    } else if job.is_stopped() {
364        "suspended"
365    } else {
366        "running"
367    };
368
369    format!("[{}]{} {:10}  {}", job_num, marker, status, job.text)
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn test_process_new() {
378        let proc = Process::new(1234);
379        assert_eq!(proc.pid, 1234);
380        assert!(proc.is_running());
381    }
382
383    #[test]
384    fn test_job_new() {
385        let job = Job::new();
386        assert_eq!(job.stat, 0);
387        assert!(!job.is_done());
388        assert!(!job.is_stopped());
389    }
390
391    #[test]
392    fn test_job_table_new() {
393        let table = JobTable::new();
394        assert!(table.is_empty());
395    }
396
397    #[test]
398    fn test_job_table_remove() {
399        // This test would require spawning a real process, skipping for now
400    }
401
402    #[test]
403    fn test_job_make_running() {
404        let mut job = Job::new();
405        job.stat |= stat::STOPPED;
406        job.procs.push(Process {
407            status: 0x007f,
408            ..Process::new(1234)
409        }); // Stopped
410
411        job.make_running();
412        assert!(!job.is_stopped());
413        assert!(job.procs[0].is_running());
414    }
415
416    #[test]
417    fn test_format_job() {
418        let mut job = Job::new();
419        job.text = "vim file.txt".to_string();
420        job.stat |= stat::STOPPED;
421
422        let formatted = format_job(&job, 1, Some(1), None);
423        assert!(formatted.contains("[1]+"));
424        assert!(formatted.contains("suspended"));
425        assert!(formatted.contains("vim file.txt"));
426    }
427
428    #[test]
429    fn test_job_state_enum() {
430        let state = JobState::Running;
431        assert_eq!(state, JobState::Running);
432        assert_ne!(state, JobState::Stopped);
433        assert_ne!(state, JobState::Done);
434    }
435}
436
437/// Job state for simpler tracking
438#[derive(Clone, Copy, Debug, PartialEq, Eq)]
439pub enum JobState {
440    Running,
441    Stopped,
442    Done,
443}
444
445/// Simple job entry for executor compatibility
446#[derive(Debug)]
447pub struct JobEntry {
448    pub pid: i32,
449    pub child: Option<Child>,
450    pub command: String,
451    pub state: JobState,
452    pub is_current: bool,
453}
454
455/// Send a signal to a process
456#[cfg(unix)]
457pub fn send_signal(pid: i32, sig: nix::sys::signal::Signal) -> Result<(), String> {
458    use nix::sys::signal::kill;
459    use nix::unistd::Pid;
460
461    kill(Pid::from_raw(pid), sig).map_err(|e| e.to_string())
462}
463
464#[cfg(not(unix))]
465pub fn send_signal(_pid: i32, _sig: i32) -> Result<(), String> {
466    Err("Signal sending not supported on this platform".to_string())
467}
468
469/// Continue a stopped job
470#[cfg(unix)]
471pub fn continue_job(pid: i32) -> Result<(), String> {
472    use nix::sys::signal::{kill, Signal};
473    use nix::unistd::Pid;
474
475    kill(Pid::from_raw(pid), Signal::SIGCONT).map_err(|e| e.to_string())
476}
477
478#[cfg(not(unix))]
479pub fn continue_job(_pid: i32) -> Result<(), String> {
480    Err("Job control not supported on this platform".to_string())
481}
482
483/// Wait for a job to complete
484#[cfg(unix)]
485pub fn wait_for_job(pid: i32) -> Result<i32, String> {
486    use nix::sys::wait::{waitpid, WaitStatus};
487    use nix::unistd::Pid;
488
489    loop {
490        match waitpid(Pid::from_raw(pid), None) {
491            Ok(WaitStatus::Exited(_, code)) => return Ok(code),
492            Ok(WaitStatus::Signaled(_, sig, _)) => return Ok(128 + sig as i32),
493            Ok(WaitStatus::Stopped(_, _)) => return Ok(128),
494            Ok(_) => continue,
495            Err(nix::errno::Errno::ECHILD) => return Ok(0),
496            Err(e) => return Err(e.to_string()),
497        }
498    }
499}
500
501#[cfg(not(unix))]
502pub fn wait_for_job(_pid: i32) -> Result<i32, String> {
503    Err("Job waiting not supported on this platform".to_string())
504}
505
506/// Wait for a child process
507pub fn wait_for_child(child: &mut Child) -> Result<i32, String> {
508    match child.wait() {
509        Ok(status) => Ok(status.code().unwrap_or(0)),
510        Err(e) => Err(e.to_string()),
511    }
512}
513
514/// Get clock ticks per second (from jobs.c get_clktck lines 720-748)
515pub fn get_clktck() -> i64 {
516    #[cfg(unix)]
517    {
518        use std::sync::OnceLock;
519        static CLKTCK: OnceLock<i64> = OnceLock::new();
520        *CLKTCK.get_or_init(|| unsafe { libc::sysconf(libc::_SC_CLK_TCK) as i64 })
521    }
522    #[cfg(not(unix))]
523    {
524        100 // Default on non-Unix
525    }
526}
527
528/// Format time as hh:mm:ss.xx (from jobs.c printhhmmss lines 752-765)
529pub fn format_hhmmss(secs: f64) -> String {
530    let mins = (secs / 60.0) as i32;
531    let hours = mins / 60;
532    let secs = secs - (mins * 60) as f64;
533    let mins = mins - (hours * 60);
534
535    if hours > 0 {
536        format!("{}:{:02}:{:05.2}", hours, mins, secs)
537    } else if mins > 0 {
538        format!("{}:{:05.2}", mins, secs)
539    } else {
540        format!("{:.3}", secs)
541    }
542}
543
544/// Time format specifiers (from jobs.c printtime lines 768-949)
545pub fn format_time(
546    elapsed_secs: f64,
547    user_secs: f64,
548    system_secs: f64,
549    format: &str,
550    job_name: &str,
551) -> String {
552    let mut result = String::new();
553    let total_time = user_secs + system_secs;
554    let percent = if elapsed_secs > 0.0 {
555        (100.0 * total_time / elapsed_secs) as i32
556    } else {
557        0
558    };
559
560    let mut chars = format.chars().peekable();
561    while let Some(c) = chars.next() {
562        if c == '%' {
563            match chars.next() {
564                Some('E') => result.push_str(&format!("{:.2}s", elapsed_secs)),
565                Some('U') => result.push_str(&format!("{:.2}s", user_secs)),
566                Some('S') => result.push_str(&format!("{:.2}s", system_secs)),
567                Some('P') => result.push_str(&format!("{}%", percent)),
568                Some('J') => result.push_str(job_name),
569                Some('m') => match chars.next() {
570                    Some('E') => result.push_str(&format!("{:.0}ms", elapsed_secs * 1000.0)),
571                    Some('U') => result.push_str(&format!("{:.0}ms", user_secs * 1000.0)),
572                    Some('S') => result.push_str(&format!("{:.0}ms", system_secs * 1000.0)),
573                    _ => result.push_str("%m"),
574                },
575                Some('u') => match chars.next() {
576                    Some('E') => result.push_str(&format!("{:.0}us", elapsed_secs * 1_000_000.0)),
577                    Some('U') => result.push_str(&format!("{:.0}us", user_secs * 1_000_000.0)),
578                    Some('S') => result.push_str(&format!("{:.0}us", system_secs * 1_000_000.0)),
579                    _ => result.push_str("%u"),
580                },
581                Some('n') => match chars.next() {
582                    Some('E') => {
583                        result.push_str(&format!("{:.0}ns", elapsed_secs * 1_000_000_000.0))
584                    }
585                    Some('U') => result.push_str(&format!("{:.0}ns", user_secs * 1_000_000_000.0)),
586                    Some('S') => {
587                        result.push_str(&format!("{:.0}ns", system_secs * 1_000_000_000.0))
588                    }
589                    _ => result.push_str("%n"),
590                },
591                Some('*') => match chars.next() {
592                    Some('E') => result.push_str(&format_hhmmss(elapsed_secs)),
593                    Some('U') => result.push_str(&format_hhmmss(user_secs)),
594                    Some('S') => result.push_str(&format_hhmmss(system_secs)),
595                    _ => result.push_str("%*"),
596                },
597                Some('%') => result.push('%'),
598                Some(other) => {
599                    result.push('%');
600                    result.push(other);
601                }
602                None => result.push('%'),
603            }
604        } else {
605            result.push(c);
606        }
607    }
608    result
609}
610
611/// Default time format (from jobs.c DEFAULT_TIMEFMT)
612pub const DEFAULT_TIMEFMT: &str = "%J  %U user %S system %P cpu %*E total";
613
614/// Time a command's execution
615pub struct CommandTimer {
616    start: std::time::Instant,
617    job_name: String,
618}
619
620impl CommandTimer {
621    pub fn new(job_name: &str) -> Self {
622        CommandTimer {
623            start: std::time::Instant::now(),
624            job_name: job_name.to_string(),
625        }
626    }
627
628    pub fn elapsed(&self) -> Duration {
629        self.start.elapsed()
630    }
631
632    pub fn format(
633        &self,
634        user_time: Duration,
635        sys_time: Duration,
636        format_str: Option<&str>,
637    ) -> String {
638        let elapsed = self.start.elapsed().as_secs_f64();
639        let user = user_time.as_secs_f64();
640        let sys = sys_time.as_secs_f64();
641
642        format_time(
643            elapsed,
644            user,
645            sys,
646            format_str.unwrap_or(DEFAULT_TIMEFMT),
647            &self.job_name,
648        )
649    }
650}
651
652/// Pipestats management (from jobs.c storepipestats lines 420-454)
653pub struct PipeStats {
654    stats: Vec<i32>,
655}
656
657impl Default for PipeStats {
658    fn default() -> Self {
659        Self::new()
660    }
661}
662
663impl PipeStats {
664    pub fn new() -> Self {
665        PipeStats { stats: Vec::new() }
666    }
667
668    pub fn clear(&mut self) {
669        self.stats.clear();
670    }
671
672    pub fn add(&mut self, status: i32) {
673        if self.stats.len() < MAX_PIPESTATS {
674            self.stats.push(status);
675        }
676    }
677
678    pub fn get(&self) -> &[i32] {
679        &self.stats
680    }
681
682    pub fn len(&self) -> usize {
683        self.stats.len()
684    }
685
686    pub fn is_empty(&self) -> bool {
687        self.stats.is_empty()
688    }
689
690    pub fn pipefail_status(&self) -> i32 {
691        *self.stats.iter().rev().find(|&&s| s != 0).unwrap_or(&0)
692    }
693}
694
695/// Signal message lookup (from jobs.c sigmsg lines 1106-1118)
696pub fn sigmsg(sig: i32) -> &'static str {
697    match sig {
698        libc::SIGHUP => "hangup",
699        libc::SIGINT => "interrupt",
700        libc::SIGQUIT => "quit",
701        libc::SIGILL => "illegal instruction",
702        libc::SIGTRAP => "trace trap",
703        libc::SIGABRT => "abort",
704        libc::SIGBUS => "bus error",
705        libc::SIGFPE => "floating point exception",
706        libc::SIGKILL => "killed",
707        libc::SIGUSR1 => "user-defined signal 1",
708        libc::SIGSEGV => "segmentation fault",
709        libc::SIGUSR2 => "user-defined signal 2",
710        libc::SIGPIPE => "broken pipe",
711        libc::SIGALRM => "alarm",
712        libc::SIGTERM => "terminated",
713        libc::SIGCHLD => "child exited",
714        libc::SIGCONT => "continued",
715        libc::SIGSTOP => "stopped (signal)",
716        libc::SIGTSTP => "stopped",
717        libc::SIGTTIN => "stopped (tty input)",
718        libc::SIGTTOU => "stopped (tty output)",
719        libc::SIGURG => "urgent I/O condition",
720        libc::SIGXCPU => "CPU time exceeded",
721        libc::SIGXFSZ => "file size exceeded",
722        libc::SIGVTALRM => "virtual timer expired",
723        libc::SIGPROF => "profiling timer expired",
724        libc::SIGWINCH => "window changed",
725        libc::SIGIO => "I/O ready",
726        libc::SIGSYS => "bad system call",
727        _ => "unknown signal",
728    }
729}
730
731/// Format process status for display (from jobs.c printjob lines 1136-1400)
732pub fn format_process_status(status: i32) -> String {
733    if status == SP_RUNNING {
734        "running".to_string()
735    } else if (status & 0x7f) == 0 {
736        // Exited normally
737        let code = (status >> 8) & 0xff;
738        if code == 0 {
739            "done".to_string()
740        } else {
741            format!("exit {}", code)
742        }
743    } else if (status & 0xff) == 0x7f {
744        // Stopped
745        let sig = (status >> 8) & 0xff;
746        format!("suspended ({})", sigmsg(sig))
747    } else {
748        // Signaled
749        let sig = status & 0x7f;
750        let core = (status >> 7) & 1;
751        if core != 0 {
752            format!("{} (core dumped)", sigmsg(sig))
753        } else {
754            sigmsg(sig).to_string()
755        }
756    }
757}
758
759/// Print job in long format (from jobs.c printjob)
760pub fn format_job_long(
761    job_num: usize,
762    current: bool,
763    pid: i32,
764    status: &str,
765    text: &str,
766) -> String {
767    let marker = if current { '+' } else { '-' };
768    format!("[{}]  {} {:>5} {}  {}", job_num, marker, pid, status, text)
769}
770
771/// Print job in short format
772pub fn format_job_short(job_num: usize, current: bool, status: &str, text: &str) -> String {
773    let marker = if current { '+' } else { '-' };
774    format!("[{}]  {} {}  {}", job_num, marker, status, text)
775}
776
777/// Background status tracking (from jobs.c bgstatus)
778pub struct BgStatus {
779    statuses: std::collections::HashMap<i32, i32>,
780}
781
782impl Default for BgStatus {
783    fn default() -> Self {
784        Self::new()
785    }
786}
787
788impl BgStatus {
789    pub fn new() -> Self {
790        BgStatus {
791            statuses: std::collections::HashMap::new(),
792        }
793    }
794
795    pub fn add(&mut self, pid: i32, status: i32) {
796        self.statuses.insert(pid, status);
797    }
798
799    pub fn get(&self, pid: i32) -> Option<i32> {
800        self.statuses.get(&pid).copied()
801    }
802
803    pub fn remove(&mut self, pid: i32) -> Option<i32> {
804        self.statuses.remove(&pid)
805    }
806
807    pub fn clear(&mut self) {
808        self.statuses.clear();
809    }
810}
811
812/// Wait for a specific PID (from jobs.c waitforpid lines 1627-1663)
813pub fn waitforpid(pid: i32) -> Option<i32> {
814    #[cfg(unix)]
815    {
816        use std::os::unix::process::ExitStatusExt;
817        loop {
818            let mut status: i32 = 0;
819            let result = unsafe { libc::waitpid(pid, &mut status, 0) };
820            if result == pid {
821                if libc::WIFEXITED(status) {
822                    return Some(libc::WEXITSTATUS(status));
823                } else if libc::WIFSIGNALED(status) {
824                    return Some(128 + libc::WTERMSIG(status));
825                } else if libc::WIFSTOPPED(status) {
826                    return None;
827                }
828            } else if result == -1 {
829                return None;
830            }
831        }
832    }
833    #[cfg(not(unix))]
834    {
835        let _ = pid;
836        None
837    }
838}
839
840/// Wait for job (from jobs.c zwaitjob lines 1673-1750)
841pub fn waitjob(job: &mut Job) -> Option<i32> {
842    if job.procs.is_empty() {
843        return Some(0);
844    }
845
846    let mut last_status = 0;
847    for proc in &mut job.procs {
848        if proc.is_running() {
849            if let Some(status) = waitforpid(proc.pid) {
850                proc.status = make_status(status);
851                last_status = status;
852            }
853        } else {
854            last_status = proc.exit_status();
855        }
856    }
857
858    job.stat |= stat::DONE;
859    Some(last_status)
860}
861
862/// Make status from exit code
863pub fn make_status(code: i32) -> i32 {
864    code << 8
865}
866
867/// Make status from signal
868pub fn make_signal_status(sig: i32) -> i32 {
869    sig
870}
871
872/// Check if job has pending children (from jobs.c havefiles lines 1604-1616)
873pub fn havefiles(job: &Job) -> bool {
874    !job.filelist.is_empty()
875}
876
877/// Delete job (from jobs.c deletejob lines 1511-1526)
878pub fn deletejob(job: &mut Job, disowning: bool) {
879    if !disowning {
880        job.filelist.clear();
881    }
882    job.procs.clear();
883    job.auxprocs.clear();
884    job.stat = 0;
885}
886
887/// Free job (from jobs.c freejob lines 1456-1508)
888pub fn freejob(job: &mut Job, notify: bool) {
889    let _ = notify;
890    job.procs.clear();
891    job.auxprocs.clear();
892    job.filelist.clear();
893    job.stat = 0;
894    job.gleader = 0;
895    job.text.clear();
896}
897
898/// Add process to job (from jobs.c addproc lines 1537-1597)
899pub fn addproc(job: &mut Job, pid: i32, text: &str, aux: bool) {
900    let proc = Process::new(pid);
901    let proc = Process {
902        pid,
903        status: SP_RUNNING,
904        text: text.to_string(),
905        ..proc
906    };
907
908    if aux {
909        job.auxprocs.push(proc);
910    } else {
911        if job.gleader == 0 {
912            job.gleader = pid;
913        }
914        job.procs.push(proc);
915    }
916
917    job.stat &= !stat::DONE;
918}
919
920/// Kill process group (from jobs.c killjob lines 2040-2085)
921pub fn killjob(job: &Job, sig: i32) -> bool {
922    #[cfg(unix)]
923    {
924        if job.gleader > 0 {
925            let result = unsafe { libc::killpg(job.gleader, sig) };
926            return result == 0;
927        }
928
929        let mut success = true;
930        for proc in &job.procs {
931            if proc.is_running() {
932                let result = unsafe { libc::kill(proc.pid, sig) };
933                if result != 0 {
934                    success = false;
935                }
936            }
937        }
938        success
939    }
940    #[cfg(not(unix))]
941    {
942        let _ = (job, sig);
943        false
944    }
945}
946
947/// Continue job in foreground (from jobs.c fg)
948pub fn fg_job(job: &mut Job) -> Option<i32> {
949    #[cfg(unix)]
950    {
951        if (job.stat & stat::STOPPED) != 0 {
952            if job.gleader > 0 {
953                unsafe { libc::killpg(job.gleader, libc::SIGCONT) };
954            } else {
955                for proc in &job.procs {
956                    unsafe { libc::kill(proc.pid, libc::SIGCONT) };
957                }
958            }
959            job.stat &= !stat::STOPPED;
960        }
961
962        waitjob(job)
963    }
964    #[cfg(not(unix))]
965    {
966        let _ = job;
967        None
968    }
969}
970
971/// Continue job in background (from jobs.c bg)
972pub fn bg_job(job: &mut Job) -> bool {
973    #[cfg(unix)]
974    {
975        if (job.stat & stat::STOPPED) != 0 {
976            if job.gleader > 0 {
977                unsafe { libc::killpg(job.gleader, libc::SIGCONT) };
978            } else {
979                for proc in &job.procs {
980                    unsafe { libc::kill(proc.pid, libc::SIGCONT) };
981                }
982            }
983            job.stat &= !stat::STOPPED;
984            return true;
985        }
986        false
987    }
988    #[cfg(not(unix))]
989    {
990        let _ = job;
991        false
992    }
993}
994
995/// Disown job (from jobs.c disown)
996pub fn disown_job(job: &mut Job) {
997    job.stat |= stat::DISOWN;
998}
999
1000/// Check if all processes in job are done
1001pub fn job_is_done(job: &Job) -> bool {
1002    (job.stat & stat::DONE) != 0 || job.procs.iter().all(|p| !p.is_running())
1003}
1004
1005/// Check if job is stopped
1006pub fn job_is_stopped(job: &Job) -> bool {
1007    (job.stat & stat::STOPPED) != 0 || job.procs.iter().any(|p| p.is_stopped())
1008}
1009
1010/// Get job text (combined process commands)
1011pub fn get_job_text(job: &Job) -> String {
1012    if !job.text.is_empty() {
1013        return job.text.clone();
1014    }
1015    job.procs
1016        .iter()
1017        .map(|p| p.text.as_str())
1018        .collect::<Vec<_>>()
1019        .join(" | ")
1020}
1021
1022/// Super job tracking (from jobs.c super_job lines 393-417)
1023pub fn super_job(jobtab: &[Job], job_idx: usize) -> Option<usize> {
1024    for (i, job) in jobtab.iter().enumerate() {
1025        if (job.stat & stat::SUPERJOB) != 0 && job.other == job_idx {
1026            return Some(i);
1027        }
1028    }
1029    None
1030}
1031
1032/// Set current/previous job (from jobs.c setjobpwn lines 697-745)
1033pub struct JobPointers {
1034    pub cur_job: Option<usize>,
1035    pub prev_job: Option<usize>,
1036}
1037
1038impl JobPointers {
1039    pub fn new() -> Self {
1040        JobPointers {
1041            cur_job: None,
1042            prev_job: None,
1043        }
1044    }
1045
1046    pub fn set_current(&mut self, job: usize) {
1047        if Some(job) != self.cur_job {
1048            self.prev_job = self.cur_job;
1049            self.cur_job = Some(job);
1050        }
1051    }
1052
1053    pub fn clear(&mut self, job: usize) {
1054        if self.cur_job == Some(job) {
1055            self.cur_job = self.prev_job;
1056            self.prev_job = None;
1057        } else if self.prev_job == Some(job) {
1058            self.prev_job = None;
1059        }
1060    }
1061}
1062
1063impl Default for JobPointers {
1064    fn default() -> Self {
1065        Self::new()
1066    }
1067}
1068
1069// ---------------------------------------------------------------------------
1070// Missing functions from jobs.c
1071// ---------------------------------------------------------------------------
1072
1073/// Parse job specification string (from jobs.c getjob lines 2063-2165)
1074///
1075/// Syntax: %N (by number), %+ or %% (current), %- (previous),
1076/// %str (by command prefix), %?str (by substring)
1077pub fn getjob(spec: &str, table: &JobTable, ptrs: &JobPointers) -> Option<usize> {
1078    if spec.is_empty() {
1079        return ptrs.cur_job;
1080    }
1081
1082    let spec = if spec.starts_with('%') {
1083        &spec[1..]
1084    } else {
1085        spec
1086    };
1087
1088    match spec {
1089        "+" | "%" | "" => ptrs.cur_job,
1090        "-" => ptrs.prev_job,
1091        _ => {
1092            // Try as number
1093            if let Ok(n) = spec.parse::<usize>() {
1094                if table.get(n).is_some() {
1095                    return Some(n);
1096                }
1097                return None;
1098            }
1099
1100            // ?string - search by substring
1101            if let Some(substr) = spec.strip_prefix('?') {
1102                for (id, job) in table.iter() {
1103                    if job.command.contains(substr) {
1104                        return Some(id);
1105                    }
1106                }
1107                return None;
1108            }
1109
1110            // string - search by prefix
1111            for (id, job) in table.iter() {
1112                if job.command.starts_with(spec) {
1113                    return Some(id);
1114                }
1115            }
1116
1117            None
1118        }
1119    }
1120}
1121
1122/// Find job by command name (from jobs.c findjobnam)
1123pub fn findjobnam(name: &str, table: &JobTable) -> Option<usize> {
1124    for (id, job) in table.iter() {
1125        if job.command == name {
1126            return Some(id);
1127        }
1128    }
1129    None
1130}
1131
1132/// Check if string is a number (from jobs.c isanum)
1133pub fn isanum(s: &str) -> bool {
1134    !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
1135}
1136
1137/// Initialize jobs subsystem (from jobs.c init_jobs)
1138pub fn init_jobs() -> (JobTable, JobPointers) {
1139    (JobTable::new(), JobPointers::new())
1140}
1141
1142/// Acquire process group (from jobs.c acquire_pgrp)
1143#[cfg(unix)]
1144pub fn acquire_pgrp() -> bool {
1145    unsafe {
1146        let mypgrp = libc::getpgrp();
1147        let tpgrp = libc::tcgetpgrp(0);
1148        if tpgrp == -1 || tpgrp == mypgrp {
1149            return true;
1150        }
1151        // We need to be in the foreground process group
1152        if libc::setpgid(0, 0) == 0 {
1153            libc::tcsetpgrp(0, libc::getpgrp());
1154            return true;
1155        }
1156        false
1157    }
1158}
1159
1160/// Store pipestats from job (from jobs.c storepipestats)
1161pub fn storepipestats(job: &Job) -> Vec<i32> {
1162    job.procs.iter().map(|p| p.status).collect()
1163}
1164
1165/// Clear the job table (from jobs.c clearjobtab)
1166pub fn clearjobtab(table: &mut JobTable, ptrs: &mut JobPointers) {
1167    table.jobs.clear();
1168    table.next_id = 1;
1169    ptrs.cur_job = None;
1170    ptrs.prev_job = None;
1171}
1172
1173/// Scan jobs and print changed status (from jobs.c scanjobs)
1174pub fn scanjobs(table: &JobTable) -> Vec<String> {
1175    let mut output = Vec::new();
1176    for (id, job) in table.iter() {
1177        let state_str = match job.state {
1178            JobState::Running => "running",
1179            JobState::Done => "done",
1180            JobState::Stopped => "stopped",
1181            _ => "unknown",
1182        };
1183        output.push(format!("[{}]  {}  {}", id, state_str, job.command));
1184    }
1185    output
1186}
1187
1188/// Shell time accounting (from jobs.c shelltime)
1189#[derive(Debug, Clone, Default)]
1190pub struct ChildTimes {
1191    pub user_sec: f64,
1192    pub sys_sec: f64,
1193}
1194
1195pub fn shelltime() -> ChildTimes {
1196    #[cfg(unix)]
1197    {
1198        let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
1199        if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 {
1200            return ChildTimes {
1201                user_sec: usage.ru_utime.tv_sec as f64
1202                    + usage.ru_utime.tv_usec as f64 / 1_000_000.0,
1203                sys_sec: usage.ru_stime.tv_sec as f64 + usage.ru_stime.tv_usec as f64 / 1_000_000.0,
1204            };
1205        }
1206    }
1207    ChildTimes::default()
1208}
1209
1210/// Get children's time accounting
1211pub fn childtime() -> ChildTimes {
1212    #[cfg(unix)]
1213    {
1214        let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
1215        if unsafe { libc::getrusage(libc::RUSAGE_CHILDREN, &mut usage) } == 0 {
1216            return ChildTimes {
1217                user_sec: usage.ru_utime.tv_sec as f64
1218                    + usage.ru_utime.tv_usec as f64 / 1_000_000.0,
1219                sys_sec: usage.ru_stime.tv_sec as f64 + usage.ru_stime.tv_usec as f64 / 1_000_000.0,
1220            };
1221        }
1222    }
1223    ChildTimes::default()
1224}
1225
1226/// Update process status after waitpid (from jobs.c update_process)
1227pub fn update_process(proc: &mut Process, status: i32) {
1228    proc.end_time = Some(Instant::now());
1229    proc.status = status;
1230}
1231
1232/// Find a process by PID in the job table (from jobs.c findproc)
1233pub fn findproc(jobtab: &[Job], pid: i32) -> Option<(usize, usize, bool)> {
1234    for (ji, job) in jobtab.iter().enumerate() {
1235        for (pi, proc) in job.procs.iter().enumerate() {
1236            if proc.pid == pid {
1237                return Some((ji, pi, false));
1238            }
1239        }
1240        for (pi, proc) in job.auxprocs.iter().enumerate() {
1241            if proc.pid == pid {
1242                return Some((ji, pi, true));
1243            }
1244        }
1245    }
1246    None
1247}
1248
1249/// Update job status after process change (from jobs.c update_job)
1250pub fn update_job(job: &mut Job) -> bool {
1251    // Check if all aux procs are done
1252    for proc in &job.auxprocs {
1253        if proc.is_running() {
1254            return false;
1255        }
1256    }
1257
1258    // Check main processes
1259    let all_done = true;
1260    let mut some_stopped = false;
1261    let mut last_status = 0;
1262
1263    for proc in &job.procs {
1264        if proc.is_running() {
1265            return false; // Still running
1266        }
1267        if proc.is_stopped() {
1268            some_stopped = true;
1269        }
1270    }
1271
1272    // Get last process status
1273    if let Some(last) = job.procs.last() {
1274        if last.is_signaled() {
1275            last_status = 0x80 | last.term_sig();
1276        } else if last.is_stopped() {
1277            last_status = 0x80 | last.stop_sig();
1278        } else {
1279            last_status = last.exit_status();
1280        }
1281    }
1282
1283    if some_stopped {
1284        job.stat |= stat::STOPPED;
1285        job.stat &= !stat::DONE;
1286    } else {
1287        job.stat |= stat::DONE;
1288        job.stat &= !stat::STOPPED;
1289    }
1290
1291    true
1292}
1293
1294/// Update a background job after waitpid (from jobs.c update_bg_job)
1295pub fn update_bg_job(jobtab: &mut [Job], pid: i32, status: i32) -> bool {
1296    if let Some((ji, pi, is_aux)) = findproc(jobtab, pid) {
1297        if is_aux {
1298            jobtab[ji].auxprocs[pi].status = status;
1299            jobtab[ji].auxprocs[pi].end_time = Some(Instant::now());
1300        } else {
1301            jobtab[ji].procs[pi].status = status;
1302            jobtab[ji].procs[pi].end_time = Some(Instant::now());
1303        }
1304        update_job(&mut jobtab[ji]);
1305        return true;
1306    }
1307    false
1308}
1309
1310/// Handle subjob completion (from jobs.c handle_sub)
1311pub fn handle_sub(jobtab: &mut [Job], super_idx: usize, fg: bool) {
1312    let sub_idx = jobtab[super_idx].other;
1313    if sub_idx >= jobtab.len() {
1314        return;
1315    }
1316
1317    // If subjob is done, mark superjob accordingly
1318    if jobtab[sub_idx].is_done() {
1319        if fg {
1320            // Get the last status from the subjob
1321        }
1322        jobtab[super_idx].stat &= !stat::SUPERJOB;
1323        jobtab[super_idx].stat |= stat::WASSUPER;
1324    }
1325}
1326
1327/// Set the previous job (from jobs.c setprevjob)
1328pub fn setprevjob(ptrs: &mut JobPointers, jobtab: &[Job], maxjob: usize) {
1329    // Find a stopped or running job that isn't the current job
1330    let mut best = None;
1331    for i in (1..=maxjob).rev() {
1332        if i >= jobtab.len() {
1333            continue;
1334        }
1335        let job = &jobtab[i];
1336        if (job.stat & stat::INUSE) != 0 && Some(i) != ptrs.cur_job {
1337            if (job.stat & stat::STOPPED) != 0 {
1338                best = Some(i);
1339                break;
1340            }
1341            if best.is_none() {
1342                best = Some(i);
1343            }
1344        }
1345    }
1346    ptrs.prev_job = best;
1347}
1348
1349/// Set current job after state change (from jobs.c setcurjob)
1350pub fn setcurjob(ptrs: &mut JobPointers, jobtab: &[Job], maxjob: usize) {
1351    ptrs.cur_job = None;
1352    for i in (1..=maxjob).rev() {
1353        if i >= jobtab.len() {
1354            continue;
1355        }
1356        if (jobtab[i].stat & (stat::INUSE | stat::STOPPED)) == (stat::INUSE | stat::STOPPED) {
1357            ptrs.cur_job = Some(i);
1358            break;
1359        }
1360    }
1361    if ptrs.cur_job.is_none() {
1362        for i in (1..=maxjob).rev() {
1363            if i >= jobtab.len() {
1364                continue;
1365            }
1366            if (jobtab[i].stat & stat::INUSE) != 0 {
1367                ptrs.cur_job = Some(i);
1368                break;
1369            }
1370        }
1371    }
1372    setprevjob(ptrs, jobtab, maxjob);
1373}
1374
1375/// Check if a job's time should be reported (from jobs.c should_report_time)
1376pub fn should_report_time(job: &Job, reporttime: f64) -> bool {
1377    if reporttime < 0.0 {
1378        return false;
1379    }
1380    if let Some(first) = job.procs.first() {
1381        if let (Some(start), Some(end)) =
1382            (first.start_time, job.procs.last().and_then(|p| p.end_time))
1383        {
1384            let elapsed = end.duration_since(start).as_secs_f64();
1385            return elapsed >= reporttime;
1386        }
1387    }
1388    false
1389}
1390
1391/// Dump timing info for a job (from jobs.c dumptime)
1392pub fn dumptime(job: &Job, format: &str) -> Option<String> {
1393    let first_start = job.procs.first()?.start_time?;
1394    let last_end = job.procs.last()?.end_time?;
1395    let elapsed = last_end.duration_since(first_start).as_secs_f64();
1396
1397    let mut total_user = 0.0;
1398    let mut total_sys = 0.0;
1399    for proc in &job.procs {
1400        total_user += proc.ti.user_time.as_secs_f64();
1401        total_sys += proc.ti.sys_time.as_secs_f64();
1402    }
1403
1404    Some(format_time(
1405        elapsed,
1406        total_user,
1407        total_sys,
1408        format,
1409        &get_job_text(job),
1410    ))
1411}
1412
1413/// Wait for all foreground jobs to finish (from jobs.c waitjobs)
1414pub fn waitjobs(jobtab: &mut Vec<Job>, thisjob: usize) {
1415    if thisjob < jobtab.len() {
1416        while !jobtab[thisjob].is_done() && !jobtab[thisjob].is_stopped() {
1417            #[cfg(unix)]
1418            {
1419                let mut status: i32 = 0;
1420                let pid = unsafe { libc::waitpid(-1, &mut status, libc::WUNTRACED) };
1421                if pid > 0 {
1422                    update_bg_job(jobtab, pid, status);
1423                } else {
1424                    break;
1425                }
1426            }
1427            #[cfg(not(unix))]
1428            {
1429                break;
1430            }
1431        }
1432    }
1433}
1434
1435/// Wait for a single specific job (from jobs.c waitonejob)
1436pub fn waitonejob(job: &mut Job) {
1437    for proc in &mut job.procs {
1438        if proc.is_running() {
1439            if let Some(_status) = waitforpid(proc.pid) {
1440                // status already updated by waitforpid
1441            }
1442        }
1443    }
1444}
1445
1446/// Initialize a new job entry (from jobs.c initjob)
1447pub fn initjob(jobtab: &mut Vec<Job>) -> usize {
1448    // Find an empty slot or add a new one
1449    for (i, job) in jobtab.iter().enumerate() {
1450        if (job.stat & stat::INUSE) == 0 {
1451            jobtab[i] = Job::new();
1452            jobtab[i].stat = stat::INUSE;
1453            return i;
1454        }
1455    }
1456    // Expand table
1457    let idx = jobtab.len();
1458    let mut job = Job::new();
1459    job.stat = stat::INUSE;
1460    jobtab.push(job);
1461    idx
1462}
1463
1464/// Set the pwd for a job (from jobs.c setjobpwd)
1465pub fn setjobpwd(job: &mut Job) {
1466    // Store current directory in job for display purposes
1467    if let Ok(cwd) = std::env::current_dir() {
1468        // Job text sometimes includes the directory
1469        let _ = cwd;
1470    }
1471}
1472
1473/// Spawn a job (mark as started, from jobs.c spawnjob)
1474pub fn spawnjob(job: &mut Job, fg: bool) {
1475    job.stat |= stat::INUSE;
1476    if !fg {
1477        // Background job
1478        job.stat &= !stat::CURSH;
1479    }
1480}
1481
1482/// Select which job table to use (from jobs.c selectjobtab)
1483/// Returns (table_ref, max_job_index)
1484pub fn selectjobtab(jobtab: &[Job]) -> usize {
1485    // Find the maximum used job index
1486    let mut max = 0;
1487    for (i, job) in jobtab.iter().enumerate() {
1488        if (job.stat & stat::INUSE) != 0 {
1489            max = i;
1490        }
1491    }
1492    max
1493}
1494
1495/// Expand job table if needed (from jobs.c expandjobtab)
1496pub fn expandjobtab(jobtab: &mut Vec<Job>, needed: usize) {
1497    while jobtab.len() <= needed {
1498        jobtab.push(Job::new());
1499    }
1500}
1501
1502/// Shrink job table if possible (from jobs.c maybeshrinkjobtab)
1503pub fn maybeshrinkjobtab(jobtab: &mut Vec<Job>) {
1504    while jobtab
1505        .last()
1506        .map(|j| (j.stat & stat::INUSE) == 0)
1507        .unwrap_or(false)
1508    {
1509        jobtab.pop();
1510    }
1511}
1512
1513/// Add file to job's temp file list (from jobs.c addfilelist)
1514pub fn addfilelist(job: &mut Job, filename: &str) {
1515    job.filelist.push(filename.to_string());
1516}
1517
1518/// Clean temp files for process substitution (from jobs.c pipecleanfilelist)
1519pub fn pipecleanfilelist(job: &mut Job, proc_subst_only: bool) {
1520    if proc_subst_only {
1521        // Only remove process substitution files (those starting with /dev/fd or /proc)
1522        job.filelist
1523            .retain(|f| !f.starts_with("/dev/fd/") && !f.starts_with("/proc/"));
1524    } else {
1525        for file in &job.filelist {
1526            let _ = std::fs::remove_file(file);
1527        }
1528        job.filelist.clear();
1529    }
1530}
1531
1532/// Delete temp files from a job (from jobs.c deletefilelist)
1533pub fn deletefilelist(job: &mut Job, disowning: bool) {
1534    if !disowning {
1535        for file in &job.filelist {
1536            let _ = std::fs::remove_file(file);
1537        }
1538    }
1539    job.filelist.clear();
1540}
1541
1542/// Print job with full detail (from jobs.c printjob)
1543pub fn printjob(
1544    job: &Job,
1545    job_num: usize,
1546    long_format: bool,
1547    cur_job: Option<usize>,
1548    prev_job: Option<usize>,
1549) -> String {
1550    let marker = if Some(job_num) == cur_job {
1551        '+'
1552    } else if Some(job_num) == prev_job {
1553        '-'
1554    } else {
1555        ' '
1556    };
1557
1558    let status_str = if job.is_done() {
1559        if let Some(last) = job.procs.last() {
1560            format_process_status(last.status)
1561        } else {
1562            "done".to_string()
1563        }
1564    } else if job.is_stopped() {
1565        "suspended".to_string()
1566    } else {
1567        "running".to_string()
1568    };
1569
1570    if long_format {
1571        let mut lines = Vec::new();
1572        for (i, proc) in job.procs.iter().enumerate() {
1573            let pstatus = format_process_status(proc.status);
1574            if i == 0 {
1575                lines.push(format!(
1576                    "[{}]  {} {:>5} {:16}  {}",
1577                    job_num, marker, proc.pid, pstatus, proc.text
1578                ));
1579            } else {
1580                lines.push(format!(
1581                    "            {:>5} {:16}  | {}",
1582                    proc.pid, pstatus, proc.text
1583                ));
1584            }
1585        }
1586        lines.join("\n")
1587    } else {
1588        format!(
1589            "[{}]  {} {:16}  {}",
1590            job_num,
1591            marker,
1592            status_str,
1593            get_job_text(job)
1594        )
1595    }
1596}
1597
1598/// Get the signal name for signal-based job output (from jobs.c getsigname)
1599pub fn getsigname(sig: i32) -> String {
1600    match sig {
1601        0 => "EXIT".to_string(),
1602        libc::SIGHUP => "HUP".to_string(),
1603        libc::SIGINT => "INT".to_string(),
1604        libc::SIGQUIT => "QUIT".to_string(),
1605        libc::SIGILL => "ILL".to_string(),
1606        libc::SIGTRAP => "TRAP".to_string(),
1607        libc::SIGABRT => "ABRT".to_string(),
1608        libc::SIGBUS => "BUS".to_string(),
1609        libc::SIGFPE => "FPE".to_string(),
1610        libc::SIGKILL => "KILL".to_string(),
1611        libc::SIGUSR1 => "USR1".to_string(),
1612        libc::SIGSEGV => "SEGV".to_string(),
1613        libc::SIGUSR2 => "USR2".to_string(),
1614        libc::SIGPIPE => "PIPE".to_string(),
1615        libc::SIGALRM => "ALRM".to_string(),
1616        libc::SIGTERM => "TERM".to_string(),
1617        libc::SIGCHLD => "CHLD".to_string(),
1618        libc::SIGCONT => "CONT".to_string(),
1619        libc::SIGSTOP => "STOP".to_string(),
1620        libc::SIGTSTP => "TSTP".to_string(),
1621        libc::SIGTTIN => "TTIN".to_string(),
1622        libc::SIGTTOU => "TTOU".to_string(),
1623        libc::SIGURG => "URG".to_string(),
1624        libc::SIGXCPU => "XCPU".to_string(),
1625        libc::SIGXFSZ => "XFSZ".to_string(),
1626        libc::SIGVTALRM => "VTALRM".to_string(),
1627        libc::SIGPROF => "PROF".to_string(),
1628        libc::SIGWINCH => "WINCH".to_string(),
1629        libc::SIGIO => "IO".to_string(),
1630        libc::SIGSYS => "SYS".to_string(),
1631        _ => format!("SIG{}", sig),
1632    }
1633}
1634
1635/// Time difference for timeval (from jobs.c dtime_tv)
1636pub fn dtime_tv(dt: &mut Duration, t1: &Duration, t2: &Duration) -> Duration {
1637    if *t2 > *t1 {
1638        *dt = *t2 - *t1;
1639    } else {
1640        *dt = Duration::ZERO;
1641    }
1642    *dt
1643}
1644
1645/// Time difference for timespec (from jobs.c dtime_ts)
1646pub fn dtime_ts(t1: &Instant, t2: &Instant) -> Duration {
1647    if *t2 > *t1 {
1648        t2.duration_since(*t1)
1649    } else {
1650        Duration::ZERO
1651    }
1652}
1653
1654/// Make all job processes running (from jobs.c makerunning)
1655pub fn makerunning(job: &mut Job) {
1656    job.make_running();
1657}
1658
1659/// Check if job has any processes (from jobs.c hasprocs)
1660pub fn hasprocs(job: &Job) -> bool {
1661    job.has_procs()
1662}
1663
1664/// Get resource usage info (from jobs.c get_usage)
1665pub fn get_usage() -> ChildTimes {
1666    childtime()
1667}
1668
1669/// Check current shell signals (from jobs.c check_cursh_sig)
1670#[cfg(unix)]
1671pub fn check_cursh_sig(jobtab: &[Job], sig: i32) {
1672    for job in jobtab {
1673        if (job.stat & stat::CURSH) != 0 && !job.is_done() {
1674            for proc in &job.procs {
1675                if proc.is_running() {
1676                    unsafe {
1677                        libc::kill(proc.pid, sig);
1678                    }
1679                }
1680            }
1681        }
1682    }
1683}
1684
1685/// Clean all file lists from jobs (from jobs.c cleanfilelists)
1686pub fn cleanfilelists(jobtab: &mut [Job]) {
1687    for job in jobtab.iter_mut() {
1688        deletefilelist(job, false);
1689    }
1690}
1691
1692/// Clear old job table entries (from jobs.c clearoldjobtab)
1693pub fn clearoldjobtab(jobtab: &mut Vec<Job>) {
1694    jobtab.retain(|j| (j.stat & stat::INUSE) != 0);
1695}
1696
1697/// Add background status (from jobs.c addbgstatus)
1698pub fn addbgstatus(bg: &mut BgStatus, pid: i32, status_val: i32) {
1699    bg.add(pid, status_val);
1700}
1701
1702/// Get background status (from jobs.c getbgstatus)
1703pub fn getbgstatus(bg: &mut BgStatus, pid: i32) -> Option<i32> {
1704    bg.remove(pid)
1705}
1706
1707/// Get trap node for signal (from jobs.c gettrapnode) - defers to signals module
1708pub fn gettrapnode(sig: i32) -> Option<String> {
1709    // This is actually in signals.rs - provide a bridge
1710    let _ = sig;
1711    None
1712}
1713
1714/// Remove trap node (from jobs.c removetrapnode) - defers to signals module
1715pub fn removetrapnode(sig: i32) {
1716    let _ = sig;
1717}
1718
1719/// Release acquired process group (from jobs.c release_pgrp)
1720#[cfg(unix)]
1721pub fn release_pgrp() {
1722    // Restore original process group if needed
1723}
1724
1725/// Signal number from name (from jobs.c getsigidx)
1726pub fn getsigidx(name: &str) -> Option<i32> {
1727    let name = name.strip_prefix("SIG").unwrap_or(name);
1728    match name.to_uppercase().as_str() {
1729        "EXIT" => Some(0),
1730        "HUP" => Some(libc::SIGHUP),
1731        "INT" => Some(libc::SIGINT),
1732        "QUIT" => Some(libc::SIGQUIT),
1733        "ILL" => Some(libc::SIGILL),
1734        "TRAP" => Some(libc::SIGTRAP),
1735        "ABRT" | "IOT" => Some(libc::SIGABRT),
1736        "BUS" => Some(libc::SIGBUS),
1737        "FPE" => Some(libc::SIGFPE),
1738        "KILL" => Some(libc::SIGKILL),
1739        "USR1" => Some(libc::SIGUSR1),
1740        "SEGV" => Some(libc::SIGSEGV),
1741        "USR2" => Some(libc::SIGUSR2),
1742        "PIPE" => Some(libc::SIGPIPE),
1743        "ALRM" => Some(libc::SIGALRM),
1744        "TERM" => Some(libc::SIGTERM),
1745        "CHLD" | "CLD" => Some(libc::SIGCHLD),
1746        "CONT" => Some(libc::SIGCONT),
1747        "STOP" => Some(libc::SIGSTOP),
1748        "TSTP" => Some(libc::SIGTSTP),
1749        "TTIN" => Some(libc::SIGTTIN),
1750        "TTOU" => Some(libc::SIGTTOU),
1751        "URG" => Some(libc::SIGURG),
1752        "XCPU" => Some(libc::SIGXCPU),
1753        "XFSZ" => Some(libc::SIGXFSZ),
1754        "VTALRM" => Some(libc::SIGVTALRM),
1755        "PROF" => Some(libc::SIGPROF),
1756        "WINCH" => Some(libc::SIGWINCH),
1757        "IO" | "POLL" => Some(libc::SIGIO),
1758        "SYS" => Some(libc::SIGSYS),
1759        _ => name.parse().ok(),
1760    }
1761}