htoprs 0.4.1

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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
//! Partial port of `DynamicColumn.c` — htop's dynamic-column registry.
//!
//! C names are preserved verbatim (htop uses `CamelCase_snake`), so
//! `non_snake_case` is allowed for the whole module — matching the spec
//! name-for-name is the point of the port. Each C function taking
//! `DynamicColumn* this` ports to a free fn (the shape `Vector.c`/
//! `History.c` use), not a method.
//!
//! Ported (self-contained, no unported substrate):
//! - `DynamicColumn_compare` (`DynamicColumn.c:52`) — `static` in C; the
//!   `Hashtable_foreach` comparison callback. Its logic is a plain
//!   `String_eq` name match (`strcmp == 0`) plus an iterator update,
//!   needing no unported substrate.
//! - `DynamicColumn_search` (`DynamicColumn.c:61`) — drives
//!   `Hashtable_foreach` over the registry, accumulating the matched key,
//!   then re-reads the value via `Hashtable_get`. Both are ported now
//!   (`hashtable.rs`).
//! - `DynamicColumn_lookup` (`DynamicColumn.c:70`) — thin wrapper over
//!   `Hashtable_get`, ported now.
//! - `DynamicColumns_new` (`DynamicColumn.c:22`) — asks
//!   `Platform_dynamicColumns()` (ported now, Linux stub) then falls back to
//!   `Hashtable_new(0, true)`.
//! - `DynamicColumn_name` (`DynamicColumn.c:36`) — thin wrapper over
//!   `Platform_dynamicColumnName` (ported now, Linux stub).
//! - `DynamicColumn_writeField` (`DynamicColumn.c:74`) — thin wrapper over
//!   `Platform_dynamicColumnWriteField` (ported now, Linux stub).
//! - `DynamicColumns_delete` (`DynamicColumn.c:29`) — a `*_delete` teardown:
//!   null-guards then calls `Platform_dynamicColumnsDone` (Linux no-op) and
//!   `Hashtable_delete` (both ported now); the moved-in `Hashtable` drops,
//!   which is C's `free(dynamics)`.
//! - `DynamicColumn_done` (`DynamicColumn.c:40`) — a `*_done` teardown that
//!   `free()`s `heading`, `caption`, `description`; setting each owned
//!   `Option<String>` to `None` drops it (C's `free`) without freeing the
//!   struct, same precedent as `Table_done`.
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)] // preserve the C-style class name `DynamicColumn_class`
#![allow(dead_code)]

use crate::ported::hashtable::{
    Hashtable, Hashtable_delete, Hashtable_foreach, Hashtable_get, Hashtable_new,
};
use crate::ported::linux::platform::{
    Platform_dynamicColumnName, Platform_dynamicColumnWriteField, Platform_dynamicColumns,
    Platform_dynamicColumnsDone,
};
use crate::ported::object::{Object, ObjectClass, Object_class};
use crate::ported::process::Process;
use crate::ported::richstring::RichString;
use crate::ported::table::Table;

/// C `#define DYNAMIC_MAX_COLUMN_WIDTH 64` (`DynamicColumn.h:19`).
pub const DYNAMIC_MAX_COLUMN_WIDTH: i32 = 64;
/// C `#define DYNAMIC_DEFAULT_COLUMN_WIDTH -5` (`DynamicColumn.h:20`).
pub const DYNAMIC_DEFAULT_COLUMN_WIDTH: i32 = -5;

/// Model of the C `DynamicColumn` struct (`DynamicColumn.h:22`). This is
/// a substrate type; the full C field set is modelled so downstream
/// consumers (Row title rendering, the setup menu, the `Platform_*`
/// column providers) can rely on it. [`DynamicColumn_compare`] itself
/// only reads `name`.
pub struct DynamicColumn {
    /// C `char name[32]` — unique, internal-only name.
    pub name: String,
    /// C `char* heading` — displayed in the main screen.
    pub heading: Option<String>,
    /// C `char* caption` — displayed in the setup menu (short name).
    pub caption: Option<String>,
    /// C `char* description` — displayed in the setup menu (detail).
    pub description: Option<String>,
    /// C `int width` — display width, +/- for value alignment.
    pub width: i32,
    /// C `bool enabled` — false means ignore this column until enabled.
    pub enabled: bool,
    /// C `Table* table` — back-pointer to the owning DynamicScreen or
    /// ProcessTable. A real back-pointer, exactly like `Table::host`
    /// (`*const Machine`); null when not attached to a table.
    pub table: *const Table,
}

