htoprs 0.5.3

A faithful Rust port of htop — the interactive process viewer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! Port of `EnvScreen.c` — htop's "show a process's environment"
//! `InfoScreen` subclass (the `e` key on the main panel).
//!
//! `EnvScreen` is a thin `InfoScreen` subclass: its struct is literally
//! `{ InfoScreen super; }` (`EnvScreen.h:17`), and all four functions are
//! defined purely in terms of the `InfoScreen` base class and its vtable
//! (`InfoScreenClass`). C names are preserved verbatim (htop uses
//! `CamelCase_snake`), so `non_snake_case` is allowed for the whole
//! module. A C fn `EnvScreen_foo(InfoScreen* this)` ports to a free fn
//! `EnvScreen_foo(this: &mut InfoScreen)` (the shape the module header of
//! `infoscreen.rs` prescribes). The embedded `InfoScreen super` becomes
//! `super_` (the Rust-keyword workaround the ported subclasses use, e.g.
//! `commandscreen.rs`/`tracescreen.rs`).
//!
//! # Ported (no unported substrate)
//!
//! - The [`EnvScreen`] struct (`EnvScreen.h:17`): `{ InfoScreen super; }`.
//! - [`EnvScreen_new`] (`EnvScreen.c:25`) — `xMalloc` the wrapper then
//!   chain-return `InfoScreen_init(&this->super, process, NULL, LINES - 2,
//!   " ")`. `InfoScreen_init` is ported (`infoscreen.rs`); `LINES` maps to
//!   `Ncurses::lines()` (the same source `InfoScreen_init` reads for
//!   `COLS`). The C `Object_setClass(this, Class(EnvScreen))` vtable install
//!   is omitted — the ported `InfoScreen` drops the `Object super` vtable
//!   slot (only the stubbed dispatch would read it); see `infoscreen.rs`.
//! - [`EnvScreen_scan`] (`EnvScreen.c:39`) — the `scan` vtable hook. Saves
//!   the panel selection, prunes it, reads the process's environment via
//!   [`Platform_getProcessEnv`] (`Platform.c:519`, ported in
//!   `linux::platform`), adds one [`InfoScreen_addLine`] per NUL-separated
//!   entry (or the "Could not read" message on `None`), then re-sorts
//!   `lines` and the panel items and restores the selection. Every
//!   dependency (`Panel_getSelectedIndex`/`Panel_prune`/`Panel_setSelected`,
//!   `Process_getPid`, `Vector_insertionSort`, `InfoScreen_addLine`) is
//!   available.
//!
//! # Stubbed (cannot be ported faithfully yet), each naming its blocker
//!
//! - [`EnvScreen_delete`] (`EnvScreen.c:31`) — `free(InfoScreen_done(this))`,
//!   heap-free only. `InfoScreen_done` is itself a `todo!()` (an owned
//!   `InfoScreen` releases its fields via `Drop`), and the owned
//!   `EnvScreen` frees itself the same way, so there is no algorithm to
//!   port (the `InfoScreen_done` / `History_delete` / `Panel_delete`
//!   precedent).
//! - [`EnvScreen_draw`] (`EnvScreen.c:35`) — the `draw` vtable hook: a
//!   single call to `InfoScreen_drawTitled(this, "Environment of process
//!   %d - %s", Process_getPid(this->process), Process_getCommand(
//!   this->process))`. `InfoScreen_drawTitled` (`infoscreen.rs`) and
//!   `Process_getPid` are now ported, but the title's `%s` argument
//!   `Process_getCommand` (`process.rs`, `todo!()` — needs
//!   `settings->showThreadNames`, a field the ported `Settings` subset lacks,
//!   reached through the opaque `Row::host` pointer) is still a stub, so the
//!   hook still cannot be drawn faithfully.
#![allow(non_snake_case)]
#![allow(dead_code)]

use core::ffi::c_int;

