Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e51cdb0c50 | |||
| 1cb05d404b | |||
| 4196066e57 | |||
| 47e96c7d92 | |||
| bae2ecb79a | |||
| bd0d15a1ae | |||
| 689498c5f4 | |||
| 34e260a612 |
+36
-8
@@ -24,8 +24,16 @@ pub fn draw_disks(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter duplicates by keeping first occurrence of each unique name
|
||||||
|
let mut seen_names = std::collections::HashSet::new();
|
||||||
|
let unique_disks: Vec<_> = mm
|
||||||
|
.disks
|
||||||
|
.iter()
|
||||||
|
.filter(|d| seen_names.insert(d.name.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
let per_disk_h = 3u16;
|
let per_disk_h = 3u16;
|
||||||
let max_cards = (inner.height / per_disk_h).min(mm.disks.len() as u16) as usize;
|
let max_cards = (inner.height / per_disk_h).min(unique_disks.len() as u16) as usize;
|
||||||
|
|
||||||
let constraints: Vec<Constraint> = (0..max_cards)
|
let constraints: Vec<Constraint> = (0..max_cards)
|
||||||
.map(|_| Constraint::Length(per_disk_h))
|
.map(|_| Constraint::Length(per_disk_h))
|
||||||
@@ -36,7 +44,7 @@ pub fn draw_disks(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
|
|||||||
.split(inner);
|
.split(inner);
|
||||||
|
|
||||||
for (i, slot) in rows.iter().enumerate() {
|
for (i, slot) in rows.iter().enumerate() {
|
||||||
let d = &mm.disks[i];
|
let d = unique_disks[i];
|
||||||
let used = d.total.saturating_sub(d.available);
|
let used = d.total.saturating_sub(d.available);
|
||||||
let ratio = if d.total > 0 {
|
let ratio = if d.total > 0 {
|
||||||
used as f64 / d.total as f64
|
used as f64 / d.total as f64
|
||||||
@@ -53,23 +61,43 @@ pub fn draw_disks(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
|
|||||||
ratatui::style::Color::Red
|
ratatui::style::Color::Red
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add indentation for partitions
|
||||||
|
let indent = if d.is_partition { "└─" } else { "" };
|
||||||
|
|
||||||
|
// Add temperature if available
|
||||||
|
let temp_str = d
|
||||||
|
.temperature
|
||||||
|
.map(|t| format!(" {}°C", t.round() as i32))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let title = format!(
|
let title = format!(
|
||||||
"{} {} {} / {} ({}%)",
|
"{}{}{}{} {} / {} ({}%)",
|
||||||
|
indent,
|
||||||
disk_icon(&d.name),
|
disk_icon(&d.name),
|
||||||
truncate_middle(&d.name, (slot.width.saturating_sub(6)) as usize / 2),
|
truncate_middle(&d.name, (slot.width.saturating_sub(6)) as usize / 2),
|
||||||
|
temp_str,
|
||||||
human(used),
|
human(used),
|
||||||
human(d.total),
|
human(d.total),
|
||||||
pct
|
pct
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Indent the entire card (block) for partitions to align with └─ prefix (4 chars)
|
||||||
|
let card_indent = if d.is_partition { 4 } else { 0 };
|
||||||
|
let card_rect = Rect {
|
||||||
|
x: slot.x + card_indent,
|
||||||
|
y: slot.y,
|
||||||
|
width: slot.width.saturating_sub(card_indent),
|
||||||
|
height: slot.height,
|
||||||
|
};
|
||||||
|
|
||||||
let card = Block::default().borders(Borders::ALL).title(title);
|
let card = Block::default().borders(Borders::ALL).title(title);
|
||||||
f.render_widget(card, *slot);
|
f.render_widget(card, card_rect);
|
||||||
|
|
||||||
let inner_card = Rect {
|
let inner_card = Rect {
|
||||||
x: slot.x + 1,
|
x: card_rect.x + 1,
|
||||||
y: slot.y + 1,
|
y: card_rect.y + 1,
|
||||||
width: slot.width.saturating_sub(2),
|
width: card_rect.width.saturating_sub(2),
|
||||||
height: slot.height.saturating_sub(2),
|
height: card_rect.height.saturating_sub(2),
|
||||||
};
|
};
|
||||||
if inner_card.height == 0 {
|
if inner_card.height == 0 {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -331,14 +331,194 @@ pub async fn collect_disks(state: &AppState) -> Vec<DiskInfo> {
|
|||||||
}
|
}
|
||||||
let mut disks_list = state.disks.lock().await;
|
let mut disks_list = state.disks.lock().await;
|
||||||
disks_list.refresh(false); // don't drop missing disks
|
disks_list.refresh(false); // don't drop missing disks
|
||||||
let disks: Vec<DiskInfo> = disks_list
|
|
||||||
|
// Collect disk temperatures from components
|
||||||
|
// NVMe temps show up as "Composite" under different chip names
|
||||||
|
let disk_temps = {
|
||||||
|
let mut components = state.components.lock().await;
|
||||||
|
components.refresh(true); // true = refresh values, not just the list
|
||||||
|
|
||||||
|
let mut composite_temps = Vec::new();
|
||||||
|
|
||||||
|
for c in components.iter() {
|
||||||
|
let label = c.label().to_ascii_lowercase();
|
||||||
|
|
||||||
|
// Collect all "Composite" temperatures (these are NVMe drives)
|
||||||
|
// Labels are like "nvme Composite CT1000N7BSS503" or "nvme Composite Sabrent Rocket 4.0"
|
||||||
|
if label.contains("composite")
|
||||||
|
&& let Some(temp) = c.temperature()
|
||||||
|
{
|
||||||
|
tracing::debug!("Found Composite temp: {}°C", temp);
|
||||||
|
composite_temps.push(temp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store composite temps indexed by their order (nvme0n1, nvme1n1, nvme2n1, etc.)
|
||||||
|
let mut temps = std::collections::HashMap::new();
|
||||||
|
for (idx, temp) in composite_temps.iter().enumerate() {
|
||||||
|
let key = format!("nvme{}n1", idx);
|
||||||
|
tracing::debug!("Mapping {} -> {}°C", key, temp);
|
||||||
|
temps.insert(key, *temp);
|
||||||
|
}
|
||||||
|
tracing::debug!("Final disk_temps map: {:?}", temps);
|
||||||
|
temps
|
||||||
|
};
|
||||||
|
|
||||||
|
// First collect all partitions from sysinfo, deduplicating by device name
|
||||||
|
// (same partition can be mounted at multiple mount points)
|
||||||
|
let mut seen_partitions = std::collections::HashSet::new();
|
||||||
|
let partitions: Vec<DiskInfo> = disks_list
|
||||||
.iter()
|
.iter()
|
||||||
.map(|d| DiskInfo {
|
.filter_map(|d| {
|
||||||
name: d.name().to_string_lossy().into_owned(),
|
let name = d.name().to_string_lossy().into_owned();
|
||||||
total: d.total_space(),
|
|
||||||
available: d.available_space(),
|
// Skip if we've already seen this partition/device
|
||||||
|
if !seen_partitions.insert(name.clone()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if this is a partition
|
||||||
|
let is_partition = name.contains("p1")
|
||||||
|
|| name.contains("p2")
|
||||||
|
|| name.contains("p3")
|
||||||
|
|| name.ends_with('1')
|
||||||
|
|| name.ends_with('2')
|
||||||
|
|| name.ends_with('3')
|
||||||
|
|| name.ends_with('4')
|
||||||
|
|| name.ends_with('5')
|
||||||
|
|| name.ends_with('6')
|
||||||
|
|| name.ends_with('7')
|
||||||
|
|| name.ends_with('8')
|
||||||
|
|| name.ends_with('9');
|
||||||
|
|
||||||
|
// Try to find temperature for this disk
|
||||||
|
let temperature = disk_temps.iter().find_map(|(key, &temp)| {
|
||||||
|
if name.starts_with(key) {
|
||||||
|
tracing::debug!("Matched {} with key {} -> {}°C", name, key, temp);
|
||||||
|
Some(temp)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if temperature.is_none() && !name.starts_with("loop") && !name.starts_with("ram") {
|
||||||
|
tracing::debug!("No temperature found for disk: {}", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(DiskInfo {
|
||||||
|
name,
|
||||||
|
total: d.total_space(),
|
||||||
|
available: d.available_space(),
|
||||||
|
temperature,
|
||||||
|
is_partition,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Now create parent disk entries by aggregating partition data
|
||||||
|
let mut parent_disks: std::collections::HashMap<String, (u64, u64, Option<f32>)> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
|
for partition in &partitions {
|
||||||
|
if partition.is_partition {
|
||||||
|
// Extract parent disk name
|
||||||
|
// nvme0n1p1 -> nvme0n1, sda1 -> sda, mmcblk0p1 -> mmcblk0
|
||||||
|
let parent_name = if let Some(pos) = partition.name.rfind('p') {
|
||||||
|
// Check if character after 'p' is a digit
|
||||||
|
if partition
|
||||||
|
.name
|
||||||
|
.chars()
|
||||||
|
.nth(pos + 1)
|
||||||
|
.is_some_and(|c| c.is_ascii_digit())
|
||||||
|
{
|
||||||
|
&partition.name[..pos]
|
||||||
|
} else {
|
||||||
|
// Handle sda1, sdb2, etc (just trim trailing digit)
|
||||||
|
partition.name.trim_end_matches(char::is_numeric)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle sda1, sdb2, etc (just trim trailing digit)
|
||||||
|
partition.name.trim_end_matches(char::is_numeric)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look up temperature for the PARENT disk, not the partition
|
||||||
|
// Strip /dev/ prefix if present for matching
|
||||||
|
let parent_name_for_match = parent_name.strip_prefix("/dev/").unwrap_or(parent_name);
|
||||||
|
let parent_temp = disk_temps.iter().find_map(|(key, &temp)| {
|
||||||
|
if parent_name_for_match.starts_with(key) {
|
||||||
|
Some(temp)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aggregate partition stats into parent
|
||||||
|
let entry = parent_disks
|
||||||
|
.entry(parent_name.to_string())
|
||||||
|
.or_insert((0, 0, parent_temp));
|
||||||
|
entry.0 += partition.total;
|
||||||
|
entry.1 += partition.available;
|
||||||
|
// Keep temperature if any partition has it (or if we just found one)
|
||||||
|
if entry.2.is_none() {
|
||||||
|
entry.2 = parent_temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create parent disk entries
|
||||||
|
let mut disks: Vec<DiskInfo> = parent_disks
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, (total, available, temperature))| DiskInfo {
|
||||||
|
name,
|
||||||
|
total,
|
||||||
|
available,
|
||||||
|
temperature,
|
||||||
|
is_partition: false,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sort parent disks by name
|
||||||
|
disks.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
||||||
|
// Add partitions after their parent disk
|
||||||
|
for partition in partitions {
|
||||||
|
if partition.is_partition {
|
||||||
|
// Find parent disk index
|
||||||
|
let parent_name = if let Some(pos) = partition.name.rfind('p') {
|
||||||
|
if partition
|
||||||
|
.name
|
||||||
|
.chars()
|
||||||
|
.nth(pos + 1)
|
||||||
|
.is_some_and(|c| c.is_ascii_digit())
|
||||||
|
{
|
||||||
|
&partition.name[..pos]
|
||||||
|
} else {
|
||||||
|
partition.name.trim_end_matches(char::is_numeric)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
partition.name.trim_end_matches(char::is_numeric)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find where to insert this partition (after its parent)
|
||||||
|
if let Some(parent_idx) = disks.iter().position(|d| d.name == parent_name) {
|
||||||
|
// Insert after parent and any existing partitions of that parent
|
||||||
|
let mut insert_idx = parent_idx + 1;
|
||||||
|
while insert_idx < disks.len()
|
||||||
|
&& disks[insert_idx].is_partition
|
||||||
|
&& disks[insert_idx].name.starts_with(parent_name)
|
||||||
|
{
|
||||||
|
insert_idx += 1;
|
||||||
|
}
|
||||||
|
disks.insert(insert_idx, partition);
|
||||||
|
} else {
|
||||||
|
// Parent not found (shouldn't happen), just add at end
|
||||||
|
disks.push(partition);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not a partition (e.g., zram0), add at end
|
||||||
|
disks.push(partition);
|
||||||
|
}
|
||||||
|
}
|
||||||
{
|
{
|
||||||
let mut cache = state.cache_disks.lock().await;
|
let mut cache = state.cache_disks.lock().await;
|
||||||
cache.set(disks.clone());
|
cache.set(disks.clone());
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ pub struct DiskInfo {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub total: u64,
|
pub total: u64,
|
||||||
pub available: u64,
|
pub available: u64,
|
||||||
|
pub temperature: Option<f32>,
|
||||||
|
pub is_partition: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ pub struct DiskInfo {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub total: u64,
|
pub total: u64,
|
||||||
pub available: u64,
|
pub available: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub temperature: Option<f32>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_partition: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|||||||
Reference in New Issue
Block a user