/// Class descriptor for [`DynamicColumn`], present solely so a
/// `DynamicColumn` can be stored as a `Box<dyn Object>` value in the
/// ported [`Hashtable`] (whose value type is `dyn Object`). htop's
/// `Hashtable` stores raw `void*`, so C's `DynamicColumn` is **not** an
/// `Object` subclass — there is no `DynamicColumn_class` in htop; this
/// exists only as the safe-Rust adapter for the ported table's owned
/// `dyn Object` value model. Rooted at [`Object_class`]; it sets no
/// `display`/`compare` slots (the table never dispatches through them).
static DynamicColumn_class: ObjectClass = ObjectClass {
    extends: Some(&Object_class),
};

impl Object for DynamicColumn {
    fn klass(&self) -> &'static ObjectClass {
        &DynamicColumn_class
    }
}

/// Model of the file-private C `DynamicIterator` struct
/// (`DynamicColumn.c:47`). `data` is a borrow of the matched column
/// (C's `const DynamicColumn*`), tied to the lifetime of the scanned
/// values.
pub struct DynamicIterator<'a> {
    /// Needle name being searched for.
    pub name: &'a str,
    /// Matched column, or `None` (C `NULL`) if unmatched.
    pub data: Option<&'a DynamicColumn>,
    /// Key of the matched column (C `unsigned int`, `0` when unmatched).
    pub key: u32,
}

/// Port of `DynamicColumn.c:52`. `Hashtable_foreach` callback: when the
/// visited column's name equals the iterator's search name (C
/// `String_eq`, i.e. exact `strcmp == 0`), record the column and its
/// key into the iterator. `ht_key_t` is C `unsigned int`.
pub fn DynamicColumn_compare<'a>(
    key: u32,
    value: &'a DynamicColumn,
    iter: &mut DynamicIterator<'a>,
) {
    if iter.name == value.name {
        iter.data = Some(value);
        iter.key = key;
    }
}

/// Port of `DynamicColumn.c:22`. Asks the platform for its dynamic-column
/// registry ([`Platform_dynamicColumns`]); when the platform has none (C
/// `NULL` — the Linux build always), falls back to an owning
/// `Hashtable_new(0, true)`. C `true` == the table owns (and frees) its
/// values.
pub fn DynamicColumns_new() -> Hashtable {
    // C: Hashtable* dynamics = Platform_dynamicColumns();
    //    if (!dynamics) dynamics = Hashtable_new(0, true);
    Platform_dynamicColumns().unwrap_or_else(|| Hashtable_new(0, true))
}

/// Port of `void DynamicColumns_delete(Hashtable* dynamics)` from
/// `DynamicColumn.c:29`. C null-guards the pointer (`if (dynamics)`) then
/// runs the platform teardown ([`Platform_dynamicColumnsDone`], a Linux
/// no-op) before [`Hashtable_delete`]. The C `Hashtable*` maps to
/// `Option<Hashtable>`: `None` is C `NULL` (no-op), and passing the owned
/// `Hashtable` by value into `Hashtable_delete` is the faithful analog of
/// C's `free(dynamics)`.
pub fn DynamicColumns_delete(dynamics: Option<Hashtable>) {
    // C: if (dynamics) {
    if let Some(dynamics) = dynamics {
        // C: Platform_dynamicColumnsDone(dynamics);
        Platform_dynamicColumnsDone(&dynamics);
        // C: Hashtable_delete(dynamics);
        Hashtable_delete(dynamics);
    }
}

/// Port of `DynamicColumn.c:36`. Thin wrapper over
/// [`Platform_dynamicColumnName`]; returns the platform-supplied name for
/// `key`, or `None` (C `NULL`) when the platform provides none.
pub fn DynamicColumn_name(key: u32) -> Option<&'static str> {
    Platform_dynamicColumnName(key)
}

/// Port of `void DynamicColumn_done(DynamicColumn* this)` from
/// `DynamicColumn.c:40`. C `free()`s the three owned heap strings
/// (`heading`, `caption`, `description`) without freeing the struct
/// itself. The safe-Rust analog sets each `Option<String>` to `None`:
/// dropping the owned `String` is C's `free`, and leaving `this` intact
/// mirrors that `_done` releases fields but not the object storage (same
/// precedent as [`Table_done`](crate::ported::table::Table_done)).
pub fn DynamicColumn_done(this: &mut DynamicColumn) {
    // C: free(this->heading);
    this.heading = None;
    // C: free(this->caption);
    this.caption = None;
    // C: free(this->description);
    this.description = None;
}