use crate::ported::functionbar::Ncurses;
use crate::ported::incset::IncSet_new;
use crate::ported::infoscreen::{
    InfoScreen, InfoScreenClass, InfoScreen_addLine, InfoScreen_done, InfoScreen_drawTitled,
    InfoScreen_init,
};
// Environment retrieval is per-OS (htop links the target's `Platform.c`). The
// TUI runs on darwin, which reads the env via `sysctl(KERN_PROCARGS2)`; the
// import was hardcoded to the linux `/proc/<pid>/environ` reader, so `e` always
// failed on macOS ("Could not read process environment"). Select per target.
#[cfg(target_os = "macos")]
use crate::ported::darwin::platform::Platform_getProcessEnv;
#[cfg(target_os = "dragonfly")]
use crate::ported::dragonflybsd::platform::Platform_getProcessEnv;
#[cfg(target_os = "freebsd")]
use crate::ported::freebsd::platform::Platform_getProcessEnv;
#[cfg(target_os = "linux")]
use crate::ported::linux::platform::Platform_getProcessEnv;
use crate::ported::listitem::ListItem_new;
#[cfg(target_os = "netbsd")]
use crate::ported::netbsd::platform::Platform_getProcessEnv;
use crate::ported::object::{Object, ObjectClass};
#[cfg(target_os = "openbsd")]
use crate::ported::openbsd::platform::Platform_getProcessEnv;
use crate::ported::panel::{Panel_getSelectedIndex, Panel_new, Panel_prune, Panel_setSelected};
use crate::ported::process::{Process, Process_getCommand, Process_getPid};
#[cfg(target_os = "solaris")]
use crate::ported::solaris::platform::Platform_getProcessEnv;
#[cfg(not(any(
    target_os = "linux",
    target_os = "macos",
    target_os = "freebsd",
    target_os = "dragonfly",
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "solaris"
)))]
use crate::ported::unsupported::platform::Platform_getProcessEnv;
use crate::ported::vector::{Vector_insertionSort, Vector_new};

/// Port of `#define VECTOR_DEFAULT_SIZE (10)` from `Vector.h:15` — the
/// initial `lines` vector capacity for the throwaway `InfoScreen` that
/// [`EnvScreen_new`] seeds before `InfoScreen_init` overwrites it (mirrors
/// the same local const in `infoscreen.rs`/`commandscreen.rs`).
const VECTOR_DEFAULT_SIZE: c_int = 10;

/// Port of `struct EnvScreen_` (`EnvScreen.h:17`): `{ InfoScreen super; }`.
/// The embedded base is exposed as `super_` (the Rust-keyword workaround the
/// ported subclasses use).
pub struct EnvScreen {
    /// C `InfoScreen super` — the scrollable info-panel base class.
    pub super_: InfoScreen,
}

/// Port of `const InfoScreenClass EnvScreen_class` (`EnvScreen.c:60`):
/// `{ .scan = EnvScreen_scan, .draw = EnvScreen_draw }`. Wires the two
/// installed vtable slots so [`InfoScreen_run`](crate::ported::infoscreen::InfoScreen_run)
/// dispatches this screen; `onErr`/`onKey` are `NULL` in C (trait defaults).
impl InfoScreenClass for EnvScreen {
    fn super_InfoScreen(&mut self) -> &mut InfoScreen {
        &mut self.super_
    }
    fn draw(&mut self) {
        EnvScreen_draw(&mut self.super_);
    }
    fn scan(&mut self) {
        EnvScreen_scan(&mut self.super_);
    }
    fn has_scan(&self) -> bool {
        true
    }
}

