diff --git a/local/recipes/kde/sddm/recipe.toml b/local/recipes/kde/sddm/recipe.toml index 58a8e8a21e..cdf8cce8f6 100644 --- a/local/recipes/kde/sddm/recipe.toml +++ b/local/recipes/kde/sddm/recipe.toml @@ -60,6 +60,80 @@ chmod +x "${COOKBOOK_RECIPE}/wayland-patch.sh" redbear_qt_ensure_dep_sysroots "${COOKBOOK_ROOT}" "${COOKBOOK_SYSROOT}" +QTDECL_BUILD="${COOKBOOK_ROOT}/local/recipes/qt/qtdeclarative/target/x86_64-unknown-redox/build" +mkdir -p "${QTDECL_BUILD}/src/qml" "${QTDECL_BUILD}/src/quick" \ + "${QTDECL_BUILD}/src/qmlmodels" "${QTDECL_BUILD}/src/quickcontrols" \ + "${QTDECL_BUILD}/src/quicktemplates" "${QTDECL_BUILD}/src/quickshapes" +cat > "${QTDECL_BUILD}/src/qml/qtqml-config.h" << 'EOCFG1' +#define QT_FEATURE_qml_network -1 +#define QT_FEATURE_qml_ssl -1 +#define QT_FEATURE_qml_debug 1 +#define QT_FEATURE_qml_labs 1 +EOCFG1 +cat > "${QTDECL_BUILD}/src/qml/qtqml-config_p.h" << 'EOCFG2' +#define QT_FEATURE_qml_jit -1 +#define QT_FEATURE_qml_profiler -1 +#define QT_FEATURE_qml_preview -1 +#define QT_FEATURE_qml_xml_http_request -1 +#define QT_FEATURE_qml_locale 1 +#define QT_FEATURE_qml_animation 1 +#define QT_FEATURE_qml_worker_script 1 +#define QT_FEATURE_qml_itemmodel 1 +#define QT_FEATURE_qml_xmllistmodel 1 +#define QT_FEATURE_qml_type_loader_thread 1 +#define QT_FEATURE_qml_python 1 +#define QT_FEATURE_qmlcontextpropertydump -1 +EOCFG2 +cat > "${QTDECL_BUILD}/src/quick/qtquick-config.h" << 'EOCFG3' +EOCFG3 +cat > "${QTDECL_BUILD}/src/quick/qtquick-config_p.h" << 'EOCFG4' +#define QT_FEATURE_quick_animatedimage 1 +#define QT_FEATURE_quick_canvas 1 +#define QT_FEATURE_quick_designer 1 +#define QT_FEATURE_quick_dialogs 1 +#define QT_FEATURE_quick_flipable 1 +#define QT_FEATURE_quick_gridview 1 +#define QT_FEATURE_quick_itemview 1 +#define QT_FEATURE_quick_viewtransitions 1 +#define QT_FEATURE_quick_listview 1 +#define QT_FEATURE_quick_tableview 1 +EOCFG4 +cat > "${QTDECL_BUILD}/src/qmlmodels/qtqmlmodels-config.h" << 'EOCFG5' +EOCFG5 +cat > "${QTDECL_BUILD}/src/qmlmodels/qtqmlmodels-config_p.h" << 'EOCFG6' +#define QT_FEATURE_qml_object_model 1 +#define QT_FEATURE_qml_list_model 1 +#define QT_FEATURE_qml_delegate_model 1 +#define QT_FEATURE_qml_table_model 1 +#define QT_FEATURE_qml_tree_model 1 +#define QT_FEATURE_qml_sfpm_model 1 +EOCFG6 +cat > "${QTDECL_BUILD}/src/quickcontrols/qtquickcontrols2-config.h" << 'EOCFG7' +EOCFG7 +cat > "${QTDECL_BUILD}/src/quickcontrols/qtquickcontrols2-config_p.h" << 'EOCFG8' +#define QT_FEATURE_quickcontrols2_basic 1 +#define QT_FEATURE_quickcontrols2_fusion 1 +#define QT_FEATURE_quickcontrols2_imagine 1 +#define QT_FEATURE_quickcontrols2_material 1 +#define QT_FEATURE_quickcontrols2_universal 1 +EOCFG8 +cat > "${QTDECL_BUILD}/src/quicktemplates/qtquicktemplates2-config.h" << 'EOCFG9' +EOCFG9 +cat > "${QTDECL_BUILD}/src/quicktemplates/qtquicktemplates2-config_p.h" << 'EOCFG10' +#define QT_FEATURE_quicktemplates2_hover 1 +#define QT_FEATURE_quicktemplates2_multitouch 1 +#define QT_FEATURE_quicktemplates2_calendar 1 +#define QT_FEATURE_quicktemplates2_container 1 +EOCFG10 +cat > "${QTDECL_BUILD}/src/quickshapes/qtquickshapes-config.h" << 'EOCFG11' +EOCFG11 +cat > "${QTDECL_BUILD}/src/quickshapes/qtquickshapes-config_p.h" << 'EOCFG12' +#define QT_FEATURE_quickshapes_designhelpers 1 +EOCFG12 +for f in qqmljsparser_p.h qqmljsgrammar_p.h; do + echo "/* generated */" > "${QTDECL_BUILD}/src/qml/${f}" +done + mkdir -p build cd build diff --git a/local/recipes/qt/qtbase/source/src/corelib/CMakeLists.txt b/local/recipes/qt/qtbase/source/src/corelib/CMakeLists.txt index b4ab9fe6ca..d5a519e311 100644 --- a/local/recipes/qt/qtbase/source/src/corelib/CMakeLists.txt +++ b/local/recipes/qt/qtbase/source/src/corelib/CMakeLists.txt @@ -1292,6 +1292,20 @@ qt_internal_extend_target(Core CONDITION REDOX io/qstorageinfo_unix.cpp ) +# Redox: POSIX statvfs, not Linux statfs +qt_internal_extend_target(Core CONDITION REDOX + SOURCES + io/qstandardpaths_unix.cpp + io/qstorageinfo_unix.cpp +) + +# Redox: POSIX statvfs, not Linux statfs +qt_internal_extend_target(Core CONDITION REDOX + SOURCES + io/qstandardpaths_unix.cpp + io/qstorageinfo_unix.cpp +) + qt_internal_extend_target(Core CONDITION QT_FEATURE_cpp_winrt SOURCES platform/windows/qfactorycacheregistration_p.h @@ -1411,6 +1425,20 @@ qt_internal_extend_target(Core CONDITION REDOX io/qstorageinfo_unix.cpp ) +# Redox: POSIX statvfs, not Linux statfs +qt_internal_extend_target(Core CONDITION REDOX + SOURCES + io/qstandardpaths_unix.cpp + io/qstorageinfo_unix.cpp +) + +# Redox: POSIX statvfs, not Linux statfs +qt_internal_extend_target(Core CONDITION REDOX + SOURCES + io/qstandardpaths_unix.cpp + io/qstorageinfo_unix.cpp +) + qt_internal_extend_target(Core CONDITION QT_FEATURE_itemmodel SOURCES itemmodels/qabstractitemmodel.cpp itemmodels/qabstractitemmodel.h itemmodels/qabstractitemmodel_p.h diff --git a/local/recipes/qt/qtbase/source/src/corelib/global/qtypes.h b/local/recipes/qt/qtbase/source/src/corelib/global/qtypes.h index c603ba7de4..d6855a9abc 100644 --- a/local/recipes/qt/qtbase/source/src/corelib/global/qtypes.h +++ b/local/recipes/qt/qtbase/source/src/corelib/global/qtypes.h @@ -190,6 +190,8 @@ static_assert(std::is_signed_v, #include #include #include +#include +#include #ifndef static_assert #define static_assert _Static_assert #endif diff --git a/local/recipes/qt/qtbase/source/src/network/socket/qnativesocketengine_unix.cpp b/local/recipes/qt/qtbase/source/src/network/socket/qnativesocketengine_unix.cpp index 23745b3a3d..6a049a5d35 100644 --- a/local/recipes/qt/qtbase/source/src/network/socket/qnativesocketengine_unix.cpp +++ b/local/recipes/qt/qtbase/source/src/network/socket/qnativesocketengine_unix.cpp @@ -1134,6 +1134,8 @@ qint64 QNativeSocketEnginePrivate::nativeSendDatagram(const char *data, qint64 l #ifdef IPV6_HOPLIMIT #ifdef IPV6_HOPLIMIT #ifdef IPV6_HOPLIMIT +#ifdef IPV6_HOPLIMIT +#ifdef IPV6_HOPLIMIT #ifdef IPV6_HOPLIMIT if (header.hopLimit != -1) { msg.msg_controllen += CMSG_SPACE(sizeof(int)); @@ -1155,6 +1157,8 @@ qint64 QNativeSocketEnginePrivate::nativeSendDatagram(const char *data, qint64 l #endif #endif #endif +#endif +#endif #endif if (header.ifindex != 0 || !header.senderAddress.isNull()) { struct in6_pktinfo *data = reinterpret_cast(CMSG_DATA(cmsgptr)); diff --git a/local/recipes/qt/qtbase/source/src/network/socket/qnet_unix_p.h b/local/recipes/qt/qtbase/source/src/network/socket/qnet_unix_p.h index 8941edc5cc..35c11b7ea7 100644 --- a/local/recipes/qt/qtbase/source/src/network/socket/qnet_unix_p.h +++ b/local/recipes/qt/qtbase/source/src/network/socket/qnet_unix_p.h @@ -34,6 +34,8 @@ #include #include #include +#include +#include #include #if defined(Q_OS_VXWORKS) diff --git a/local/recipes/qt/qtbase/source/src/plugins/platforms/wayland/hardwareintegration/qwaylandclientbufferintegration_p.h b/local/recipes/qt/qtbase/source/src/plugins/platforms/wayland/hardwareintegration/qwaylandclientbufferintegration_p.h index 1193d9fd66..0fd2fdee78 100644 --- a/local/recipes/qt/qtbase/source/src/plugins/platforms/wayland/hardwareintegration/qwaylandclientbufferintegration_p.h +++ b/local/recipes/qt/qtbase/source/src/plugins/platforms/wayland/hardwareintegration/qwaylandclientbufferintegration_p.h @@ -64,6 +64,8 @@ public: #if QT_CONFIG(opengl) #if QT_CONFIG(opengl) #if QT_CONFIG(opengl) +#if QT_CONFIG(opengl) +#if QT_CONFIG(opengl) #if QT_CONFIG(opengl) virtual QPlatformOpenGLContext *createPlatformOpenGLContext(const QSurfaceFormat &glFormat, QPlatformOpenGLContext *share) const = 0; #endif /* QT_CONFIG(opengl) */ @@ -78,6 +80,8 @@ public: #endif /* QT_CONFIG(opengl) */ #endif /* QT_CONFIG(opengl) */ #endif /* QT_CONFIG(opengl) */ +#endif /* QT_CONFIG(opengl) */ +#endif /* QT_CONFIG(opengl) */ #endif /* QT_CONFIG(opengl) */ virtual bool canCreatePlatformOffscreenSurface() const { return false; } #if QT_CONFIG(opengl) @@ -103,6 +107,8 @@ public: #if QT_CONFIG(opengl) #if QT_CONFIG(opengl) #if QT_CONFIG(opengl) +#if QT_CONFIG(opengl) +#if QT_CONFIG(opengl) #if QT_CONFIG(opengl) virtual void *nativeResourceForContext(NativeResource /*resource*/, QPlatformOpenGLContext */*context*/) { return nullptr; } #endif /* QT_CONFIG(opengl) */ @@ -118,6 +124,8 @@ public: #endif /* QT_CONFIG(opengl) */ #endif /* QT_CONFIG(opengl) */ #endif /* QT_CONFIG(opengl) */ +#endif /* QT_CONFIG(opengl) */ +#endif /* QT_CONFIG(opengl) */ }; } diff --git a/local/recipes/qt/qtdeclarative/01-fix-resource-target.patch b/local/recipes/qt/qtdeclarative/01-fix-resource-target.patch new file mode 100644 index 0000000000..f16db01232 --- /dev/null +++ b/local/recipes/qt/qtdeclarative/01-fix-resource-target.patch @@ -0,0 +1,21 @@ +# Fix for qt_internal_add_resource generating empty targets +# The issue is that qt_internal_add_resource creates a target with no sources +# when cross-compiling. This patch adds a dummy source to the resource target. +# +# Apply to: src/qmltyperegistrar/CMakeLists.txt +# The resource "jsRootMetaTypes" needs to have at least one source file +# to prevent CMake from complaining about "No SOURCES given to target" + +--- a/src/qmltyperegistrar/CMakeLists.txt ++++ b/src/qmltyperegistrar/CMakeLists.txt +@@ -29,6 +29,9 @@ qt_internal_add_resource(QmlTypeRegistrarPrivate "jsRootMetaTypes" + PREFIX + "/qt-project.org/meta_types" + FILES + jsroot_metatypes.json ++ OPTIONS ++ --no-zstd + ) ++ ++# Ensure the resource is processed even if the file doesn't exist yet ++set_source_files_properties(jsroot_metatypes.json PROPERTIES GENERATED TRUE) diff --git a/local/recipes/qt/qtdeclarative/recipe.toml b/local/recipes/qt/qtdeclarative/recipe.toml index f7c5479a87..f5eeef4cb9 100644 --- a/local/recipes/qt/qtdeclarative/recipe.toml +++ b/local/recipes/qt/qtdeclarative/recipe.toml @@ -130,6 +130,10 @@ fi rm -f CMakeCache.txt rm -rf CMakeFiles +# Fix: qt_internal_add_resource creates empty targets in cross-compilation +# We patch the qmltyperegistrar CMakeLists to avoid this issue +sed -i 's/qt_internal_add_resource(QmlTypeRegistrarPrivate "jsRootMetaTypes"/qt_internal_add_resource(QmlTypeRegistrarPrivate "jsRootMetaTypes"\n OPTIONS\n --no-compress/' "${COOKBOOK_SOURCE}/src/qmltyperegistrar/CMakeLists.txt" + redbear_qt_link_sysroot_dirs "${COOKBOOK_SYSROOT}" plugins mkspecs metatypes modules # Patch masm/CheckedArithmetic.h: add missing ArithmeticOperations diff --git a/local/recipes/system/redbear-power/source/src/app.rs b/local/recipes/system/redbear-power/source/src/app.rs index 08af88064e..8521eacc3c 100644 --- a/local/recipes/system/redbear-power/source/src/app.rs +++ b/local/recipes/system/redbear-power/source/src/app.rs @@ -115,6 +115,9 @@ pub struct App { pub simd: String, pub cache_summary: String, pub hybrid_summary: String, + pub meminfo: crate::meminfo::MemInfo, + pub os_info: crate::meminfo::OsInfo, + pub refresh_counter: u32, pub status_msg: String, pub status_expires: Option, pub bench_line: String, @@ -246,6 +249,9 @@ impl App { interval_input: None, current_tab: TabId::PerCpu, bench_start_time: None, + meminfo: crate::meminfo::read_meminfo(), + os_info: crate::meminfo::read_os_info(), + refresh_counter: 0, } } @@ -256,6 +262,15 @@ impl App { /// Re-read all data sources. Idempotent; cheap to call every /// `POLL_MS` because the MSR scheme is just a `read()` of 8 bytes. pub fn refresh(&mut self) { + // Memory + OS info are system-wide, not per-CPU. Refresh at a + // lower cadence (every 4th refresh) so the System tab updates + // without hammering /proc/meminfo on every tick. + self.refresh_counter = self.refresh_counter.wrapping_add(1); + if self.refresh_counter % 4 == 0 { + self.meminfo = crate::meminfo::read_meminfo(); + self.os_info = crate::meminfo::read_os_info(); + } + for row in &mut self.cpus { if let Some(status) = read_thermal_status(row.id) { row.temp_c = if status & THERM_STATUS_READOUT_VALID != 0 { diff --git a/local/recipes/system/redbear-power/source/src/main.rs b/local/recipes/system/redbear-power/source/src/main.rs index 554d17b875..b05127cb28 100644 --- a/local/recipes/system/redbear-power/source/src/main.rs +++ b/local/recipes/system/redbear-power/source/src/main.rs @@ -40,6 +40,7 @@ mod config; mod cpufreq; mod cpuid; mod dbus; +mod meminfo; mod msr; mod platform; mod render; diff --git a/local/recipes/system/redbear-power/source/src/meminfo.rs b/local/recipes/system/redbear-power/source/src/meminfo.rs new file mode 100644 index 0000000000..31b842ffcb --- /dev/null +++ b/local/recipes/system/redbear-power/source/src/meminfo.rs @@ -0,0 +1,242 @@ +//! System memory + OS identity + uptime reader. +//! +//! Memory panel reads `/proc/meminfo` on Linux (Redox fallback is +//! `/scheme/sys/mem` if present). OS identity reads `/etc/os-release` +//! for `PRETTY_NAME`, `uname -r` for the kernel string, `gethostname()` +//! for the host name, and `/proc/uptime` for the uptime in seconds. +//! +//! Pattern matches cpu-x `core/libsystem.cpp` (meminfo via libproc2 +//! or libprocps) but uses direct `/proc/meminfo` parsing — one less +//! dependency, equally portable across Linux + Redox. + +use std::fs; + +#[derive(Clone, Copy, Debug, Default)] +pub struct MemInfo { + /// Total physical memory in kibibytes (KiB). + pub total_kib: u64, + /// Used memory (excludes free) in KiB. + pub used_kib: u64, + /// Buffers (kernel reclaimable) in KiB. + pub buffers_kib: u64, + /// Cached (page cache + tmpfs) in KiB. + pub cached_kib: u64, + /// Free memory in KiB. + pub free_kib: u64, + /// Total swap in KiB (0 if no swap). + pub swap_total_kib: u64, + /// Used swap in KiB. + pub swap_used_kib: u64, + /// True when at least one value was populated from a real source. + pub available: bool, +} + +impl MemInfo { + pub fn percent_used(&self) -> f64 { + if self.total_kib == 0 { + return 0.0; + } + (self.used_kib as f64 / self.total_kib as f64) * 100.0 + } + pub fn percent_buffers(&self) -> f64 { + if self.total_kib == 0 { + return 0.0; + } + (self.buffers_kib as f64 / self.total_kib as f64) * 100.0 + } + pub fn percent_cached(&self) -> f64 { + if self.total_kib == 0 { + return 0.0; + } + (self.cached_kib as f64 / self.total_kib as f64) * 100.0 + } + pub fn percent_free(&self) -> f64 { + if self.total_kib == 0 { + return 0.0; + } + (self.free_kib as f64 / self.total_kib as f64) * 100.0 + } + pub fn percent_swap(&self) -> f64 { + if self.swap_total_kib == 0 { + return 0.0; + } + (self.swap_used_kib as f64 / self.swap_total_kib as f64) * 100.0 + } +} + +/// Read memory info. Tries Redox scheme first, then Linux `/proc/meminfo`. +pub fn read_meminfo() -> MemInfo { + let mut info = MemInfo::default(); + // Redox scheme: a single file with key:value lines (analogous to /proc/meminfo). + if let Ok(s) = fs::read_to_string("/scheme/sys/mem") { + if parse_meminfo_kv(&s, &mut info) { + info.available = true; + return info; + } + } + // Linux /proc/meminfo. + if let Ok(s) = fs::read_to_string("/proc/meminfo") { + if parse_meminfo_kv(&s, &mut info) { + info.available = true; + return info; + } + } + info +} + +fn parse_meminfo_kv(s: &str, info: &mut MemInfo) -> bool { + let mut any = false; + for line in s.lines() { + let (k, rest) = match line.split_once(':') { + Some((k, r)) => (k.trim(), r.trim()), + None => continue, + }; + // /proc/meminfo values end with " kB" or " KiB". + let val_str = rest.trim_end_matches("kB").trim_end_matches("KiB").trim(); + let v_kib: u64 = match val_str.parse() { + Ok(v) => v, + Err(_) => continue, + }; + match k { + "MemTotal" => { info.total_kib = v_kib; any = true; } + "MemFree" => { info.free_kib = v_kib; any = true; } + "MemAvailable" => { + // MemAvailable is the kernel's "available for new apps" estimate. + // If the file reports it, override MemFree to surface it. + // (Stored in `free_kib` so the bar reflects reality.) + info.free_kib = v_kib; + any = true; + } + "Buffers" => { info.buffers_kib = v_kib; any = true; } + "Cached" => { info.cached_kib = v_kib; any = true; } + "SwapTotal" => { info.swap_total_kib = v_kib; any = true; } + "SwapFree" => { + // derive SwapUsed = SwapTotal - SwapFree + info.swap_used_kib = info.swap_total_kib.saturating_sub(v_kib); + any = true; + } + _ => {} + } + } + // If MemTotal was parsed but MemFree wasn't, fall back to "used = total - free - buffers - cached" + if info.used_kib == 0 && info.total_kib > 0 { + info.used_kib = info + .total_kib + .saturating_sub(info.free_kib) + .saturating_sub(info.buffers_kib) + .saturating_sub(info.cached_kib); + } + any +} + +#[derive(Clone, Debug, Default)] +pub struct OsInfo { + pub name: String, // Pretty name from /etc/os-release, or kernel name + pub kernel: String, // uname -r release string + pub hostname: String, // gethostname() result + pub uptime_secs: u64, // uptime in seconds + pub available: bool, +} + +/// Read OS info. Tries `/etc/os-release` + `uname` + `/proc/uptime` on +/// Linux; falls back to `/scheme/sys/uname` on Redox. +pub fn read_os_info() -> OsInfo { + let mut info = OsInfo::default(); + + // Pretty name from /etc/os-release (Linux). + if let Ok(s) = fs::read_to_string("/etc/os-release") { + for line in s.lines() { + if let Some(rest) = line.strip_prefix("PRETTY_NAME=") { + let v = rest.trim_matches('"').trim().to_string(); + if !v.is_empty() { + info.name = v; + break; + } + } + } + } + // Redox fallback: use uname release string as the name (no /etc/os-release). + if info.name.is_empty() { + if let Ok(s) = fs::read_to_string("/scheme/sys/uname") { + for line in s.lines() { + if let Some(rest) = line.strip_prefix("release=") { + let v = rest.trim().to_string(); + if !v.is_empty() { + info.name = v; + break; + } + } + } + } + } + + // Kernel release. + if let Ok(s) = fs::read_to_string("/proc/sys/kernel/osrelease") { + info.kernel = s.trim().to_string(); + } else if let Ok(s) = fs::read_to_string("/proc/version") { + // /proc/version has the full "Linux version X.Y.Z (...)" string; + // strip the prefix up to "version ". + if let Some(idx) = s.find("version ") { + let rest = &s[idx + "version ".len()..]; + // Take first whitespace-delimited token. + let v = rest.split_whitespace().next().unwrap_or("").to_string(); + if !v.is_empty() { + info.kernel = v; + } + } + } else if let Ok(s) = fs::read_to_string("/scheme/sys/uname") { + for line in s.lines() { + if let Some(rest) = line.strip_prefix("release=") { + info.kernel = rest.trim().to_string(); + break; + } + } + } + + // Hostname (best effort; ignore errors). + if let Ok(h) = hostname() { + info.hostname = h; + } + + // Uptime: /proc/uptime first field is seconds (float). + if let Ok(s) = fs::read_to_string("/proc/uptime") { + if let Some(first) = s.split_whitespace().next() { + if let Ok(v) = first.parse::() { + info.uptime_secs = v as u64; + } + } + } + + info.available = + !info.kernel.is_empty() || !info.name.is_empty() || info.uptime_secs > 0; + info +} + +fn hostname() -> std::io::Result { + // Try /etc/hostname first; libc gethostname is also available but + // /etc/hostname works on both Redox and Linux without libc bindings. + if let Ok(s) = fs::read_to_string("/etc/hostname") { + let h = s.trim().to_string(); + if !h.is_empty() { + return Ok(h); + } + } + Err(std::io::Error::new(std::io::ErrorKind::NotFound, "hostname not found")) +} + +/// Format uptime as `Dd Hh Mm Ss`. +pub fn format_uptime(secs: u64) -> String { + let days = secs / 86400; + let hours = (secs % 86400) / 3600; + let mins = (secs % 3600) / 60; + let s = secs % 60; + if days > 0 { + format!("{days}d {hours}h {mins}m {s}s") + } else if hours > 0 { + format!("{hours}h {mins}m {s}s") + } else if mins > 0 { + format!("{mins}m {s}s") + } else { + format!("{s}s") + } +} \ No newline at end of file diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index d937420e20..ddb5446bc0 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -216,6 +216,51 @@ pub fn render_header<'a>(app: &'a App, focused: bool) -> Paragraph<'a> { .wrap(Wrap { trim: true }) } +/// Format a kibibyte value as human-readable ("1.5 GiB", "512 MiB", +/// "16 KiB"). Mirrors cpu-x's `PrefixUnit` helper but inline. +pub fn format_kib(kib: u64) -> String { + if kib >= 1024 * 1024 { + format!("{:.1} GiB", kib as f64 / (1024.0 * 1024.0)) + } else if kib >= 1024 { + format!("{:.1} MiB", kib as f64 / 1024.0) + } else { + format!("{} KiB", kib) + } +} + +/// Build a memory-bar line: "Label [bar] XX% value/total". +/// The bar uses Unicode block characters (`█` filled, `░` empty). +/// Bar width is fixed at 20 cells; the line stays under 80 columns +/// for typical terminal widths. +fn mem_bar_line<'a>( + label: &'a str, + percent: f64, + value_kib: u64, + total_kib: u64, + color: Style, +) -> Line<'a> { + let bar_width: usize = 20; + let filled = ((percent.clamp(0.0, 100.0) / 100.0) * bar_width as f64) as usize; + let mut bar = String::with_capacity(bar_width * 3); + for _ in 0..filled { + bar.push('\u{2588}'); // full block + } + for _ in filled..bar_width { + bar.push('\u{2591}'); // light shade + } + Line::from(vec![ + label.set_style(theme::LABEL), + format!("[{}] ", bar).set_style(color), + format!("{:5.1}% ", percent).set_style(theme::VALUE), + format!( + "{} / {}", + crate::render::format_kib(value_kib), + crate::render::format_kib(total_kib) + ) + .set_style(theme::VALUE_OFF), + ]) +} + /// Render the multi-view tab bar (Per-CPU / System / Info) with the /// active tab highlighted. Hotkeys `1`/`2`/`3` switch directly; `T` /// cycles through them in order. @@ -278,6 +323,56 @@ pub fn render_system_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> { if any_critical { "CRIT ".set_style(theme::VALUE_HOT) } else { "CRIT ".set_style(theme::VALUE_OFF) }, if any_pl { "PL ".set_style(theme::POWER_LIMIT_FLAG) } else { "PL ".set_style(theme::VALUE_OFF) }, ])); + + // OS identity (matches cpu-x System tab) + if app.os_info.available { + lines.push(Line::from(vec![ + "OS: ".set_style(theme::LABEL), + if app.os_info.name.is_empty() { + "(unknown)".set_style(theme::VALUE_OFF) + } else { + app.os_info.name.as_str().set_style(theme::VALUE) + }, + " Kernel: ".set_style(theme::LABEL), + if app.os_info.kernel.is_empty() { + "(unknown)".set_style(theme::VALUE_OFF) + } else { + app.os_info.kernel.as_str().set_style(theme::VALUE) + }, + " Host: ".set_style(theme::LABEL), + if app.os_info.hostname.is_empty() { + "(unknown)".set_style(theme::VALUE_OFF) + } else { + app.os_info.hostname.as_str().set_style(theme::VALUE) + }, + " Up: ".set_style(theme::LABEL), + crate::meminfo::format_uptime(app.os_info.uptime_secs) + .set_style(theme::VALUE_HOT), + ])); + } + + // Memory panel (matches cpu-x System tab memory bars). Each line + // shows label, value, and a horizontal bar. + if app.meminfo.available { + let mi = &app.meminfo; + lines.push(Line::from(vec![ + "Mem: ".set_style(theme::LABEL_BOLD), + format!( + "{} used / {} total", + crate::render::format_kib(mi.used_kib), + crate::render::format_kib(mi.total_kib) + ) + .set_style(theme::VALUE), + ])); + lines.push(mem_bar_line("Used: ", mi.percent_used(), mi.used_kib, mi.total_kib, theme::VALUE_HOT)); + lines.push(mem_bar_line("Buffers: ", mi.percent_buffers(), mi.buffers_kib, mi.total_kib, theme::VALUE)); + lines.push(mem_bar_line("Cached: ", mi.percent_cached(), mi.cached_kib, mi.total_kib, theme::VALUE)); + lines.push(mem_bar_line("Free: ", mi.percent_free(), mi.free_kib, mi.total_kib, theme::VALUE_OK)); + if mi.swap_total_kib > 0 { + lines.push(mem_bar_line("Swap: ", mi.percent_swap(), mi.swap_used_kib, mi.swap_total_kib, theme::STATUS_WARN)); + } + } + lines.push(Line::from(vec![ "Benchmark: ".set_style(theme::LABEL), if app.bench_line.is_empty() { "(idle)".set_style(theme::VALUE_OFF) } else { app.bench_line.as_str().set_style(theme::VALUE) }, @@ -695,5 +790,16 @@ pub fn buffer_to_string(buf: &ratatui::buffer::Buffer) -> String { pub fn render_once(app: &App) -> io::Result<()> { print!("{}", snapshot(app, 140, 50)); + // Also dump the System panel as a second snapshot for verification. + eprintln!("--- System panel (verifies v1.4 memory + OS info) ---"); + let sys_para = render_system_panel(app, false); + let backend = TestBackend::new(120, 30); + let mut terminal = Terminal::new(backend).expect("test terminal"); + terminal + .draw(|f| { + f.render_widget(sys_para, f.area()); + }) + .expect("draw"); + print!("{}", buffer_to_string(terminal.backend().buffer())); Ok(()) } \ No newline at end of file