/// Port of `DynamicColumn.c:61`. Scans the registry for a column whose
/// name equals `name`, returning it (C `iter.data`) and writing its key
/// through `key` when the out-param is non-null (C `if (key) *key =
/// iter.key`). A null table is skipped (C `if (dynamics)`), leaving
/// `key` `0` and returning `None`.
///
/// The ported [`Hashtable_foreach`] hands each value to the callback as a
/// `&dyn Object` valid only for that call, so the matched reference
/// cannot escape the scan the way C's `void* value` pointer does. The
/// faithful two-step: drive [`DynamicColumn_compare`] into a scratch
/// [`DynamicIterator`] per bucket (its logic is what C runs), record the
/// `Copy` key of the last match (C's callback overwrites on every match,
/// so the last visited wins), then re-read the value with the same probe
/// order via [`Hashtable_get`]. `Hashtable_get` and `Hashtable_foreach`
/// resolve to the same stored value, so this yields exactly C's result.
pub fn DynamicColumn_search<'a>(
    dynamics: Option<&'a Hashtable>,
    name: &str,
    key: Option<&mut u32>,
) -> Option<&'a DynamicColumn> {
    // C: DynamicIterator iter = { .key = 0, .data = NULL, .name = name };
    let mut matched_key: u32 = 0;
    let mut matched = false;

    if let Some(dynamics) = dynamics {
        Hashtable_foreach(dynamics, &mut |k, value| {
            // C: const DynamicColumn* column = (const DynamicColumn*)value;
            let any: &dyn core::any::Any = value;
            let column = any
                .downcast_ref::<DynamicColumn>()
                .expect("DynamicColumn_search: hashtable value is not a DynamicColumn");

            let mut iter = DynamicIterator {
                name,
                data: None,
                key: 0,
            };
            DynamicColumn_compare(k, column, &mut iter);
            if iter.data.is_some() {
                matched = true;
                matched_key = iter.key;
            }
        });
    }

    // C: if (key) *key = iter.key;
    if let Some(key) = key {
        *key = matched_key;
    }

    // C: return iter.data;
    if matched {
        dynamics
            .and_then(|d| Hashtable_get(d, matched_key))
            .and_then(|o| {
                let any: &dyn core::any::Any = o;
                any.downcast_ref::<DynamicColumn>()
            })
    } else {
        None
    }
}

/// Port of `DynamicColumn.c:70`. Thin wrapper over [`Hashtable_get`]:
/// C casts the returned `void*` straight to `const DynamicColumn*`; the
/// safe-Rust analog downcasts the `&dyn Object` value back to its
/// concrete type via `Any`. A miss returns `None` (C `NULL`).
pub fn DynamicColumn_lookup(dynamics: &Hashtable, key: u32) -> Option<&DynamicColumn> {
    Hashtable_get(dynamics, key).and_then(|o| {
        let any: &dyn core::any::Any = o;
        any.downcast_ref::<DynamicColumn>()
    })
}