/// Port of `EnvScreen* EnvScreen_new(Process* process)` from
/// `EnvScreen.c:25`. `xMalloc(sizeof(EnvScreen))`, install the
/// `Class(EnvScreen)` vtable, then chain-return `InfoScreen_init(&this->super,
/// process, NULL, LINES - 2, " ")`.
///
/// C's `xMalloc` hands `InfoScreen_init` uninitialized storage which it then
/// overwrites field-for-field; the faithful analog seeds a throwaway
/// `InfoScreen` (same bootstrap as the private `InfoScreen::empty`: null
/// `process`, empty `Panel`/`IncSet`, a `ListItem`-typed `lines` vector) and
/// lets `InfoScreen_init` overwrite every field. `LINES` maps to
/// `Ncurses::lines()` (the same terminal-metric source `InfoScreen_init`
/// reads for `COLS`). `NULL` is passed for the function bar so
/// `InfoScreen_init` builds the default `InfoScreen` bar. The
/// `Object_setClass(this, Class(EnvScreen))` vtable install is omitted (the
/// vtable is not modelled; see the module docs). C returns
/// `(EnvScreen*) InfoScreen_init(&this->super, ...)`; since `super` is at
/// offset 0 the cast is identity, so the port returns `this`.
pub fn EnvScreen_new(process: &Process) -> EnvScreen {
    // C: EnvScreen* this = xMalloc(sizeof(EnvScreen));
    // The xMalloc storage is uninitialized; seed a valid throwaway
    // InfoScreen (InfoScreen_init overwrites process/display/inc/lines).
    let list_item_class: &'static ObjectClass = ListItem_new("", 0).klass();
    let mut this = EnvScreen {
        super_: InfoScreen {
            process: core::ptr::null(),
            display: Panel_new(0, 0, 0, 0, None),
            inc: IncSet_new(None),
            lines: Vector_new(list_item_class, true, VECTOR_DEFAULT_SIZE),
        },
    };

    // C: return (EnvScreen*) InfoScreen_init(&this->super, process, NULL, LINES - 2, " ");
    InfoScreen_init(
        &mut this.super_,
        process as *const Process,
        None,
        Ncurses::lines() - 2,
        " ",
    );

    this
}

/// Port of `void EnvScreen_delete(Object* this)` from `EnvScreen.c:31`:
/// `free(InfoScreen_done((InfoScreen*)this))`. Taking `this` by value
/// consumes the screen; the embedded `super_` [`InfoScreen`] is handed to
/// [`InfoScreen_done`] (mirroring the C call graph), whose by-value consume
/// folds in the outer `free`.
pub fn EnvScreen_delete(this: EnvScreen) {
    let EnvScreen { super_ } = this;
    InfoScreen_done(super_);
}

/// TODO: port of `static void EnvScreen_draw(InfoScreen* this)` from
/// `EnvScreen.c:35`. Single call to `InfoScreen_drawTitled(this,
/// "Environment of process %d - %s", Process_getPid(this->process),
/// Process_getCommand(this->process))` with the C `printf` format
/// pre-built into a `&str` (the ported `InfoScreen_drawTitled` convention).
/// `%s` is [`Process_getCommand`], rendered lossily from its bytes
/// (`None` → empty).
pub fn EnvScreen_draw(this: &mut InfoScreen) {
    // C: InfoScreen_drawTitled(this, "Environment of process %d - %s",
    //        Process_getPid(this->process), Process_getCommand(this->process));
    let pid = Process_getPid(unsafe { &*this.process });
    let cmd = match Process_getCommand(unsafe { &*this.process }) {
        Some(b) => String::from_utf8_lossy(b).into_owned(),
        None => String::new(),
    };
    let title = format!("Environment of process {} - {}", pid, cmd);
    InfoScreen_drawTitled(this, &title);
}

