#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
#![allow(non_upper_case_globals)]
#![allow(dead_code)]
use std::collections::HashMap;
use crate::ported::crt::{ColorElements, ColorScheme, TreeStr};
use crate::ported::machine::Machine;
use crate::ported::object::Object;
use crate::ported::panel::{
Panel, PanelItem, Panel_getSelectedIndex, Panel_prune, Panel_setSelected,
Panel_setSelectionColor, Panel_size,
};
use crate::ported::process::ProcessField;
use crate::ported::richstring::{
RichString, RichString_appendAscii, RichString_appendWide, RichString_getCharVal,
RichString_rewind, RichString_size,
};
use crate::ported::row::{
Row, RowField_alignedTitle, Row_compareByParent_Base, Row_getGroupOrParent,
Row_isChildOf,
};
use crate::ported::settings::{
RowField, ScreenSettings_getActiveDirection, ScreenSettings_getActiveSortKey, Settings,
};
use crate::ported::vector::{insertionSort, quickSort};
pub struct Table {
pub rows: Vec<Option<Box<dyn Object>>>,
pub displayList: Vec<usize>,
pub table: HashMap<i32, usize>,
pub host: *const Machine,
pub incFilter: Option<String>,
pub needsSort: bool,
pub following: i32,
pub stableId: i32,
pub stableLastIdx: i32,
pub panel: *mut Panel,
pub rows_isDirty: bool,
pub klass: *const TableClass,
}
pub type Table_ScanPrepare = fn(*mut Table);
pub type Table_ScanIterate = fn(*mut Table);
pub type Table_ScanCleanup = fn(*mut Table);
pub struct TableClass {
pub prepare: Option<Table_ScanPrepare>,
pub iterate: Option<Table_ScanIterate>,
pub cleanup: Option<Table_ScanCleanup>,
}
pub fn Table_scanPrepare(this: *mut Table) {
let klass = unsafe { (*this).klass.as_ref() };
match klass.and_then(|k| k.prepare) {
Some(f) => f(this),
None => Table_prepareEntries(unsafe { &mut *this }),
}
}
pub fn Table_scanIterate(this: *mut Table) {
let klass = unsafe { (*this).klass.as_ref() }
.expect("Table_scanIterate: table has no class (null klass)");
let iterate = klass
.iterate
.expect("Table_scanIterate: iterate is mandatory but unset");
iterate(this);
}
pub fn Table_scanCleanup(this: *mut Table) {
let klass = unsafe { (*this).klass.as_ref() };
match klass.and_then(|k| k.cleanup) {
Some(f) => f(this),
None => Table_cleanupEntries(unsafe { &mut *this }),
}
}
impl Table {
pub fn empty() -> Table {
Table {
rows: Vec::new(),
displayList: Vec::new(),
table: HashMap::new(),
host: core::ptr::null(),
incFilter: None,
needsSort: true,
following: -1,
stableId: -1,
stableLastIdx: 0,
panel: core::ptr::null_mut(),
rows_isDirty: false,
klass: core::ptr::null(),
}
}
fn rebuild_index(&mut self) {
self.table.clear();
for (i, slot) in self.rows.iter().enumerate() {
if let Some(obj) = slot {
self.table.insert(obj.as_row().unwrap().id, i);
}
}
}
#[inline]
fn row(&self, i: usize) -> &Row {
self.rows[i]
.as_ref()
.expect("Table::row: NULL slot")
.as_row()
.expect("Table::row: non-Row object")
}
#[inline]
fn row_mut(&mut self, i: usize) -> &mut Row {
self.rows[i]
.as_mut()
.expect("Table::row_mut: NULL slot")
.as_row_mut()
.expect("Table::row_mut: non-Row object")
}
#[inline]
fn row_opt(&self, i: usize) -> Option<&Row> {
self.rows[i].as_ref().and_then(|o| o.as_row())
}
}
pub fn Table_init(this: &mut Table, host: *const Machine) {
this.rows = Vec::new();
this.displayList = Vec::new();
this.table = HashMap::new();
this.needsSort = true;
this.following = -1;
this.stableId = -1;
this.stableLastIdx = 0;
this.host = host;
this.rows_isDirty = false;
}
pub fn Table_done(this: &mut Table) {
this.table.clear();
this.displayList.clear();
this.rows.clear();
}
pub fn Table_delete(mut this: Table) {
Table_done(&mut this);
}
pub fn Table_setPanel(this: &mut Table, panel: *mut Panel) {
this.panel = panel;
}
pub fn Table_add(this: &mut Table, mut row: Box<dyn Object>) {
let id = row.as_row().expect("Table_add: non-Row object").id;
debug_assert!(
!this.table.contains_key(&id),
"Table_add: id already present"
);
row.as_row_mut().unwrap().seenStampMs = unsafe { (*this.host).monotonicMs };
let idx = this.rows.len();
this.rows.push(Some(row));
this.table.insert(id, idx);
debug_assert!(this.table.contains_key(&id));
}
fn Table_removeIndex(this: &mut Table, idx: usize) {
let row = this.row(idx);
let rowid = row.id;
let rowparent = Row_getGroupOrParent(row);
debug_assert!(this.table.contains_key(&rowid));
this.table.remove(&rowid);
this.rows[idx] = None;
this.rows_isDirty = true;
if this.following != -1 && this.following == rowid {
this.following = -1;
}
if this.stableId != -1 && this.stableId == rowid {
if rowparent != 0 && rowparent != rowid && this.table.contains_key(&rowparent) {
this.stableId = rowparent;
} else {
this.stableId = -1;
}
}
debug_assert!(!this.table.contains_key(&rowid));
}
fn Table_buildTreeBranch(this: &mut Table, rowid: i32, level: u32, indent: i32, show: bool) {
if rowid == 0 {
return;
}
let vsize = this.rows.len() as isize;
let mut l: isize = 0;
let mut r: isize = vsize;
while l < r {
let c = l + (r - l) / 2;
let row = this.row(c as usize);
let parent = if row.isRoot {
0
} else {
Row_getGroupOrParent(row)
};
if parent < rowid {
l = c + 1;
} else {
r = c;
}
}
let mut last_shown = r;
while r < vsize {
let row = this.row(r as usize);
if !Row_isChildOf(row, rowid) {
break;
}
if row.show {
last_shown = r;
}
r += 1;
}
let mut i = l;
while i < r {
if !show {
this.row_mut(i as usize).show = false;
}
let row_id = this.row(i as usize).id;
this.displayList.push(i as usize);
let shift = core::cmp::min(level, (core::mem::size_of::<i32>() as u32) * 8 - 2);
let next_indent = indent | (1i32 << shift);
let child_show = {
let row = this.row(i as usize);
row.show && row.showChildren
};
let branch_indent = if i < last_shown { next_indent } else { indent };
Table_buildTreeBranch(this, row_id, level + 1, branch_indent, child_show);
if i == last_shown {
this.row_mut(i as usize).indent = -next_indent;
} else {
this.row_mut(i as usize).indent = next_indent;
}
this.row_mut(i as usize).tree_depth = level + 1;
i += 1;
}
}
fn compareRowByKnownParentThenNatural(v1: &Row, v2: &Row) -> i32 {
Row_compareByParent_Base(v1, v2)
}
pub fn Table_buildTree(this: &mut Table) {
this.displayList.clear();
let vsize = this.rows.len();
for i in 0..vsize {
let (id, parent) = {
let row = this.row(i);
(row.id, Row_getGroupOrParent(row))
};
#[allow(clippy::if_same_then_else)]
let is_root = if id == parent {
true
} else if parent == 0 {
true
} else {
Table_findRow(this, parent).is_none()
};
this.row_mut(i).isRoot = is_root;
}
let n = this.rows.len() as isize;
quickSort(&mut this.rows, 0, n - 1, &|a, b| {
compareRowByKnownParentThenNatural(
a.as_ref().unwrap().as_row().unwrap(),
b.as_ref().unwrap().as_row().unwrap(),
)
});
this.rebuild_index();
for i in 0..vsize {
if this.row(i).isRoot {
{
let row = this.row_mut(i);
row.indent = 0;
row.tree_depth = 0;
}
let id = this.row(i).id;
let show_children = this.row(i).showChildren;
this.displayList.push(i);
Table_buildTreeBranch(this, id, 0, 0, show_children);
}
}
this.needsSort = false;
debug_assert_eq!(this.displayList.len(), vsize);
}
pub fn Table_updateDisplayList(this: &mut Table) {
let tree_view = unsafe {
let settings = (*this.host)
.settings
.as_ref()
.expect("Table_updateDisplayList: host->settings is NULL");
settings.screens[settings.ssIndex as usize].treeView
};
if tree_view {
if this.needsSort {
Table_buildTree(this);
}
} else {
if this.needsSort {
let n = this.rows.len() as isize;
insertionSort(&mut this.rows, 0, n - 1, &|a, b| {
a.as_ref()
.unwrap()
.as_ref()
.compare(b.as_ref().unwrap().as_ref())
});
this.rebuild_index();
}
this.displayList.clear();
for i in 0..this.rows.len() {
this.displayList.push(i);
}
}
this.needsSort = false;
}
pub fn Table_expandTree(this: &mut Table) {
for obj in this.rows.iter_mut().flatten() {
obj.as_row_mut().unwrap().showChildren = true;
}
}
pub fn Table_collapseAllBranches(this: &mut Table) {
Table_buildTree(this); this.needsSort = true; for obj in this.rows.iter_mut().flatten() {
let row = obj.as_row_mut().unwrap();
if row.tree_depth > 0 && row.id > 1 {
row.showChildren = false;
}
}
}
pub fn Table_rebuildPanel(this: &mut Table) {
Table_updateDisplayList(this);
let panel = this.panel;
debug_assert!(!panel.is_null());
let (curr_pos, curr_scroll_v, curr_size) = unsafe {
(
Panel_getSelectedIndex(&*panel),
(*panel).scrollV,
Panel_size(&*panel),
)
};
unsafe {
Panel_prune(&mut *panel);
}
if this.following != -1 {
if let Some(&fidx) = this.table.get(&this.following) {
let group = this.row(fidx).group;
let followed_is_visible = true; if this.table.contains_key(&group) && !followed_is_visible {
this.following = group;
}
}
}
let row_count = this.displayList.len();
let mut found_followed = false;
let mut idx: i32 = 0;
for i in 0..row_count {
let row_index = this.displayList[i];
let (show, id) = {
let row = this.row(row_index);
(row.show, row.id)
};
if !show {
continue;
}
let row_ptr: *mut dyn Object = this.rows[row_index].as_mut().unwrap().as_mut();
unsafe {
let p = &mut *panel;
if (idx as usize) < p.items.len() {
p.items[idx as usize] = PanelItem::Borrowed(row_ptr);
} else {
p.items.push(PanelItem::Borrowed(row_ptr));
}
}
if this.following != -1 && id == this.following {
found_followed = true;
unsafe {
Panel_setSelected(&mut *panel, idx);
(*panel).scrollV = idx - (curr_pos - curr_scroll_v);
}
}
idx += 1;
}
if this.following != -1 && !found_followed {
this.following = -1;
unsafe {
Panel_setSelectionColor(&mut *panel, ColorElements::PANEL_SELECTION_FOCUS);
}
}
if this.following == -1 {
unsafe {
if curr_pos > 0 && curr_pos == curr_size - 1 {
Panel_setSelected(&mut *panel, Panel_size(&*panel) - 1);
} else {
Panel_setSelected(&mut *panel, curr_pos);
}
(*panel).scrollV = curr_scroll_v;
}
}
}
pub fn Table_printHeader(settings: &Settings, header: &mut RichString) {
RichString_rewind(header, RichString_size(header));
let ss = &settings.screens[settings.ssIndex as usize];
let key = ScreenSettings_getActiveSortKey(ss);
let scheme = ColorScheme::active();
for &field in &ss.fields {
if field == 0 {
break; }
let color = if ss.treeView && ss.treeViewAlwaysByPID {
ColorElements::PANEL_HEADER_FOCUS.packed(scheme)
} else if key == field {
ColorElements::PANEL_SELECTION_FOCUS.packed(scheme)
} else {
ColorElements::PANEL_HEADER_FOCUS.packed(scheme)
};
RichString_appendWide(
header,
color,
RowField_alignedTitle(settings, field).as_bytes(),
);
if key == field
&& RichString_getCharVal(header, (RichString_size(header) - 1) as usize) == ' '
{
let ascending = ScreenSettings_getActiveDirection(ss) == 1;
RichString_rewind(header, 1);
let glyph = if ascending {
TreeStr::TREE_STR_ASC
} else {
TreeStr::TREE_STR_DESC
};
RichString_appendWide(
header,
ColorElements::PANEL_SELECTION_FOCUS.packed(scheme),
glyph.glyph().as_bytes(),
);
}
if field == ProcessField::COMM as RowField && settings.showMergedCommand {
RichString_appendAscii(header, color, b"(merged)");
}
}
}
pub fn Table_prepareEntries(this: &mut Table) {
for obj in this.rows.iter_mut().flatten() {
let row = obj.as_row_mut().unwrap();
row.updated = false;
row.wasShown = row.show;
row.show = true;
}
}
pub fn Table_cleanupRow(this: &mut Table, idx: usize) -> bool {
let (mono, highlight_changes, highlight_delay) = unsafe {
let host = &*this.host;
let settings = host
.settings
.as_ref()
.expect("Table_cleanupRow: host->settings is NULL");
(
host.monotonicMs,
settings.highlightChanges,
settings.highlightDelaySecs,
)
};
let (tomb, updated, was_shown) = {
let row = this.row(idx);
(row.tombStampMs, row.updated, row.wasShown)
};
let should_remove;
if tomb > 0 {
should_remove = mono >= tomb;
} else if !updated {
if highlight_changes && was_shown {
this.row_mut(idx).tombStampMs = mono + (1000i64 * highlight_delay as i64) as u64;
should_remove = false;
} else {
should_remove = true;
}
} else {
should_remove = false;
}
if should_remove {
Table_removeIndex(this, idx);
return false;
}
true
}
pub fn Table_cleanupEntries(this: &mut Table) {
let mut dirty_index = this.rows.len();
for i in (0..this.rows.len()).rev() {
if !Table_cleanupRow(this, i) {
dirty_index = i;
}
}
Table_compact(this, dirty_index);
}
pub fn Table_compact(this: &mut Table, dirtyIndex: usize) {
if this.rows_isDirty && dirtyIndex < this.rows.len() {
debug_assert!(this.rows[dirtyIndex].is_none());
let items = this.rows.len();
let mut di = dirtyIndex;
for i in (dirtyIndex + 1)..items {
if this.rows[i].is_some() {
this.rows.swap(di, i);
di += 1;
}
}
this.rows.truncate(di);
this.rows_isDirty = false;
this.rebuild_index();
}
this.needsSort = true;
}
pub fn Table_findRow(this: &Table, id: i32) -> Option<&Row> {
this.table.get(&id).and_then(|&i| this.row_opt(i))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::machine::{Machine, ScreenSettings, Settings};
fn host(mono: u64, tree_view: bool, highlight_changes: bool, highlight_delay: i32) -> Machine {
let mut m = Machine::default();
m.monotonicMs = mono;
m.settings = Some(Settings {
highlightChanges: highlight_changes,
highlightDelaySecs: highlight_delay,
screens: vec![ScreenSettings {
treeView: tree_view,
..Default::default()
}],
..Default::default()
});
m
}
fn row(id: i32, group: i32, parent: i32) -> Row {
let mut r = Row::default();
r.id = id;
r.group = group;
r.parent = parent;
r.show = true;
r.showChildren = true;
r
}
fn table_with(h: &Machine, rows: Vec<Row>) -> Table {
let mut t = Table::empty();
Table_init(&mut t, h as *const Machine);
for r in rows {
Table_add(&mut t, Box::new(r));
}
t
}
fn display_ids(t: &Table) -> Vec<i32> {
t.displayList.iter().map(|&i| t.row(i).id).collect()
}
#[test]
fn init_sets_defaults() {
let h = host(0, false, false, 0);
let mut t = Table::empty();
Table_init(&mut t, &h as *const Machine);
assert!(t.needsSort);
assert_eq!(t.following, -1);
assert!(t.rows.is_empty());
assert!(t.displayList.is_empty());
}
#[test]
fn rebuild_panel_borrows_only_shown_rows() {
use crate::ported::panel::{Panel_get, Panel_new, Panel_size};
let h = host(1000, false, false, 0);
let mut t = table_with(&h, vec![row(1, 0, 0), row(2, 0, 0), row(3, 0, 0)]);
t.row_mut(1).show = false;
let mut panel = Panel_new(0, 0, 80, 24, None);
t.panel = &mut panel as *mut Panel;
Table_rebuildPanel(&mut t);
assert_eq!(Panel_size(&panel), 2);
let shown: Vec<i32> = (0..Panel_size(&panel))
.map(|i| Panel_get(&panel, i).as_row().unwrap().id)
.collect();
assert!(shown.contains(&1) && shown.contains(&3));
assert!(!shown.contains(&2));
}
#[test]
fn add_registers_row_and_stamps_seen() {
let h = host(4242, false, false, 0);
let t = table_with(&h, vec![row(10, 10, 0), row(20, 20, 0)]);
assert_eq!(t.rows.len(), 2);
assert_eq!(Table_findRow(&t, 10).unwrap().id, 10);
assert_eq!(Table_findRow(&t, 20).unwrap().id, 20);
assert!(Table_findRow(&t, 99).is_none());
assert_eq!(t.row(0).seenStampMs, 4242);
}
#[test]
fn non_tree_display_list_is_insertion_sorted_by_id() {
let h = host(1, false, false, 0);
let mut t = table_with(&h, vec![row(30, 30, 0), row(10, 10, 0), row(20, 20, 0)]);
Table_updateDisplayList(&mut t);
assert_eq!(display_ids(&t), vec![10, 20, 30]);
assert!(!t.needsSort);
}
#[test]
fn build_tree_orders_children_after_parent_with_depth_and_indent() {
let h = host(1, true, false, 0);
let mut t = table_with(
&h,
vec![row(1, 1, 0), row(2, 2, 1), row(3, 3, 1), row(4, 4, 2)],
);
Table_updateDisplayList(&mut t);
assert_eq!(display_ids(&t), vec![1, 2, 4, 3]);
let depth = |id: i32| Table_findRow(&t, id).unwrap().tree_depth;
assert_eq!(depth(1), 0);
assert_eq!(depth(2), 1);
assert_eq!(depth(4), 2);
assert_eq!(depth(3), 1);
assert_eq!(Table_findRow(&t, 1).unwrap().indent, 0);
assert!(Table_findRow(&t, 3).unwrap().indent < 0);
assert!(Table_findRow(&t, 2).unwrap().indent > 0);
assert!(Table_findRow(&t, 4).unwrap().indent < 0);
assert_eq!(t.displayList.len(), t.rows.len());
}
#[test]
fn build_tree_multiple_roots_sorted_by_id() {
let h = host(1, true, false, 0);
let mut t = table_with(
&h,
vec![
row(100, 100, 0),
row(101, 101, 100),
row(50, 50, 0),
row(51, 51, 50),
],
);
Table_updateDisplayList(&mut t);
assert_eq!(display_ids(&t), vec![50, 51, 100, 101]);
}
#[test]
fn unknown_parent_becomes_root() {
let h = host(1, true, false, 0);
let mut t = table_with(&h, vec![row(5, 5, 999), row(6, 6, 5)]);
Table_updateDisplayList(&mut t);
assert_eq!(display_ids(&t), vec![5, 6]);
assert!(Table_findRow(&t, 5).unwrap().isRoot);
}
#[test]
fn collapsed_children_are_hidden_from_show_but_present_in_list() {
let h = host(1, true, false, 0);
let mut t = table_with(&h, vec![row(1, 1, 0), row(2, 2, 1), row(4, 4, 2)]);
t.row_mut(0).showChildren = false;
Table_updateDisplayList(&mut t);
assert_eq!(display_ids(&t), vec![1, 2, 4]);
assert!(Table_findRow(&t, 1).unwrap().show);
assert!(!Table_findRow(&t, 2).unwrap().show);
assert!(!Table_findRow(&t, 4).unwrap().show);
}
#[test]
fn expand_tree_sets_show_children_everywhere() {
let h = host(1, true, false, 0);
let mut t = table_with(&h, vec![row(1, 1, 0), row(2, 2, 1)]);
t.row_mut(0).showChildren = false;
t.row_mut(1).showChildren = false;
Table_expandTree(&mut t);
assert!(t.row(0).showChildren);
assert!(t.row(1).showChildren);
}
#[test]
fn collapse_all_branches_collapses_non_root_rows() {
let h = host(1, true, false, 0);
let mut t = table_with(&h, vec![row(1, 1, 0), row(2, 2, 1), row(3, 3, 2)]);
Table_collapseAllBranches(&mut t);
assert!(t.needsSort);
assert!(Table_findRow(&t, 1).unwrap().showChildren); assert!(!Table_findRow(&t, 2).unwrap().showChildren);
assert!(!Table_findRow(&t, 3).unwrap().showChildren);
}
#[test]
fn prepare_entries_resets_scan_flags() {
let h = host(1, false, false, 0);
let mut t = table_with(&h, vec![row(1, 1, 0)]);
{
let r = t.row_mut(0);
r.updated = true;
r.show = false;
}
Table_prepareEntries(&mut t);
let r = t.row(0);
assert!(!r.updated);
assert!(r.show);
assert!(!r.wasShown); }
#[test]
fn cleanup_removes_not_updated_row_when_not_highlighting() {
let h = host(1000, false, false, 0);
let mut t = table_with(&h, vec![row(1, 1, 0), row(2, 2, 0), row(3, 3, 0)]);
t.row_mut(0).updated = true;
t.row_mut(1).updated = false;
t.row_mut(2).updated = true;
Table_cleanupEntries(&mut t);
assert_eq!(t.rows.len(), 2);
assert!(t.rows.iter().all(|s| s.is_some()));
assert!(Table_findRow(&t, 1).is_some());
assert!(Table_findRow(&t, 2).is_none());
assert!(Table_findRow(&t, 3).is_some());
assert!(t.needsSort);
}
#[test]
fn cleanup_tombs_not_updated_row_when_highlighting() {
let h = host(1000, false, true, 5);
let mut t = table_with(&h, vec![row(7, 7, 0)]);
{
let r = t.row_mut(0);
r.updated = false;
r.wasShown = true;
}
Table_cleanupEntries(&mut t);
assert_eq!(t.rows.len(), 1);
assert_eq!(t.row(0).tombStampMs, 1000 + 1000 * 5);
}
#[test]
fn cleanup_removes_tombed_row_after_stamp_elapses() {
let h = host(1000, false, true, 5);
let mut t = table_with(&h, vec![row(8, 8, 0)]);
{
let r = t.row_mut(0);
r.updated = false;
r.tombStampMs = 500;
}
Table_cleanupEntries(&mut t);
assert!(t.rows.is_empty());
assert!(Table_findRow(&t, 8).is_none());
}
#[test]
fn remove_index_via_cleanup_clears_following() {
let h = host(1000, false, false, 0);
let mut t = table_with(&h, vec![row(1, 1, 0), row(2, 2, 0)]);
t.following = 2;
t.row_mut(0).updated = true;
t.row_mut(1).updated = false; Table_cleanupEntries(&mut t);
assert_eq!(t.following, -1);
}
#[test]
fn compact_preserves_order_of_survivors() {
let h = host(1000, false, false, 0);
let mut t = table_with(
&h,
vec![
row(1, 1, 0),
row(2, 2, 0),
row(3, 3, 0),
row(4, 4, 0),
row(5, 5, 0),
],
);
for i in 0..5 {
t.row_mut(i).updated = true;
}
t.row_mut(2).updated = false; Table_cleanupEntries(&mut t);
let ids: Vec<i32> = t
.rows
.iter()
.map(|s| s.as_ref().unwrap().as_row().unwrap().id)
.collect();
assert_eq!(ids, vec![1, 2, 4, 5]);
assert_eq!(t.row(*t.table.get(&4).unwrap()).id, 4);
}
#[test]
fn print_header_renders_columns_and_is_idempotent() {
use crate::ported::settings::ScreenSettings;
let mut settings = Settings::default();
settings.screens = vec![ScreenSettings {
fields: vec![
ProcessField::PID as RowField,
ProcessField::NICE as RowField,
],
sortKey: ProcessField::PID as RowField,
direction: 1,
..Default::default()
}];
let mut header = RichString::default();
Table_printHeader(&settings, &mut header);
let n = RichString_size(&header);
assert!(n > 0);
Table_printHeader(&settings, &mut header);
assert_eq!(RichString_size(&header), n);
}
}