/// Port of `DynamicColumn.c:74`. Thin wrapper over
/// [`Platform_dynamicColumnWriteField`]: renders the dynamic column `key`
/// for `proc` into `str`, returning whether the platform handled it (C
/// `false` when the platform has no such column).
pub fn DynamicColumn_writeField(proc: &Process, str: &mut RichString, key: u32) -> bool {
    Platform_dynamicColumnWriteField(proc, str, key)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ported::hashtable::{Hashtable_count, Hashtable_put};

    fn col(name: &str) -> DynamicColumn {
        DynamicColumn {
            name: name.to_string(),
            heading: None,
            caption: None,
            description: None,
            width: DYNAMIC_DEFAULT_COLUMN_WIDTH,
            enabled: false,
            table: core::ptr::null(),
        }
    }

    // Build a registry the way DynamicColumns_new would: a non-owner
    // table (values live in the caller's world) keyed by column index.
    fn registry(names: &[(u32, &str)]) -> crate::ported::hashtable::Hashtable {
        let mut ht = Hashtable_new(0, false);
        for &(k, n) in names {
            Hashtable_put(&mut ht, k, Box::new(col(n)));
        }
        ht
    }

    #[test]
    fn lookup_returns_column_for_present_key() {
        let ht = registry(&[(1, "cpu"), (2, "mem"), (100, "io")]);
        assert_eq!(DynamicColumn_lookup(&ht, 1).unwrap().name, "cpu");
        assert_eq!(DynamicColumn_lookup(&ht, 2).unwrap().name, "mem");
        assert_eq!(DynamicColumn_lookup(&ht, 100).unwrap().name, "io");
    }

    #[test]
    fn lookup_returns_none_for_absent_key() {
        let ht = registry(&[(1, "cpu")]);
        assert!(DynamicColumn_lookup(&ht, 999).is_none());
    }

    #[test]
    fn search_finds_by_name_and_writes_key() {
        let ht = registry(&[(10, "cpu"), (20, "mem"), (30, "io")]);
        let mut key: u32 = 0;
        let found = DynamicColumn_search(Some(&ht), "mem", Some(&mut key));
        assert_eq!(found.unwrap().name, "mem");
        assert_eq!(key, 20);
    }

    #[test]
    fn search_key_out_param_is_optional() {
        // C: `if (key)` — passing None (C NULL) must not fault.
        let ht = registry(&[(5, "cpu")]);
        let found = DynamicColumn_search(Some(&ht), "cpu", None);
        assert_eq!(found.unwrap().name, "cpu");
    }

    #[test]
    fn search_miss_returns_none_and_zeroes_key() {
        let ht = registry(&[(1, "cpu"), (2, "mem")]);
        let mut key: u32 = 12345;
        let found = DynamicColumn_search(Some(&ht), "nonexistent", Some(&mut key));
        assert!(found.is_none());
        // C leaves iter.key at its 0 init when nothing matches.
        assert_eq!(key, 0);
    }

    #[test]
    fn search_null_table_returns_none() {
        // C: `if (dynamics)` guards the foreach; a null table yields
        // iter.data == NULL and iter.key == 0.
        let mut key: u32 = 7;
        let found = DynamicColumn_search(None, "cpu", Some(&mut key));
        assert!(found.is_none());
        assert_eq!(key, 0);
    }

    #[test]
    fn search_is_case_sensitive_like_string_eq() {
        // String_eq is strcmp==0: "CPU" != "cpu".
        let ht = registry(&[(1, "CPU")]);
        let mut key: u32 = 0;
        assert!(DynamicColumn_search(Some(&ht), "cpu", Some(&mut key)).is_none());
        assert_eq!(key, 0);
        assert_eq!(
            DynamicColumn_search(Some(&ht), "CPU", None).unwrap().name,
            "CPU"
        );
    }

    #[test]
    fn search_result_matches_lookup_of_returned_key() {
        // The key written by search must round-trip through lookup to the
        // same column (the two-phase foreach/get must agree).
        let ht = registry(&[(3, "alpha"), (17, "beta"), (42, "gamma")]);
        for name in ["alpha", "beta", "gamma"] {
            let mut key: u32 = 0;
            let s = DynamicColumn_search(Some(&ht), name, Some(&mut key)).unwrap();
            let l = DynamicColumn_lookup(&ht, key).unwrap();
            assert_eq!(s.name, name);
            assert_eq!(l.name, name);
            assert!(std::ptr::eq(s, l));
        }
    }

    #[test]
    fn compare_records_match_and_key() {
        let cpu = col("cpu");
        let mut iter = DynamicIterator {
            name: "cpu",
            data: None,
            key: 0,
        };
        DynamicColumn_compare(7, &cpu, &mut iter);
        assert_eq!(iter.key, 7);
        assert!(matches!(iter.data, Some(c) if c.name == "cpu"));
    }

    #[test]
    fn compare_ignores_non_match() {
        let mem = col("mem");
        let mut iter = DynamicIterator {
            name: "cpu",
            data: None,
            key: 0,
        };
        DynamicColumn_compare(3, &mem, &mut iter);
        // no match: iterator left untouched (C leaves .data=NULL, .key=0)
        assert_eq!(iter.key, 0);
        assert!(iter.data.is_none());
    }

    #[test]
    fn compare_is_exact_case_sensitive_strcmp() {
        // String_eq is strcmp==0: case-sensitive, no trimming
        let upper = col("CPU");
        let mut iter = DynamicIterator {
            name: "cpu",
            data: None,
            key: 0,
        };
        DynamicColumn_compare(9, &upper, &mut iter);
        assert_eq!(iter.key, 0);
        assert!(iter.data.is_none());
    }

    #[test]
    fn new_returns_empty_owning_registry() {
        // Platform_dynamicColumns() is NULL on Linux, so C falls back to
        // Hashtable_new(0, true): a fresh, empty registry.
        let ht = DynamicColumns_new();
        assert_eq!(Hashtable_count(&ht), 0);
    }

    #[test]
    fn name_is_none_on_this_platform() {
        // Platform_dynamicColumnName() returns NULL for every key on Linux.
        assert!(DynamicColumn_name(0).is_none());
        assert!(DynamicColumn_name(12345).is_none());
    }

    #[test]
    fn compare_last_match_wins() {
        // C callback overwrites on every match; a later duplicate wins
        let a = col("dup");
        let b = col("dup");
        let mut iter = DynamicIterator {
            name: "dup",
            data: None,
            key: 0,
        };
        DynamicColumn_compare(1, &a, &mut iter);
        DynamicColumn_compare(2, &b, &mut iter);
        assert_eq!(iter.key, 2);
        assert!(std::ptr::eq(iter.data.unwrap(), &b));
    }
}