/// Port of `static void EnvScreen_scan(InfoScreen* this)` from
/// `EnvScreen.c:39`. The vtable `scan` hook. C accesses only base
/// `InfoScreen` fields (`this->display`/`this->process`/`this->lines`, no
/// downcast to `EnvScreen`), so the port takes `this: &mut InfoScreen`
/// directly — the shape the `infoscreen.rs` module header prescribes for a
/// `Foo_bar(InfoScreen* this)` C fn.
///
/// Saves the selection (`MAXIMUM(Panel_getSelectedIndex(panel), 0)` ->
/// `.max(0)`), prunes the panel, reads the process's NUL-separated
/// environment block via [`Platform_getProcessEnv`], and — on success —
/// walks each NUL-terminated entry (C's `for (p = env; *p; p = strrchr(p, 0)
/// + 1)`; `str::split('\0')` yields the same entries, and the C loop's `*p`
/// stop condition — halting at the first empty entry / the double-NUL
/// terminator — becomes the `break` on an empty split fragment). On `None`
/// (C `NULL`) it adds the single "Could not read" line. Finally re-sorts the
/// `lines` `Vector` and the panel's items and restores the selection.
///
/// Divergences: C `free(env)` is the owned `String` drop at end of scope.
/// `Vector_insertionSort(panel->items)` has no direct call because the ported
/// `Panel.items` is a plain `Vec<Box<dyn Object>>` (not a `Vector`); it is
/// sorted in place with the same `Object::compare` comparator
/// `Vector_insertionSort` uses (the `openfilesscreen.rs` precedent). The C
/// `Process_getPid(this->process)` derefs the raw `process` back-pointer —
/// an `unsafe { &*this.process }` (the `tracescreen.rs` precedent).
pub fn EnvScreen_scan(this: &mut InfoScreen) {
    // C: Panel* panel = this->display;
    //    int idx = MAXIMUM(Panel_getSelectedIndex(panel), 0);
    let idx = Panel_getSelectedIndex(&this.display).max(0);

    // C: Panel_prune(panel);
    Panel_prune(&mut this.display);

    // C: char* env = Platform_getProcessEnv(Process_getPid(this->process));
    let pid = unsafe { Process_getPid(&*this.process) };
    match Platform_getProcessEnv(pid as libc::pid_t) {
        Some(env) => {
            // C: for (const char* p = env; *p; p = strrchr(p, 0) + 1)
            //        InfoScreen_addLine(this, p);
            // env is a NUL-separated block (double-NUL terminated). Each
            // split fragment is one entry; the first empty fragment is the
            // C loop's `*p == 0` stop (the terminator).
            for entry in env.split('\0') {
                if entry.is_empty() {
                    break;
                }
                InfoScreen_addLine(this, entry);
            }
            // C: free(env); — owned String dropped at end of scope.
        }
        None => {
            // C: InfoScreen_addLine(this, "Could not read process environment.");
            InfoScreen_addLine(this, "Could not read process environment.");
        }
    }

    // C: Vector_insertionSort(this->lines);
    Vector_insertionSort(&mut this.lines);
    // C: Vector_insertionSort(panel->items);  (see the divergence note above)
    this.display
        .items
        .sort_by(|a, b| a.object().compare(b.object()).cmp(&0));
    // C: Panel_setSelected(panel, idx);
    Panel_setSelected(&mut this.display, idx);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ported::incset::IncSet_filter;
    use crate::ported::listitem::ListItem;
    use crate::ported::panel::{Panel_get, Panel_headerHeight, Panel_size};
    use crate::ported::process::Process_setPid;
    use crate::ported::vector::Vector_size;

    /// The InfoScreen function-bar labels `InfoScreen_init` installs when
    /// `EnvScreen_new` passes `NULL` for the bar (`InfoScreen.c:25`).
    const INFO_FUNCTIONS: [&str; 4] = ["Search ", "Filter ", "Refresh", "Done   "];

    /// Read the `value` of the `ListItem` shown at panel index `i`.
    fn panel_value(p: &crate::ported::panel::Panel, i: i32) -> String {
        let any: &dyn std::any::Any = Panel_get(p, i);
        any.downcast_ref::<ListItem>().unwrap().value.clone()
    }

    #[test]
    fn new_initializes_infoscreen_base() {
        let p = Process::default();
        let es = EnvScreen_new(&p);
        // process back-pointer stored (points at the passed Process).
        assert_eq!(es.super_.process, &p as *const Process);
        // Fresh screen: no lines scanned yet, panel empty.
        assert_eq!(Vector_size(&es.super_.lines), 0);
        assert_eq!(Panel_size(&es.super_.display), 0);
        // No filter active on a fresh IncSet.
        assert!(IncSet_filter(&es.super_.inc).is_none());
    }

    #[test]
    fn new_geometry_matches_c_panel_new_args() {
        // C: Panel_new(0, 1, COLS, LINES - 2, ...) inside InfoScreen_init,
        // height == LINES - 2 passed by EnvScreen_new.
        let p = Process::default();
        let es = EnvScreen_new(&p);
        assert_eq!(es.super_.display.x, 0);
        assert_eq!(es.super_.display.y, 1);
        assert_eq!(es.super_.display.w, Ncurses::cols());
        assert_eq!(es.super_.display.h, Ncurses::lines() - 2);
        // Header " " installed -> headerHeight 1.
        assert_eq!(Panel_headerHeight(&es.super_.display), 1);
    }

    #[test]
    fn new_builds_default_infoscreen_bar() {
        // NULL bar -> InfoScreen_init synthesizes the InfoScreen default bar.
        let p = Process::default();
        let es = EnvScreen_new(&p);
        let bar = es
            .super_
            .display
            .defaultBar
            .as_ref()
            .expect("default bar built");
        assert_eq!(bar.functions, INFO_FUNCTIONS.to_vec());
        // The IncSet received the same bar content (cloned + moved).
        let inc_bar = es.super_.inc.defaultBar.as_ref().expect("inc default bar");
        assert_eq!(inc_bar.functions, INFO_FUNCTIONS.to_vec());
    }

    #[test]
    fn scan_missing_pid_adds_error_line() {
        // An impossible pid -> Platform_getProcessEnv opens no environ file
        // and returns None on any host, so the C `else` branch runs and adds
        // exactly the single "Could not read" line (deterministic anywhere).
        let mut p = Process::default();
        Process_setPid(&mut p, 2147483646);
        let mut es = EnvScreen_new(&p);

        EnvScreen_scan(&mut es.super_);

        assert_eq!(Vector_size(&es.super_.lines), 1);
        assert_eq!(Panel_size(&es.super_.display), 1);
        assert_eq!(
            panel_value(&es.super_.display, 0),
            "Could not read process environment."
        );
    }

    /// On Linux the current process always has a readable `environ`, so the
    /// scan populates one sorted line per environment entry.
    #[cfg(target_os = "linux")]
    #[test]
    fn scan_self_populates_sorted_env_lines() {
        let mut p = Process::default();
        Process_setPid(&mut p, std::process::id() as i32);
        let mut es = EnvScreen_new(&p);

        EnvScreen_scan(&mut es.super_);

        // At least one env entry recorded, and no trailing empty line from
        // the double-NUL terminator.
        let n = Vector_size(&es.super_.lines);
        assert!(n > 0);
        // Panel items are sorted (Vector_insertionSort(panel->items)).
        let mut prev = panel_value(&es.super_.display, 0);
        for i in 1..Panel_size(&es.super_.display) {
            let cur = panel_value(&es.super_.display, i);
            assert!(prev <= cur, "panel items must be sorted");
            prev = cur;
        }
    }

    /// Regression: on macOS the env came from the linux `/proc/<pid>/environ`
    /// reader (nonexistent on darwin), so every `e` press yielded a single
    /// "Could not read process environment." line. This scans the test process
    /// itself and asserts a real `KEY=VALUE` entry is present — which only holds
    /// once the darwin `sysctl(KERN_PROCARGS2)` reader is wired in. `n > 0`
    /// alone would pass even on the bug (the error line counts), so we check for
    /// an actual `=` and the absence of the failure string.
    #[cfg(target_os = "macos")]
    #[test]
    fn scan_self_reads_real_env_on_darwin() {
        let mut p = Process::default();
        Process_setPid(&mut p, std::process::id() as i32);
        let mut es = EnvScreen_new(&p);

        EnvScreen_scan(&mut es.super_);

        let n = Panel_size(&es.super_.display);
        assert!(n > 0, "scan recorded no lines");
        let mut saw_kv = false;
        for i in 0..n {
            let line = panel_value(&es.super_.display, i);
            assert_ne!(
                line, "Could not read process environment.",
                "darwin env read failed"
            );
            if line.contains('=') {
                saw_kv = true;
            }
        }
        assert!(saw_kv, "expected at least one KEY=VALUE env entry");
    }
}