diff --git a/CMakeLists.txt b/CMakeLists.txt index 3901e9d..027302f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,11 +52,10 @@ if(KMOD) pkg_check_modules(KMOD QUIET IMPORTED_TARGET libkmod>=30) endif() add_executable(qlinuxdevicemanager - src/main.cpp src/devices/device.h src/devices/device.cpp + src/main.cpp src/ui/reporttable.h src/ui/reporttable.cpp src/devices/enumerator.h src/devices/enumerator.cpp src/devices/monitorstate.h src/devices/monitorstate.cpp - src/devices/deviceevents.h src/devices/deviceevents.cpp src/devices/deviceproperties.h src/devices/deviceproperties.cpp src/devices/networkproperties.h src/devices/networkproperties.cpp src/devices/ethernetproperties.h src/devices/ethernetproperties.cpp @@ -71,15 +70,10 @@ add_executable(qlinuxdevicemanager src/devices/storageproperties.h src/devices/storageproperties.cpp src/devices/directoryscan.h src/devices/naturalsort.h src/devices/storagepresentation.h src/devices/storagepresentation.cpp - src/devices/deviceresources.h src/devices/deviceresources.cpp src/devices/resourceformat.h src/devices/resourceformat.cpp - src/devices/cpumetadata.h src/devices/cpumetadata.cpp - src/devices/functionnames.h src/devices/functionnames.cpp - src/devices/acpivendors.h src/devices/acpivendors.cpp - src/devices/devicelabel.h src/devices/devicelabel.cpp - src/devices/efivariables.h src/devices/efivariables.cpp - src/devices/modulenames.h src/devices/modulenames.cpp - src/devices/devicepresentation.h src/devices/devicepresentation.cpp + + + src/devices/devicesearch.h src/devices/devicesearch.cpp src/devices/propertytext.h src/devices/propertytext.cpp src/ui/devicefilter.h src/ui/devicefilter.cpp @@ -109,8 +103,6 @@ add_executable(qlinuxdevicemanager src/ui/devicepages/storagepage.h src/ui/devicepages/storagepage.cpp src/ui/devicepages/eventspage.h src/ui/devicepages/eventspage.cpp src/ui/devicepages/resourcespage.h src/ui/devicepages/resourcespage.cpp - src/devices/secureboot.h src/devices/secureboot.cpp - src/devices/systemproperties.h src/devices/systemproperties.cpp src/devices/cputopology.h src/ui/systempages/rampage.h src/ui/systempages/rampage.cpp src/ui/systempages/motherboardpage.h src/ui/systempages/motherboardpage.cpp src/ui/systempages/processorpage.h src/ui/systempages/processorpage.cpp @@ -211,6 +203,74 @@ target_compile_definitions(qlinuxdevicemanager PRIVATE QLDM_INSTALLED_BLUETOOTH_SERVICES="${CMAKE_INSTALL_FULL_DATADIR}/${APP_NAME}/bluetooth-services.ids") target_compile_definitions(qlinuxdevicemanager PRIVATE QLDM_INSTALLED_ACPI_IDS="${CMAKE_INSTALL_FULL_DATADIR}/${APP_NAME}/acpi-pnp.ids") +# Shared synchronous discovery and naming; neither frontend toolkit belongs here. +add_library(qldm-core STATIC + src/core/inventory.h src/core/inventory.cpp src/core/connectionparents.cpp + src/core/commonproperties.h src/core/commonproperties.cpp + src/core/systemreport.h src/core/systemreportplatform.cpp + src/core/systemreportprocessor.cpp src/core/systemreportmemory.cpp + src/devices/secureboot.h src/devices/secureboot.cpp + src/devices/systemproperties.h src/devices/systemproperties.cpp src/devices/cputopology.h + src/devices/device.h src/devices/device.cpp + src/devices/deviceevents.h src/devices/deviceevents.cpp + src/devices/cpumetadata.h src/devices/cpumetadata.cpp + src/devices/functionnames.h src/devices/functionnames.cpp + src/devices/acpivendors.h src/devices/acpivendors.cpp + src/devices/devicelabel.h src/devices/devicelabel.cpp + src/devices/efivariables.h src/devices/efivariables.cpp + src/devices/modulenames.h src/devices/modulenames.cpp + src/devices/devicepresentation.h src/devices/devicepresentation.cpp + src/devices/deviceresources.h src/devices/deviceresources.cpp +) +target_include_directories(qldm-core PUBLIC src/core src/devices) +target_link_libraries(qldm-core PUBLIC Qt6::Core PRIVATE Qt6::Network PkgConfig::UDEV) +target_compile_definitions(qldm-core PRIVATE + QLDM_INSTALLED_ACPI_IDS="${CMAKE_INSTALL_FULL_DATADIR}/${APP_NAME}/acpi-pnp.ids" + QT_ENABLE_STRICT_MODE_UP_TO=${APP_QT_STRICT_VERSION}) +if(KMOD AND TARGET PkgConfig::KMOD) + target_link_libraries(qldm-core PRIVATE PkgConfig::KMOD) + target_compile_definitions(qldm-core PRIVATE QLDM_HAVE_KMOD) +endif() +target_link_libraries(qlinuxdevicemanager PRIVATE qldm-core) + +option(TUI "Build the optional FINAL CUT terminal frontend" OFF) +# Some static FINAL CUT installations omit transitive dependencies from finalcut.pc. +# Keep these configurable: GPM and the terminal-capability provider vary by build. +set(TUI_FINALCUT_EXTRA_LIBRARIES "" CACHE STRING + "Additional FINAL CUT link dependencies (semicolon-separated, e.g. gpm;tinfo)") +if(TUI) + pkg_check_modules(FINALCUT QUIET IMPORTED_TARGET finalcut>=0.9.0) + if(TARGET PkgConfig::FINALCUT) + add_executable(qlinuxdevicemanager-tui src/tui/main.cpp + src/tui/browser.h src/tui/browser.cpp + src/tui/browsermenu.h src/tui/browsermenu.cpp + src/tui/modalcombobox.h src/tui/modalcombobox.cpp + src/tui/systemdialog.h src/tui/systemdialog.cpp + src/tui/systemsnapshot.h src/tui/systemsnapshot.cpp + src/tui/systempages/pages.h + src/tui/systempages/report.h src/tui/systempages/report.cpp + src/tui/systempages/operatingsystempage.cpp + src/tui/systempages/motherboardpage.cpp + src/tui/systempages/processorpage.cpp + src/tui/systempages/rampage.cpp + src/tui/treeprojection.h src/tui/treeprojection.cpp + src/tui/inventoryfilter.h src/tui/inventoryfilter.cpp + src/tui/terminaltext.h src/tui/terminaltext.cpp + src/tui/propertiesdialog.h src/tui/propertiesdialog.cpp + src/tui/propertypagecontrol.h + src/tui/propertiesstatus.h src/tui/propertiesstatus.cpp + src/tui/propertypages.h src/tui/propertypages.cpp + src/tui/devicetree.h src/tui/devicetree.cpp) + target_include_directories(qlinuxdevicemanager-tui PRIVATE src/tui) + target_link_libraries(qlinuxdevicemanager-tui PRIVATE + qldm-core PkgConfig::FINALCUT ${TUI_FINALCUT_EXTRA_LIBRARIES}) + target_compile_options(qlinuxdevicemanager-tui PRIVATE -Wall -Wextra -Wpedantic) + install(TARGETS qlinuxdevicemanager-tui RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + else() + message(WARNING "FINAL CUT >= 0.9.0 not found; skipping terminal frontend") + endif() +endif() + # Optional tools: basic viewing never requires Linguist. find_package(Qt6 ${APP_MINIMUM_QT} QUIET COMPONENTS LinguistTools) if(TARGET Qt6::lupdate) @@ -302,9 +362,11 @@ if(BUILD_TESTING) src/devices/functionnames.cpp src/devices/functionnames.h src/devices/acpivendors.cpp src/devices/acpivendors.h src/ui/devicemodel.cpp src/ui/devicemodel.h + src/core/connectionparents.cpp src/core/inventory.h + src/core/commonproperties.h src/core/commonproperties.cpp src/ui/icontheme.cpp src/ui/icontheme.h src/ui/devicefilter.cpp src/ui/devicefilter.h) - target_include_directories(device-search-test PRIVATE src/devices src/ui + target_include_directories(device-search-test PRIVATE src/core src/devices src/ui ${DISPLAY_X11_INCLUDE_DIRS} ${DISPLAY_WAYLAND_INCLUDE_DIRS}) target_link_libraries(device-search-test PRIVATE Qt6::Widgets PkgConfig::UDEV PkgConfig::V4L2) if(DISPLAY_INFO AND TARGET PkgConfig::DISPLAY_INFO) @@ -331,11 +393,14 @@ if(BUILD_TESTING) target_include_directories(system-properties-test PRIVATE src/devices) target_link_libraries(system-properties-test PRIVATE Qt6::Core Qt6::Network PkgConfig::UDEV) add_test(NAME system-properties COMMAND system-properties-test) + add_executable(system-report-test tests/system-report.cpp) + target_link_libraries(system-report-test PRIVATE qldm-core) + add_test(NAME system-report COMMAND system-report-test) endif() # Apply the declared Qt baseline's API checks to project executables and fixture targets. foreach(qldm_target IN ITEMS qlinuxdevicemanager monitor-state-test device-events-test - device-rules-test device-resources-test device-search-test system-properties-test + device-rules-test device-resources-test device-search-test system-properties-test system-report-test directory-scan-test display-identity-test gpu-link-sample-test pcie-link-text-test tree-export-test) if(TARGET ${qldm_target}) target_compile_definitions(${qldm_target} PRIVATE QT_ENABLE_STRICT_MODE_UP_TO=${APP_QT_STRICT_VERSION}) diff --git a/README.md b/README.md index f48d463..0c462f3 100644 --- a/README.md +++ b/README.md @@ -268,3 +268,10 @@ with the directory name used as the theme name. See the for HiDPI sizes, optional device-specific icons, fallbacks and installation. Packagers can omit the separately licensed set with `-DICONS8_ICONS=OFF`. No Icons8 artwork is included until authorized downloads are added. + +### Optional terminal frontend + +A snapshot-only FINAL CUT proof of concept can be enabled with `-DTUI=ON`. +It shares device discovery with the GUI and provides type/connection trees and +General/Driver/Details properties, on-demand filtering, All Devices visibility, +and System Information. See [TUI build and usage](doc/TUI.md). diff --git a/doc/TUI.md b/doc/TUI.md new file mode 100644 index 0000000..863c4ef --- /dev/null +++ b/doc/TUI.md @@ -0,0 +1,341 @@ +# FINAL CUT terminal proof of concept + +Install FINAL CUT >= 0.9.0 with its development headers and `finalcut.pc`. +Use your distribution's development package where available; otherwise follow +FINAL CUT's upstream installation instructions at https://github.com/gansm/finalcut. +Check discovery with `pkg-config --modversion finalcut`. For a custom prefix, +add its pkgconfig directory to `PKG_CONFIG_PATH`. + +The existing project development dependencies are still required to configure +this combined build. A desktop session is not required to run the TUI. + +```sh +cmake -S . -B build -DTUI=ON +cmake --build build --target qlinuxdevicemanager qlinuxdevicemanager-tui +./build/qlinuxdevicemanager-tui +``` + +## Static FINAL CUT builds + +A static `libfinal.a` is supported; its dependencies must also appear on the +executable's link line. Some FINAL CUT installations supply a `finalcut.pc` +containing only `-lfinal`, without the private dependencies needed for static +linking. For a build using GPM and a separate libtinfo, configure with: + +```sh +cmake -S . -B build -DTUI=ON -DTUI_FINALCUT_EXTRA_LIBRARIES="gpm;tinfo" +cmake --build build --target qlinuxdevicemanager-tui +``` + +The list is added after FINAL CUT to preserve static-library link ordering. +Install the development/linker files for these dependency libraries as needed. +If FINAL CUT was configured with another terminal-capability provider, supply +that library instead of `tinfo`. Omit `gpm` when FINAL CUT was built without GPM. +Library names or absolute library paths are accepted as a CMake list. +This option supplies dependencies; it does not force archive selection when +both shared and static FINAL CUT libraries are installed. With only +`libfinal.a` installed, the existing discovery selects that archive. +Embedding FINAL CUT removes its runtime library dependency, while Qt and any +GPM/terminal libraries linked dynamically remain runtime dependencies. + +TUI defaults to OFF. Missing FINAL CUT skips only the TUI target with a warning; +the GUI retains its existing installation conventions. + +F2 switches devices by type / connection. Arrow keys navigate; Enter toggles +branches and opens properties for leaf devices. Right/Left still expand/collapse. +Native mouse activation is unchanged, including properties for device branches. +General and Driver use the GUI's shared common-property formatter, including +manufacturer, hardware identifiers, binding and module ownership when captured. +Details retains raw inventory properties and attributes separately. +F7 / F8 select the previous / next General, Driver or Details page, wrapping +at either end. Tab / Shift+Tab retain normal control focus traversal, including +Close. Page changes focus the content so arrows and Page Up/Down scroll +immediately. Alt+G / Alt+D / Alt+T still select +pages directly; the title identifies the current page. Close, Alt+C or Escape +returns. Text views scroll vertically +and horizontally, so long values remain accessible. F5 takes a new synchronous +snapshot; F10 quits. Category nodes start collapsed. Start at 80x25 or larger. +Resizing adjusts tree and property view geometry. + +This is a read-only snapshot utility. It creates no monitor, daemon or hotplug +subscription. Startup and manual refresh enumerate synchronously; refresh can +briefly block navigation. Properties describe that snapshot, including devices +that may since have disappeared. Unfiltered selection and expansion are +remembered separately for each view, using device paths and group identities. +Removed selections fall back to the computer row. +No absent binding/module/status value is treated as proof of good health or +built-in driver status. Raw property/attribute source keys are retained. + +## Source boundaries + +- `src/core/`: synchronous inventory entry point and shared connection projection. +- `src/devices/`: existing toolkit-independent records, naming, classification + and collectors. The subset needed by enumeration is in `qldm-core`, linked + against QtCore, libudev and optionally libkmod. No frontend widgets belong here. +- `src/ui/` and `src/main.cpp`: existing Qt Widgets frontend and GUI monitoring. +- `src/tui/`: native FINAL CUT tree and dialogs. No Qt Widgets linkage. + +A QCoreApplication instance provides Qt utility/translation context, but its +event loop is never run. FINAL CUT owns terminal setup and event dispatch. +Both executables use the same discovery and classification implementation. +Connection edges use the GUI's existing directory-ancestry bridging, hidden +ancestor retention and separate UEFI firmware-record presentation group. +By default the TUI omits hidden records except ancestors needed for connection +context. F9 opens the native View popup, with Devices by type, Devices by +connection, and the checked All Devices toggle. The toggle exposes the existing +inventory's virtual/internal records using the same hidden flag as the GUI; +it performs no enumeration or additional property reads. All Devices starts off +and remains in effect across view changes, filtering and manual refresh for the +current run. Active filtering also applies to the newly visible records. + +The menu shows the selected view and visibility setting. The window title adds +`(All Devices)` while enabled so the mode remains visible with the popup closed. +Unfiltered selection and expansion survive rebuilds where their rows remain; +visibility changes also preserve a selected filtered result if it survives. +The menu uses arrows, Enter, mouse activation and native Escape dismissal. +It consumes no permanent row. The existing F2 shortcut and bottom buttons remain. + +Maintainer checks: open F9 and use both view entries and All Devices. Verify +radio/check marks stay synchronized after F2 or clicking By type/connection, +and the title indicator follows the toggle. Compare default/all visibility with +the GUI in both views, including connection ancestors and UEFI records. Search +for a normally hidden record, toggle All Devices and confirm results update. +Check surviving selection/expansion, refresh, Escape from the popup with a filter +active, repeated open/close, mouse activation, and resizing at 80x25 and 211x60. +Open Properties and confirm its contextual bar hides F9 and restores it on close. + +Hardware-specific collectors can later be shared individually where independent +of widgets. Their presentation needs separate FINAL CUT pages. System Information +will similarly need its own text presentation; neither requires replacing the +inventory backend. GUI-only asynchronous/session collectors need a separate +review before reuse without Qt event dispatch. + +## Maintainer validation checklist + +Compilation and runtime validation are deliberately left to the maintainer. +Build both targets; also configure with TUI off and with FINAL CUT absent. +Compare visible categories and device parents with the GUI on the same host. +Open two distinctly named devices and verify their paths/bindings differ as +expected. Inspect unbound and permission-restricted records. Exercise expand, +collapse, Enter, mouse activation, both property pages and scrolling of long +values. Resize at 80x25 and larger. Refresh after a hotplug change and confirm +no automatic change occurs before refresh. Close properties, then quit normally. + +## Filtering devices + +The Filter field is hidden initially, giving its space to the tree. F4 or Ctrl+F +reveals and focuses it. Typing a case-insensitive substring immediately filters +the current projection. Matching devices and +their ancestors remain; all retained branches expand so results are visible. +The status bar reports the number of matching devices, excluding context-only +ancestors. With no matches, only the computer row remains. + +Matching uses names, identifiers, driver/module names, paths, category/subsystem, +udev property values and available captured attribute values. It performs no +additional hardware reads. Hidden records omitted from the normal view remain +excluded; retained connection-context ancestors are eligible for matching. +This does not include the GUI's hardware-specific deep-property collection. + +Enter in the field selects a match and focuses the tree; Enter on a device opens +properties, keeping the filter visible and active. F3/F6 cycle matches +forward/backward with wraparound. Escape clears and hides the field, restores +the full tree's height and focuses it. Backspacing to an empty query also restores +the full projection and its pre-filter selection and expansion, but keeps the +field open for further input until Escape. Whitespace alone +means no filter. The query remains active across F2 and F5. Each view retains +its own unfiltered state; paths absent after a refresh are skipped on restore. +Header sorting resets when the tree is rebuilt. + +The browser uses one outer frame, carrying the Device/Enter caption; the native +tree's separate border and headline are suppressed. With the filter hidden, +device rows begin immediately below that frame. The tree's scrollbar ends +immediately above the buttons, reclaiming the former blank rows at both ends. +Native tree item drawing, scrolling and activation remain unchanged. + +Maintainer checks: at 80x25 and 211x60, verify a single frame, its caption, the +first device row directly below it, and no blank row before the buttons. +Open/close the filter, switch views and refresh; scroll in both directions and +resize. Confirm the caption stays on the outer frame, buttons and their shadows +remain intact, and Properties restores the browser without stale borders. + +The TUI source is separated by responsibility: + +- `main.cpp`: QtCore context, initial enumeration and FINAL CUT startup. +- `browser.cpp`: controls, callbacks and match navigation. +- `browsermenu.cpp`: native on-demand view/visibility menu. +- `treeprojection.cpp`: type/connection construction, ancestor retention and state. +- `inventoryfilter.cpp`: snapshot-only matching. +- `devicetree.cpp`: FINAL CUT 0.9.1-compatible selection and scrolling. +- `propertiesdialog.cpp`: modal General/Driver presentation. +- `terminaltext.cpp`: conversion and control-character sanitization. + +Maintainer checks: type and erase queries while focus stays in the field; verify +unrelated branches disappear and matching ancestors remain. Check no-match and +whitespace queries, F3/F6 wraparound, Enter-to-properties and Escape. Before +filtering, select a nested device and expand several branches; clear the filter +and verify that state returns. Repeat in both views, after sorting, after a +refresh that removes the selected device, and at 80x25. Confirm the Qt GUI builds +and its search behavior is unchanged. + +For filter visibility, check startup and Tab traversal with the field hidden; +open with both F4 and Ctrl+F, then type, erase, and press Enter. The field must +stay visible while results are active. Escape should clear/hide both field and +label, restore prior tree state and reclaim the space. Repeat show/hide after +F2, F5 and terminal resizing at 80x25 and larger. Escape inside Properties should +close only that dialog, leaving the active tree filter intact. + +## Common properties + +The TUI adapts the captured inventory into `CommonProperties`, a small shared +record with values, sources and read state. Both frontends use +`commonPropertyEntries()` for General/Driver labels, manufacturer precedence, +hardware identifiers, EFI applicability, status wording and module interpretation. +The GUI's common-field formatter keeps its field order and interpretation. +Shared captions are shortened for both frontends (for example, Kernel driver, +Kernel module, Module type and Module file). Status and metadata notes are also +shortened while retaining their qualifications. GUI on-demand collection +continues to supply its data. + +The TUI does not run those additional collectors. Missing module files, versions +or metadata remain unavailable; an absent module link never proves a built-in +driver. A description used only as a naming candidate is not promoted to driver +metadata. Device status identifies snapshot scope. Raw values remain in +Details, with the shared read-error/permission wording. `propertypages.cpp` owns +the TUI snapshot adaptation and text rendering; `propertiesdialog.cpp` owns only +page selection and layout. Long values remain accessible by scrolling. + +Maintainer checks: compare General/Driver field names with the GUI, opening a +USB device, PCI device, unbound record and UEFI variable. Check manufacturer and +identifier values against Details and confirm absent fields are not reported +as healthy or built-in. Switch all three pages using buttons and mnemonics, +scroll long identifiers and raw attributes, resize, and close with Escape. +Rebuild the GUI and compare its General/Driver output before/after extraction. + +## Compact properties layout + +Properties is centered and limited to 100 columns and 28 rows. Short pages use +less height; terminal resizing reduces the dialog to the available space. +Fields use aligned label/value columns with no blank rows between fields. +Padding uses terminal column widths, so values and multiline continuations +start at the same column. Every other logical field receives native FINAL CUT +line highlighting with a contrasting light background; continuation lines keep +the same band. Monochrome terminals retain the aligned table without color. +Details keeps a single separating line before each raw-data section. Horizontal and +vertical scrolling preserve access to long values and large snapshots. + +Maintainer checks: at 211x60, verify the dialog is centered and no larger than +100x28. At 80x25, verify all page buttons and Close fit. Switch General, Driver +and Details, check aligned columns and alternating field backgrounds, including +multiline identifiers. Scroll long values and resize while each page is open. +Check a monochrome terminal if available; alignment must remain readable. + +For Enter navigation, check the computer/category rows and multi-level connection +branches: repeated Enter must expand/collapse without opening Properties. Enter +on a leaf device must still open Properties. Repeat after filtering and while +horizontally scrolled, and confirm Left/Right and mouse activation still work. + +For Properties page navigation, use F7/F8 to cycle backward/forward through all +pages, including wraparound. Repeat after clicking a page or Close button, and +confirm scroll keys act on the content immediately after a page change. Check +Alt+G/D/T, Escape and mouse Close. Tab must reach Close; Shift+Tab must traverse +controls in reverse. Main-window Tab behavior must remain unchanged. + +While Properties is open, the bottom bar shows F7 Previous and F8 Next, matching +the keyboard bindings. Clicking either hint also switches pages. The browser's +status keys and message return when the dialog closes; browser accelerators are +removed from the bar while Properties is active. `propertiesstatus.cpp` owns this +scoped change, reusing the existing bar rather than replacing its widget. + +Maintainer checks: open Properties, use F7/F8 and click both hints, then close +using Escape and the Close button. Confirm the main F2/F4/F3/F6/F5/F10 hints +return and their actions execute once. Repeat opening and closing several times, +and resize with Properties open to check the bottom bar placement. + +Properties omits the repeated inventory-snapshot banner and the Captured +attributes heading. Raw attribute rows remain present, aligned and shaded with +the rest of Details. Snapshot behavior is documented here and in Device status; +there is no startup confirmation. Future dialogs should keep routine scope +explanations in documentation and reserve visible notes for necessary context. + +Maintainer checks: open all three Properties pages and confirm the banner is +absent. On Details, confirm attribute rows remain, the redundant heading is +absent and alternating bands still follow the correct logical fields. + + +## System Information + +Open System Information with Alt+I or the F9 menu. Its Operating System, +Motherboard, Processor and RAM pages follow the GUI's order and use the same +field captions, units, interpretation and read-state formatting. F7/F8 cycle +pages; the four page buttons support mouse activation. Tab/Shift+Tab retain +normal focus traversal, including the RAM chip selector and Close. Escape +closes the dialog. The contextual bottom bar shows Previous/Next while open. + +Tables are plain aligned text with headers and alternating row shading. RAM +shows one firmware record at a time, selected by the dropdown above its table; +CPU core groups and cache hierarchy are separate tables. There are no nested +boxes, Copy/Copy All controls or dialog Refresh controls. The dialog is centered, +bounded to 110x32 and shrinks for shorter pages and terminals. Long content +remains available through native scrolling. Firmware memory records do not +establish physical slots, channels, upgradeability or active ECC; the existing +collector's evidence and read errors are preserved. + +The instruction-set/CPU-feature field displays a short hint to run +`cat /proc/cpuinfo` instead of the full list. The GUI's instruction-set Copy +action remains available. The hint is plain text; the TUI executes no command. + +The first opening collects system information in one QThread worker. Native +FINAL CUT timers check completion without a Qt event loop; polling stops once +the result is consumed. Reopening uses the cached result. The application's +existing F5/Refresh action also invalidates that cache and starts fresh system +collection after a successful inventory refresh. A newer request supersedes +an older collection; only one system worker runs at a time. Closing the dialog +does not cancel or lose the cache, and application shutdown interrupts and joins +the worker. No worker accesses widgets or retains a dialog pointer. + +`systemdialog.cpp` owns page selection and layout. Each TUI page has its own +source under `src/tui/systempages/`; `report.cpp` only aligns and shades text. +`systemsnapshot.cpp` owns collection/cache lifetime. Shared presentation lives in +`src/core/systemreport{platform,processor,memory}.cpp`, used by both frontends; +the Qt page widgets retain their layouts, tooltips, copying and refresh behavior. +The shared collector uses QtNetwork for existing MOK certificate parsing, so the +TUI now also links QtNetwork through the core. It still uses no Qt Widgets or +Qt event loop at runtime. + +Maintainer checks: build both frontends and run `ctest --test-dir build -R +'system-(properties|report)' --output-on-failure`. Open via both entry points, +cycle all pages, check Tab reaches Close, and switch RAM records with keyboard +and mouse. Compare values and headers with the GUI, including unavailable or +permission-denied firmware data. Confirm the CPU feature field shows the command +hint in the TUI and the full list remains available in the GUI. Inspect SMT cache sharing, +long identifiers and certificate dates. Repeat at 80x25 and 211x60, resize and +cycle short/long pages to check for stale borders. Verify stripes follow wrapped +rows and scrolling. Close during collection, reopen, refresh from the browser +and reopen again, then quit while a collection is active. Confirm no dialog +Copy or Refresh controls appear and browser status keys return on close. + +### RAM selector compatibility + +The RAM summary remains above the selector; the selector replaces the static +Chip heading immediately above the Property/Value table. The table and summary +use separate borderless text views so scrolling the chip fields does not move +its selector. Selection changes update only the table while the popup is open; +accepting a selection can then refit the dialog. Page switching closes the popup +before changing the layout. + +`modalcombobox.cpp` contains the FINAL CUT 0.9.1 compatibility adjustment. That +release's dropdown is a non-modal window, so FApplication's modal-event filter +rejects its input and focus events. Using the public child/flags APIs, the +component gives only its owned FDropDownListBox the menu-window exemption also +used by native menus. It does not modify dialog modality or the modal counter. +Native popup drawing, item selection, mouse handling and Escape behavior remain +in FINAL CUT. Tab/Shift+Tab explicitly dismiss the popup before normal traversal. + +Maintainer checks: open System Information, choose RAM and open the selector +with its arrow or F4. Navigate with Up/Down and accept with Enter; click another +entry with the mouse. Check that fields follow the selected chip and the popup +stays interactive during navigation. Escape must dismiss it without closing the +dialog; Tab/Shift+Tab must dismiss it and traverse controls. Check outside-click +dismissal, scrolling with more than eight records if available, F7/F8 page +switching, and resizing at 80x25 and 211x60. Confirm the selector replaces the +static Chip heading, the summary stays visible, and the GUI remains unchanged. diff --git a/src/core/commonproperties.cpp b/src/core/commonproperties.cpp new file mode 100644 index 0000000..0319a98 --- /dev/null +++ b/src/core/commonproperties.cpp @@ -0,0 +1,156 @@ +#include "commonproperties.h" +#include "devicelabel.h" +#include +#include +#include + +QString propertyReadValue(const Attribute &a) +{ + switch (a.state) { + case ReadState::Available: + return a.value.isEmpty() ? QCoreApplication::translate("PropertiesDialog", "Empty value") : a.value; + case ReadState::Unavailable: return QCoreApplication::translate("PropertiesDialog", "Unavailable"); + case ReadState::PermissionDenied: return QCoreApplication::translate("PropertiesDialog", "Permission denied"); + case ReadState::Removed: return QCoreApplication::translate("PropertiesDialog", "Device removed"); + case ReadState::Unsupported: return QCoreApplication::translate("PropertiesDialog", "Unsupported"); + case ReadState::NotApplicable: return QCoreApplication::translate("PropertiesDialog", "Not applicable"); + case ReadState::Error: + switch (a.error) { + case ETIMEDOUT: return QCoreApplication::translate("PropertiesDialog", "Query timed out (errno %1)").arg(a.error); + case EOVERFLOW: return QCoreApplication::translate("PropertiesDialog", "Data exceeds supported limits (errno %1)").arg(a.error); + case EAGAIN: return QCoreApplication::translate("PropertiesDialog", "Temporarily unavailable; retry the query (errno %1)").arg(a.error); + case EINVAL: + case EPROTO: return QCoreApplication::translate("PropertiesDialog", "Invalid data or query (errno %1)").arg(a.error); + case ECANCELED: return QCoreApplication::translate("PropertiesDialog", "Query cancelled (errno %1)").arg(a.error); + default: return QCoreApplication::translate("PropertiesDialog", "Read error (errno %1)").arg(a.error); + } + } + return {}; +} +QVector commonPropertyEntries(const CommonProperties &snapshot, bool removed) +{ + QVector entries; + const Device &d = snapshot.device; + const bool efi = d.subsystem == "efivarfs"; + const Attribute notApplicable {ReadState::NotApplicable, {}, 0}; + const auto add = [&](const QString &id, const QString &label, const QString &value, + const QString &source = QString(), bool advanced = false, int tab = 2, + ReadState state = ReadState::Available, int error = 0) { + entries.append({id, label, value, source, advanced, tab, state, error}); + }; + const auto raw = [&](const QString &id) { return snapshot.values.value(id); }; + const auto addRaw = [&](const QString &id, const QString &label, int tab, const Attribute &fallback = Attribute()) { + const Attribute a = snapshot.values.value(id, fallback); + add(id, label, propertyReadValue(a), snapshot.sources.value(id), false, tab, a.state, a.error); + }; + add("name", QCoreApplication::translate("PropertiesDialog", "Device name"), deviceDisplayName(d), d.nameSource, false, 0); + add("category", QCoreApplication::translate("PropertiesDialog", "Category"), categoryLabel(d.category), QCoreApplication::translate("PropertiesDialog", "Application grouping"), false, 0); + Attribute manufacturer; + QString manufacturerSource; + if (efi) manufacturer = notApplicable; + else { + for (const QString &id : {QStringLiteral("sysfs/manufacturer"), QStringLiteral("sysfs/vendor_name"), + QStringLiteral("acpi/vendor"), + QStringLiteral("udev/ID_VENDOR_FROM_DATABASE"), QStringLiteral("udev/ID_VENDOR")}) { + const Attribute candidate = raw(id); + if (candidate.state == ReadState::Available && !candidate.value.isEmpty()) { + manufacturer = candidate; manufacturerSource = snapshot.sources.value(id); break; + } + if (candidate.state == ReadState::PermissionDenied || candidate.state == ReadState::Error) { + manufacturer = candidate; manufacturerSource = snapshot.sources.value(id); break; + } + } + if (manufacturer.state == ReadState::Unavailable) { + for (const QString &key : {QStringLiteral("ID_VENDOR_FROM_DATABASE"), QStringLiteral("ID_VENDOR")}) { + if (d.properties.value(key).isEmpty()) continue; + manufacturer = {ReadState::Available, d.properties.value(key), 0}; + manufacturerSource = d.propertySources.value(key, QStringLiteral("udev: %1 (%2)").arg(key, d.path)); + break; + } + } + } + add("manufacturer", QCoreApplication::translate("PropertiesDialog", "Manufacturer"), propertyReadValue(manufacturer), manufacturerSource, false, 0, manufacturer.state, manufacturer.error); + Attribute bus = raw("udev/ID_BUS"); + if (bus.state != ReadState::Available) bus = raw("driver/bus"); + if (efi) bus = notApplicable; + add("bus", QCoreApplication::translate("PropertiesDialog", "Bus / subsystem"), efi ? propertyReadValue(bus) + : bus.state == ReadState::Available ? bus.value + " / " + d.subsystem : d.subsystem, + QCoreApplication::translate("PropertiesDialog", "Device udev metadata and direct driver bus"), false, 0, efi ? ReadState::NotApplicable : ReadState::Available); + add("location", QCoreApplication::translate("PropertiesDialog", "Location"), efi ? d.parentPath : d.path, QCoreApplication::translate("PropertiesDialog", "Kernel path"), false, 0); + QString status; + if (removed) status = QCoreApplication::translate("PropertiesDialog", "Removed or replaced; showing a snapshot."); + else if (snapshot.state != ReadState::Available) status = QCoreApplication::translate("PropertiesDialog", "Inventory snapshot; properties unverified."); + else { + status = QCoreApplication::translate("PropertiesDialog", "Present at last property read."); + if (!efi) { + const Attribute binding = raw("driver/name"); + status += '\n' + (binding.state == ReadState::Available + ? binding.value.isEmpty() ? QCoreApplication::translate("PropertiesDialog", "No driver bound; not necessarily a fault.") : QCoreApplication::translate("PropertiesDialog", "Driver bound: %1").arg(binding.value) + : QCoreApplication::translate("PropertiesDialog", "Driver binding: %1").arg(propertyReadValue(binding))); + const Attribute authorization = raw("sysfs/authorized"); + if (authorization.state == ReadState::Available && authorization.value == "0") status += '\n' + QCoreApplication::translate("PropertiesDialog", "USB device/interface is unauthorized."); + else if (authorization.state == ReadState::Available && authorization.value == "1") status += '\n' + QCoreApplication::translate("PropertiesDialog", "USB device/interface is authorized."); + else if (authorization.state == ReadState::PermissionDenied || authorization.state == ReadState::Error) + status += '\n' + QCoreApplication::translate("PropertiesDialog", "USB authorization: %1").arg(propertyReadValue(authorization)); + } + } + add("status", QCoreApplication::translate("PropertiesDialog", "Device status"), status, QCoreApplication::translate("PropertiesDialog", "Observed presence, binding and USB authorization only"), false, 0); + Attribute binding = snapshot.values.value("driver/name", d.driver.isEmpty() ? Attribute() + : Attribute{ReadState::Available, d.driver, 0}); + if (efi) binding = notApplicable; + const QString driverName = !efi && binding.state == ReadState::Available && binding.value.isEmpty() + ? QCoreApplication::translate("PropertiesDialog", "No driver bound") : propertyReadValue(binding); + add("driver/name", QCoreApplication::translate("PropertiesDialog", "Kernel driver"), driverName, efi ? QString() : d.path + "/driver", false, 0, binding.state, binding.error); + QStringList identifiers; + const Attribute alias = snapshot.values.value("udev/MODALIAS", + d.properties.contains("MODALIAS") + ? Attribute{ReadState::Available, d.properties.value("MODALIAS"), 0} : Attribute()); + if (alias.state == ReadState::Available && !alias.value.isEmpty()) { + // Use the collected alias, never a device-name prefix or a related device's ID. + if ((d.subsystem == "acpi" || d.subsystem == "platform" || d.subsystem == "i2c") + && alias.value.startsWith("acpi:")) { + const QStringList ids = alias.value.mid(5).split(':', Qt::SkipEmptyParts); + if (!ids.isEmpty()) identifiers.append( + QCoreApplication::translate("PropertiesDialog", "ACPI identifiers: %1").arg(ids.join(", "))); + } + identifiers.append(QCoreApplication::translate("PropertiesDialog", "Modalias: %1").arg(alias.value)); + } + for (const QString &key : {QStringLiteral("PCI_ID"), QStringLiteral("PCI_SUBSYS_ID"), QStringLiteral("PRODUCT"), + QStringLiteral("ID_VENDOR_ID"), QStringLiteral("ID_MODEL_ID"), QStringLiteral("ID_SERIAL_SHORT")}) { + const Attribute a = snapshot.values.value("udev/" + key, + d.properties.contains(key) ? Attribute{ReadState::Available, d.properties.value(key), 0} : Attribute()); + if (a.state == ReadState::Available && !a.value.isEmpty()) identifiers.append(key + ": " + a.value); + } + for (const QString &key : {QStringLiteral("vendor"), QStringLiteral("device"), QStringLiteral("idVendor"), + QStringLiteral("idProduct"), QStringLiteral("serial"), QStringLiteral("hid"), QStringLiteral("uid")}) { + const Attribute a = raw("sysfs/" + key); + if (a.state != ReadState::Unavailable && !(a.state == ReadState::Available && a.value.isEmpty())) + identifiers.append(key + ": " + propertyReadValue(a)); + } + add("identifiers", QCoreApplication::translate("PropertiesDialog", "Hardware identifiers"), efi ? propertyReadValue(notApplicable) + : identifiers.isEmpty() ? QCoreApplication::translate("PropertiesDialog", "Unavailable") : identifiers.join('\n'), QCoreApplication::translate("PropertiesDialog", "Selected udev and direct sysfs identifiers"), false, 0, + efi ? ReadState::NotApplicable : identifiers.isEmpty() ? ReadState::Unavailable : ReadState::Available); + add("driver/binding", QCoreApplication::translate("PropertiesDialog", "Kernel driver"), driverName, efi ? QString() : d.path + "/driver", false, 1, binding.state, binding.error); + const Attribute moduleFallback = efi || (binding.state == ReadState::Available && binding.value.isEmpty()) + ? notApplicable : Attribute(); + addRaw("driver/bus", QCoreApplication::translate("PropertiesDialog", "Driver bus"), 1, moduleFallback); + addRaw("module/name", QCoreApplication::translate("PropertiesDialog", "Kernel module"), 1, moduleFallback); + const Attribute type = snapshot.values.value("module/type", moduleFallback); + add("module/type", QCoreApplication::translate("PropertiesDialog", "Module type"), type.state == ReadState::Available ? QCoreApplication::translate("PropertiesDialog", "Modular (kernel initstate)") + : type.state == ReadState::NotApplicable || type.state == ReadState::PermissionDenied || type.state == ReadState::Error + ? propertyReadValue(type) : QCoreApplication::translate("PropertiesDialog", "Undetermined; a missing module link/version does not prove built-in."), + snapshot.sources.value("module/type"), false, 1, + type.state == ReadState::NotApplicable || type.state == ReadState::PermissionDenied || type.state == ReadState::Error + ? type.state : ReadState::Available, type.error); + addRaw("module/runtime_version", QCoreApplication::translate("PropertiesDialog", "Running version"), 1, moduleFallback); + const QStringList fields {"filename", "version", "description", "author", "license", "firmware"}; + const QStringList labels {QCoreApplication::translate("PropertiesDialog", "Module file"), QCoreApplication::translate("PropertiesDialog", "Installed version"), QCoreApplication::translate("PropertiesDialog", "Description"), + QCoreApplication::translate("PropertiesDialog", "Author"), QCoreApplication::translate("PropertiesDialog", "License"), QCoreApplication::translate("PropertiesDialog", "Firmware names")}; + for (int i = 0; i < fields.size(); ++i) addRaw("module/" + fields[i], labels[i], 1, moduleFallback); + addRaw("sysfs/firmware_rev", QCoreApplication::translate("PropertiesDialog", "Device firmware revision"), 1, + d.subsystem == "nvme" ? Attribute() : notApplicable); + add("module/note", QCoreApplication::translate("PropertiesDialog", "Metadata scope"), efi ? propertyReadValue(notApplicable) + : QCoreApplication::translate("PropertiesDialog", "Installed metadata may differ from loaded code. Firmware names do not confirm loading or version."), {}, false, 1, + efi ? ReadState::NotApplicable : ReadState::Available); + return entries; +} diff --git a/src/core/commonproperties.h b/src/core/commonproperties.h new file mode 100644 index 0000000..7927182 --- /dev/null +++ b/src/core/commonproperties.h @@ -0,0 +1,24 @@ +#pragma once +#include "device.h" + +// Pure text presentation shared by Properties and deep search; no widgets. +struct PropertyEntry { + QString id; + QString label; + QString value; + QString source; + bool advanced = false; + int tab = 2; + ReadState state = ReadState::Available; + int error = 0; +}; + +// Only common captured fields: independent of hardware-specific collectors. +struct CommonProperties { + Device device; + QHash values; + QHash sources; + ReadState state = ReadState::Unavailable; +}; +QString propertyReadValue(const Attribute &attribute); +QVector commonPropertyEntries(const CommonProperties &snapshot, bool removed = false); diff --git a/src/core/connectionparents.cpp b/src/core/connectionparents.cpp new file mode 100644 index 0000000..65c8992 --- /dev/null +++ b/src/core/connectionparents.cpp @@ -0,0 +1,19 @@ +#include "inventory.h" + +QHash connectionParents(const QVector &devices) +{ + QSet paths; + for (const Device &d : devices) paths.insert(d.path); + QHash parents; + for (const Device &d : devices) { + QString path = d.parentPath; + if (!paths.contains(path) && path.startsWith("/sys/devices/") + && d.path.startsWith(path + '/')) { + while (path.startsWith("/sys/devices/") && !paths.contains(path)) + path.truncate(path.lastIndexOf('/')); + if (!paths.contains(path)) path.clear(); + } + parents.insert(d.path, path); + } + return parents; +} diff --git a/src/core/inventory.cpp b/src/core/inventory.cpp new file mode 100644 index 0000000..d0f47bf --- /dev/null +++ b/src/core/inventory.cpp @@ -0,0 +1,192 @@ +#include "inventory.h" +#include +#include "cpumetadata.h" +#include "devicepresentation.h" +#include "modulenames.h" +#include "efivariables.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "deviceresources.h" + +namespace { +QString text(const char *value) +{ + // All values are treated as plain text. Bound the owned copy. + return value ? QString::fromUtf8(value, static_cast(strnlen(value, 4096))) : QString(); +} +Attribute readAttribute(const QString &path) +{ + Attribute result; + const QByteArray encoded = path.toUtf8(); + const int fd = open(encoded.constData(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if (fd < 0) { + result.error = errno; + result.state = errno == EACCES || errno == EPERM ? ReadState::PermissionDenied + : errno == ENOENT ? ReadState::Unavailable : ReadState::Error; + return result; + } + char buffer[4097]; + ssize_t count; + do { count = read(fd, buffer, sizeof(buffer)); } while (count < 0 && errno == EINTR); + const int readError = errno; + close(fd); + if (count < 0) { + result.error = readError; + result.state = readError == ENODEV ? ReadState::Removed + : readError == EACCES || readError == EPERM ? ReadState::PermissionDenied : ReadState::Error; + } else if (count > 4096) { + result.error = EOVERFLOW; + result.state = ReadState::Error; + } else { + result.state = ReadState::Available; + result.value = QString::fromUtf8(buffer, static_cast(count)).trimmed(); + } + return result; +} +} +Inventory collectInventory(const std::function &cancelled) +{ + Inventory result; + std::unique_ptr context(udev_new(), &udev_unref); + if (!context) { + result.error = QCoreApplication::translate("Enumerator", "Could not create the udev context."); + return result; + } + std::unique_ptr scan( + udev_enumerate_new(context.get()), &udev_enumerate_unref); + if (!scan || udev_enumerate_scan_devices(scan.get()) < 0) { + result.error = QCoreApplication::translate("Enumerator", "Could not enumerate kernel devices."); + return result; + } + // hwdb is an optional local naming database, still accessed through libudev. + std::unique_ptr hwdb(udev_hwdb_new(context.get()), &udev_hwdb_unref); + ModuleNames moduleNames; // Worker-owned context and per-refresh lookup cache. + Attribute cpuInfo; + QHash cpuModels; + bool cpuInfoRead = false; + udev_list_entry *entry = nullptr; + udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(scan.get())) { + if ((cancelled && cancelled())) break; + std::unique_ptr raw( + udev_device_new_from_syspath(context.get(), udev_list_entry_get_name(entry)), &udev_device_unref); + if (!raw) { ++result.skipped; continue; } + Device d; + d.path = text(udev_device_get_syspath(raw.get())); + if (!d.path.startsWith("/sys/devices/")) { ++result.skipped; continue; } + d.sysname = text(udev_device_get_sysname(raw.get())); + d.subsystem = text(udev_device_get_subsystem(raw.get())); + d.devtype = text(udev_device_get_devtype(raw.get())); + d.driver = text(udev_device_get_driver(raw.get())); + if (!d.driver.isEmpty()) { + const QString target = QFileInfo(d.path + "/driver/module").symLinkTarget(); + if (target.startsWith("/sys/module/")) d.driverModule = QFileInfo(target).fileName(); + } + if (auto *parent = udev_device_get_parent(raw.get())) + d.parentPath = text(udev_device_get_syspath(parent)); + // Explicit allowlist: no recursive sysfs reads, binary resources or hardware queries. + for (const char *key : {"ID_MODEL_FROM_DATABASE", "ID_MODEL", "NAME", "ID_V4L_PRODUCT", "MODALIAS", "PCI_CLASS", + "PCI_ID", "ID_VENDOR_FROM_DATABASE", "ID_VENDOR", "ID_BUS", + "ID_INPUT_KEYBOARD", "ID_INPUT_MOUSE", "ID_INPUT_TOUCHPAD", + "ID_INPUT_POINTINGSTICK", "ID_INPUT_SWITCH", "PRODUCT", "PHYS", "DEVNUM", + "USEC_INITIALIZED", "ID_DRIVE_THUMB"}) { + const char *value = udev_device_get_property_value(raw.get(), key); + if (value) d.properties.insert(QLatin1String(key), text(value)); + } + if (hwdb && (d.subsystem == "pci" || d.subsystem == "usb") + && (d.properties.value("ID_MODEL_FROM_DATABASE").isEmpty() + || (d.subsystem == "usb" && d.properties.value("ID_VENDOR_FROM_DATABASE").isEmpty()))) { + const QByteArray alias = d.properties.value("MODALIAS").toUtf8(); + if (!alias.isEmpty()) { + udev_list_entry *property = nullptr; + udev_list_entry_foreach(property, udev_hwdb_get_properties_list_entry(hwdb.get(), alias.constData(), 0)) { + const QString key = text(udev_list_entry_get_name(property)); + if ((key == "ID_MODEL_FROM_DATABASE" || key == "ID_VENDOR_FROM_DATABASE") + && d.properties.value(key).isEmpty()) { + d.properties.insert(key, text(udev_list_entry_get_value(property))); + d.propertySources.insert(key, QStringLiteral("hwdb: %1 (%2)").arg(key, d.properties.value("MODALIAS"))); + } + } + } + } + if (d.subsystem == "cpu") { + if (!cpuInfoRead) { + cpuInfo = readCpuInfo(); + if (cpuInfo.state == ReadState::Available) cpuModels = parseCpuModels(cpuInfo.value); + cpuInfoRead = true; + cpuInfo.value.clear(); // Keep only the selected per-processor model fields. + } + Attribute model; + model.state = cpuInfo.state; + model.error = cpuInfo.error; + bool valid = false; + const int number = d.sysname.startsWith("cpu") ? d.sysname.mid(3).toInt(&valid) : -1; + if (valid && cpuModels.contains(number)) model.value = cpuModels.value(number); + else if (model.state == ReadState::Available) model.state = ReadState::Unavailable; + d.attributes.insert("cpu_model", model); + } + struct stat info {}; + const QByteArray path = d.path.toUtf8(); + if (stat(path.constData(), &info) != 0) { ++result.skipped; continue; } + d.incarnation = QStringLiteral("%1:%2:%3") + .arg(static_cast(info.st_dev)).arg(static_cast(info.st_ino)) + .arg(d.properties.value("USEC_INITIALIZED")); + // Only documented text metadata on this device, never on a guessed parent. + QString attribute; + if (d.subsystem == "input" || d.subsystem == "video4linux") attribute = "name"; + else if (d.subsystem == "sound" && d.sysname.startsWith("card")) attribute = "id"; + else if (d.subsystem == "power_supply") attribute = "model_name"; + else if (d.subsystem == "nvme") attribute = "model"; + else if (d.subsystem == "usb" && d.devtype == "usb_device") attribute = "product"; + if (d.subsystem == "hdaudio") { + d.attributes.insert("vendor_name", readAttribute(d.path + "/vendor_name")); + d.attributes.insert("chip_name", readAttribute(d.path + "/chip_name")); + } + if (d.subsystem == "power_supply") { + d.attributes.insert("type", readAttribute(d.path + "/type")); + d.attributes.insert("manufacturer", readAttribute(d.path + "/manufacturer")); + } + if (d.subsystem == "usb" && d.devtype == "usb_device") + d.attributes.insert("manufacturer", readAttribute(d.path + "/manufacturer")); + if (d.subsystem == "drm" && d.sysname.contains('-')) { + d.attributes.insert("status", readAttribute(d.path + "/status")); + d.attributes.insert("enabled", readAttribute(d.path + "/enabled")); + } + if (d.subsystem == "block" && d.devtype == "disk") + d.attributes.insert("queue/rotational", readAttribute(d.path + "/queue/rotational")); + if (!attribute.isEmpty()) d.attributes.insert(attribute, readAttribute(d.path + '/' + attribute)); + if (d.subsystem == "pci" || d.subsystem == "pnp") { + DeviceResources resources; + const int fd = open(path.constData(), O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); + if (fd >= 0) { + resources = collectDeviceResources(fd, d); + close(fd); + } else { + const int error = errno; + resources.issues.append({d.path, {error == EACCES || error == EPERM + ? ReadState::PermissionDenied : error == ENOENT ? ReadState::Unavailable + : error == ENODEV ? ReadState::Removed : ReadState::Error, {}, error}}); + } + d.resources = std::make_shared(std::move(resources)); + } + nameDevice(d); + moduleNames.supplement(d); + struct stat after {}; + if (stat(path.constData(), &after) != 0 || after.st_dev != info.st_dev || after.st_ino != info.st_ino) { + ++result.skipped; + continue; + } + classify(d); + result.devices.append(std::move(d)); + } + if (!(cancelled && cancelled())) refinePresentation(result.devices); + if (!(cancelled && cancelled())) appendEfiVariables(result); + return result; +} diff --git a/src/core/inventory.h b/src/core/inventory.h new file mode 100644 index 0000000..d094383 --- /dev/null +++ b/src/core/inventory.h @@ -0,0 +1,8 @@ +#pragma once +#include "device.h" +#include + +// Synchronous snapshot only: no monitor, event loop or frontend ownership. +Inventory collectInventory(const std::function &cancelled = {}); +// Projection edges bridge only real directory ancestry; inventory stays unchanged. +QHash connectionParents(const QVector &devices); diff --git a/src/core/systemreport.h b/src/core/systemreport.h new file mode 100644 index 0000000..92bce36 --- /dev/null +++ b/src/core/systemreport.h @@ -0,0 +1,29 @@ +#pragma once +#include "systemproperties.h" +#include + +// Shared System Information wording and interpretation; no frontend widgets. +enum class SystemPage { OperatingSystem, Motherboard, Processor, Ram }; +using SystemFields = QVector>; +QStringList systemPageTitles(); +SystemFields systemFields(SystemPage page); +QString systemFieldCaption(SystemPage page, const QString &key); +SystemFields systemMemoryFields(); +QString systemMemoryScope(); +QString systemMemoryTitle(int recordIndex); +QString systemOsValue(const SystemProperties &snapshot, const QString &key, const Attribute &value); +QString systemProcessorValue(const SystemProperties &snapshot, const QString &key, const Attribute &value, bool compact = false); +QString systemRamSummaryValue(const SystemProperties &snapshot, const QString &key); +QString systemMemoryValue(const QString &key, const Attribute &value); +QString systemMokText(const SystemProperties &snapshot); +struct SystemCertificateReport { QString title; SystemFields fields; }; +SystemCertificateReport systemCertificateReport(const MokCertificate &certificate, int index, int count); + +QString systemMotherboardValue(const Attribute &value); +QString systemMokOmitted(int count); +QStringList systemPropertyHeaders(); +QString systemMemoryStatus(const FirmwareMemoryInformation &memory); +QString systemInformationTitle(); +QString systemCollectingText(); +QString systemMokTitle(); +QString systemMemoryHeading(); diff --git a/src/core/systemreportmemory.cpp b/src/core/systemreportmemory.cpp new file mode 100644 index 0000000..450a992 --- /dev/null +++ b/src/core/systemreportmemory.cpp @@ -0,0 +1,77 @@ +#include "systemreport.h" +#include "commonproperties.h" +#include +#include +#include +#include +#include + +namespace { +struct Text { Q_DECLARE_TR_FUNCTIONS(SystemDialog) }; +} + +QString systemMemoryScope() +{ + return Text::tr("Firmware-reported records; these do not establish removable modules, physical slots, " + "memory channels, upgradeability or active ECC. Transfer rates are firmware declarations, not live measurements."); +} + +QVector> systemMemoryFields() +{ + return {{"PRESENT", Text::tr("Presence")}, {"SIZE", Text::tr("Capacity (total)")}, + {"VOLATILE_SIZE", Text::tr("Volatile capacity")}, {"NON_VOLATILE_SIZE", Text::tr("Nonvolatile capacity")}, + {"LOCATOR", Text::tr("Device locator")}, {"BANK_LOCATOR", Text::tr("Bank locator")}, + {"TYPE", Text::tr("Memory type")}, {"FORM_FACTOR", Text::tr("Form factor")}, + {"SPEED_MTS", Text::tr("Rated transfer rate")}, {"CONFIGURED_SPEED_MTS", Text::tr("Configured transfer rate")}, + {"MANUFACTURER", Text::tr("Manufacturer")}, {"PART_NUMBER", Text::tr("Part number")}}; +} + +QString systemRamSummaryValue(const SystemProperties &snapshot, const QString &key) +{ + const Attribute value = snapshot.values.value(key); + if (value.state != ReadState::Available) return propertyReadValue(value); + constexpr qulonglong gib = 1024ULL * 1024 * 1024; + constexpr qulonglong mib = 1024ULL * 1024; + const qulonglong bytes = value.value.toULongLong(); + const int precision = key == "physical_memory" && bytes % gib == 0 ? 0 : 2; + QString text = Text::tr("%1 GiB").arg(QLocale().toString(bytes / static_cast(gib), 'f', precision)); + const Attribute physical = snapshot.values.value("physical_memory"); + if (key == "memory" && physical.state == ReadState::Available) { + const qulonglong installed = physical.value.toULongLong(); + if (installed >= bytes) + text += Text::tr(" (%1 MiB is system reserved)") + .arg(QLocale().toString((installed - bytes) / static_cast(mib), 'f', 0)); + } + return text; +} + +QString systemMemoryValue(const QString &key, const Attribute &value) +{ + if (key == "PRESENT" && value.state == ReadState::Unavailable) + return Text::tr("Unknown (no explicit presence flag)"); + if (value.state != ReadState::Available) return propertyReadValue(value); + if (key == "PRESENT") return value.value == "0" ? Text::tr("No device reported present") : Text::tr("Device reported present"); + if (key == "SPEED_MTS" || key == "CONFIGURED_SPEED_MTS") + return Text::tr("%1 MT/s").arg(QLocale().toString(value.value.toULongLong())); + if (key == "SIZE" || key == "VOLATILE_SIZE" || key == "NON_VOLATILE_SIZE") { + const qulonglong bytes = value.value.toULongLong(); + if (bytes <= static_cast(std::numeric_limits::max())) + return QLocale().formattedDataSize(static_cast(bytes), 2, QLocale::DataSizeIecFormat); + return Text::tr("%1 bytes").arg(QLocale().toString(bytes)); + } + return value.value; +} + +QString systemMemoryTitle(int recordIndex) +{ + return Text::tr("Chip %1").arg(recordIndex + 1); +} + +QStringList systemPropertyHeaders() { return {Text::tr("Property"), Text::tr("Value")}; } +QString systemMemoryStatus(const FirmwareMemoryInformation &memory) +{ + return memory.status.state != ReadState::Available ? propertyReadValue(memory.status) + : memory.devices.isEmpty() ? Text::tr("No memory-device records reported") : QString(); +} + +QString systemMemoryHeading() { return Text::tr("Memory Devices"); } diff --git a/src/core/systemreportplatform.cpp b/src/core/systemreportplatform.cpp new file mode 100644 index 0000000..f9637ad --- /dev/null +++ b/src/core/systemreportplatform.cpp @@ -0,0 +1,160 @@ +#include "systemreport.h" +#include "commonproperties.h" +#include +#include +#include +#include +#include + +namespace { +struct Text { Q_DECLARE_TR_FUNCTIONS(SystemDialog) }; +} + +static SystemFields motherboardFields() +{ + return {{"manufacturer", Text::tr("System manufacturer")}, + {"model", Text::tr("System model")}, + {"board_vendor", Text::tr("Motherboard manufacturer")}, + {"board_model", Text::tr("Motherboard model")}, + {"firmware", Text::tr("Firmware version")}, + {"firmware_date", Text::tr("Firmware date (reported)")}}; +} + +QString systemOsValue(const SystemProperties &snapshot, const QString &key, const Attribute &value) +{ + if (key == "virtualization" && value.state == ReadState::Unavailable) + return Text::tr("Unknown (no hypervisor identity reported)"); + if (key == "gpu_rebar") { + if (snapshot.gpuRebarStatus.state != ReadState::Available) + return propertyReadValue(snapshot.gpuRebarStatus); + if (snapshot.gpuRebar.isEmpty()) return Text::tr("No PCI display controller reported"); + const auto size = [](const Attribute &value) { + if (value.state != ReadState::Available) return propertyReadValue(value); + const qulonglong bytes = value.value.toULongLong(); + if (bytes <= static_cast(std::numeric_limits::max())) + return QLocale().formattedDataSize(static_cast(bytes), 0, QLocale::DataSizeIecFormat); + return Text::tr("%1 bytes").arg(QLocale().toString(bytes)); + }; + QStringList devices; + for (const GpuResizableBar &gpu : snapshot.gpuRebar) { + QStringList lines {Text::tr("GPU [%1]").arg(gpu.pciAddress)}; + if (gpu.status.state == ReadState::Unavailable) lines.append(Text::tr("Capability: Not exposed by kernel")); + else if (gpu.status.state != ReadState::Available) lines.append(propertyReadValue(gpu.status)); + else { + lines.append(Text::tr("Capability: Supported")); + QList bars = gpu.supportedSizes.keys(); + std::sort(bars.begin(), bars.end()); + for (int bar : bars) { + const Attribute bitmap = gpu.supportedSizes.value(bar); + QString supported = propertyReadValue(bitmap); + if (bitmap.state == ReadState::Available) { + const qulonglong bits = bitmap.value.toULongLong(nullptr, 16); + QStringList sizes; + for (int bit = 0; bit < 44; ++bit) + if (bits & (1ULL << bit)) sizes.append(size({ReadState::Available, QString::number(1ULL << (bit + 20)), 0})); + supported = sizes.join(", "); + } + lines.append(Text::tr("BAR %1 — Current allocation: %2; supported sizes: %3") + .arg(bar).arg(size(gpu.currentSizes.value(bar)), supported)); + } + } + lines.append(Text::tr("BIOS setting: Unknown")); + devices.append(lines.join('\n')); + } + return devices.join("\n\n"); + } + + if (value.state != ReadState::Available) return propertyReadValue(value); + if (key == "secure_boot") return value.value == "enabled" ? Text::tr("Enabled") : Text::tr("Disabled"); + if (key == "boot") + return value.value == "uefi" ? Text::tr("UEFI") + : Text::tr("UEFI not exposed (legacy boot or restricted environment)"); + if (key == "virtualization") return Text::tr("Reported hypervisor: %1").arg(value.value); + if (key == "uptime") { + const qulonglong seconds = value.value.toULongLong(); + return Text::tr("%1 days, %2 hours, %3 minutes").arg(seconds / 86400).arg(seconds / 3600 % 24).arg(seconds / 60 % 60); + } + return value.value; +} + +QString systemMokText(const SystemProperties &snapshot) +{ + const MokCertificates &mok = snapshot.mok; + if (mok.status.state != ReadState::Available) return propertyReadValue(mok.status); + QStringList certificates; + const auto known = [](const QString &text) { return text.isEmpty() ? Text::tr("Unavailable") : text; }; + for (const MokCertificate &certificate : mok.certificates) + certificates.append(Text::tr("Owner: %1\nIssuer: %2\nExpires: %3") + .arg(known(certificate.subject), known(certificate.issuer), known(certificate.expires))); + if (certificates.isEmpty()) certificates.append(Text::tr("No X.509 certificates in the exposed MOK list")); + if (mok.otherSignatures) certificates.append(systemMokOmitted(mok.otherSignatures)); + return certificates.join("\n\n"); +} + +QStringList systemPageTitles() +{ + return {Text::tr("Operating System"), Text::tr("Motherboard"), Text::tr("Processor"), Text::tr("RAM")}; +} +SystemFields systemFields(SystemPage page) +{ + switch (page) { + case SystemPage::OperatingSystem: return { + {"hostname", Text::tr("Hostname")}, + {"os", Text::tr("Linux distribution")}, + {"architecture", Text::tr("Kernel architecture")}, + {"kernel", Text::tr("Kernel version")}, + {"build", Text::tr("Kernel build")}, + {"boot", Text::tr("Boot mode")}, + {"secure_boot", Text::tr("Secure Boot")}, + {"virtualization", Text::tr("Virtualization environment")}, + {"uptime", Text::tr("Uptime")}, + {"gpu_rebar", Text::tr("Resizable BAR (GPU)")}}; + case SystemPage::Motherboard: return motherboardFields(); + case SystemPage::Processor: return { + {"cpu", Text::tr("CPU model(s)")}, + {"sockets", Text::tr("Sockets")}, + {"cores", Text::tr("Physical cores")}, + {"logical", Text::tr("Logical CPUs")}, + {"smt", Text::tr("SMT/HT")}, + {"instruction_sets", Text::tr("Instruction sets / CPU features")}, + {"core_groups", Text::tr("Core groups (online CPUs)")}, + {"cache_topology", Text::tr("Cache hierarchy (online CPUs)")}}; + case SystemPage::Ram: return {{"physical_memory", Text::tr("Physical RAM")}, + {"memory", Text::tr("Usable RAM")}}; + } + return {}; +} +QString systemFieldCaption(SystemPage page, const QString &key) +{ + for (const auto &field : systemFields(page)) if (field.first == key) return field.second; + return {}; +} + +SystemCertificateReport systemCertificateReport(const MokCertificate &certificate, int index, int count) +{ + const auto known = [](const QString &text) { return text.isEmpty() ? Text::tr("Unavailable") : text; }; + const QString owner = certificate.subjectDisplay.isEmpty() ? certificate.subject : certificate.subjectDisplay; + const QString issuer = !certificate.subject.isEmpty() && certificate.subject == certificate.issuer + ? Text::tr("Same as owner") + : (certificate.issuerDisplay.isEmpty() ? certificate.issuer : certificate.issuerDisplay); + const QDateTime expires = QDateTime::fromString(certificate.expires, Qt::ISODate); + const QString date = expires.isValid() + ? Text::tr("%1 UTC").arg(QLocale().toString(expires.toUTC(), "d MMM yyyy, HH:mm:ss")) : known(certificate.expires); + return {count == 1 ? Text::tr("MOK Certificate") : Text::tr("MOK Certificate %1").arg(index), + {{Text::tr("Owner:"), known(owner)}, + {Text::tr("Issuer:"), known(issuer)}, + {Text::tr("Expires:"), date}}}; +} + +QString systemMotherboardValue(const Attribute &value) +{ + return value.state == ReadState::Available ? value.value : propertyReadValue(value); +} +QString systemMokOmitted(int count) +{ + return Text::tr("%1 non-certificate signatures omitted").arg(count); +} +QString systemInformationTitle() { return Text::tr("System Information"); } +QString systemCollectingText() { return Text::tr("Collecting…"); } + +QString systemMokTitle() { return Text::tr("MOK certificates"); } diff --git a/src/core/systemreportprocessor.cpp b/src/core/systemreportprocessor.cpp new file mode 100644 index 0000000..2bacef6 --- /dev/null +++ b/src/core/systemreportprocessor.cpp @@ -0,0 +1,160 @@ +#include "systemreport.h" +#include "commonproperties.h" +#include +#include +#include +#include +#include + +namespace { +struct Text { Q_DECLARE_TR_FUNCTIONS(SystemDialog) }; +} + +QString systemProcessorValue(const SystemProperties &snapshot, const QString &key, const Attribute &value, bool compact) +{ + if (key == "logical") { + const Attribute online = snapshot.values.value("online"); + if (value.state == ReadState::Available && online.state == ReadState::Available && value.value == online.value) + return value.value; + return Text::tr("%1 present; %2 online").arg(propertyReadValue(value), propertyReadValue(online)); + } + if (value.state != ReadState::Available) return propertyReadValue(value); + if (key == "smt") { + if (value.value == "on") return Text::tr("Enabled (kernel policy)"); + if (value.value == "off") return Text::tr("Disabled"); + if (value.value == "forceoff") return Text::tr("Disabled by system policy"); + if (value.value == "notsupported") return Text::tr("Not supported"); + if (value.value == "notimplemented") return Text::tr("Status reporting not implemented by kernel"); + return Text::tr("Unknown (%1)").arg(value.value); + } + // Tab-separated cells are shared by the native views and Copy All. + const auto tableText = [](const QVector &rows) { + QStringList lines; + for (const auto &row : rows) lines.append(row.join('\t')); + return lines.join('\n'); + }; + if (key == "core_groups") { + QVector rows {{Text::tr("Core group"), Text::tr("Cores / threads"), Text::tr("Maximum frequency"), Text::tr("Logical CPUs")}}; + const Attribute sockets = snapshot.values.value("sockets"); + const bool showPackage = sockets.state != ReadState::Available || sockets.value.toUInt() != 1; + for (const CpuCoreGroup &group : snapshot.cpuGroups) { + if (group.members.isEmpty()) continue; + const auto &first = group.members.constFirst(); + const QString kind = first.kind == 1 ? Text::tr("Performance cores") + : first.kind == 2 ? Text::tr("Efficiency cores") : Text::tr("Cores (type unspecified)"); + QSet cpus; + QMap threads; + qulonglong minimum = std::numeric_limits::max(), maximum = 0; + Attribute frequencyError; + bool frequencyComplete = true; + for (const auto &member : group.members) { + cpus.insert(member.cpu); + ++threads[member.core]; + if (member.maximumKhz.state != ReadState::Available) { + if (frequencyComplete) frequencyError = member.maximumKhz; + frequencyComplete = false; + } else { + const qulonglong khz = member.maximumKhz.value.toULongLong(); + minimum = qMin(minimum, khz); + maximum = qMax(maximum, khz); + } + } + const auto ghz = [](qulonglong khz) { return QLocale().toString(khz / 1000000.0, 'g', 6); }; + const QString frequency = !frequencyComplete ? propertyReadValue(frequencyError) + : minimum == maximum ? Text::tr("%1 GHz").arg(ghz(maximum)) + : Text::tr("%1–%2 GHz").arg(ghz(minimum), ghz(maximum)); + const QString name = showPackage ? Text::tr("%1 (package %2)").arg(kind, first.package) : kind; + rows.append({name, Text::tr("%1 / %2").arg(threads.size()).arg(cpus.size()), frequency, cpuListText(cpus)}); + } + return rows.size() == 1 ? Text::tr("Unavailable") : tableText(rows); + } + if (key == "cache_topology") { + // Keep each sharing domain explicit; aggregate equal-sized cache instances. + QMap> kinds; + for (const auto &cache : snapshot.cpuCaches) + kinds[QStringLiteral("%1/%2/%3").arg(cache.level).arg(cache.type).arg(cache.bytes)].append(cache); + QMap cpuCores; + QMap> coreCpus; + QSet onlineCpus; + for (const auto &group : snapshot.cpuGroups) { + for (const auto &member : group.members) { + cpuCores.insert(member.cpu, member.core); + coreCpus[member.core].insert(member.cpu); + onlineCpus.insert(member.cpu); + } + } + QVector rows {{Text::tr("Cache"), Text::tr("Configuration"), Text::tr("Sharing (logical CPUs)")}}; + for (auto it = kinds.cbegin(); it != kinds.cend(); ++it) { + auto instances = it.value(); + std::sort(instances.begin(), instances.end(), [](const auto &a, const auto &b) { + return *std::min_element(a.cpus.cbegin(), a.cpus.cend()) + < *std::min_element(b.cpus.cbegin(), b.cpus.cend()); + }); + const auto &cache = instances.constFirst(); + const QString type = cache.type == "Data" ? Text::tr("data") + : cache.type == "Instruction" ? Text::tr("instruction") : Text::tr("unified"); + QStringList sharing; + QSet privateCpus; + bool allPrivate = true; + for (const auto &instance : instances) { + sharing.append(cpuListText(instance.cpus)); + allPrivate = allPrivate && instance.cpus.size() == 1; + privateCpus.unite(instance.cpus); + } + QString domains = allPrivate ? Text::tr("Private to CPUs %1").arg(cpuListText(privateCpus)) + : instances.size() == 1 ? Text::tr("Shared by CPUs %1").arg(sharing.constFirst()) + : Text::tr("CPU groups %1").arg(sharing.join("; ")); + if (compact && !onlineCpus.isEmpty()) { + // Require an exact match to a known core's online threads. Unknown + // or offline sharing IDs keep the explicit kernel sharing sets. + bool perCore = true; + QSet seenCores; + for (const auto &instance : instances) { + const int cpu = *instance.cpus.cbegin(); + const QString core = cpuCores.value(cpu); + if (!cpuCores.contains(cpu) || instance.cpus != coreCpus.value(core) + || seenCores.contains(core)) { + perCore = false; + break; + } + seenCores.insert(core); + } + if (perCore) + domains = privateCpus == onlineCpus ? Text::tr("Private per core") + : Text::tr("Private per core · CPUs %1").arg(cpuListText(privateCpus)); + else if (instances.size() == 1 && cache.cpus == onlineCpus) + domains = Text::tr("Shared by all online cores"); + } + const QString size = QLocale().formattedDataSize(static_cast(cache.bytes), 0, QLocale::DataSizeIecFormat); + rows.append({Text::tr("L%1 %2").arg(cache.level).arg(type), + instances.size() == 1 ? size : Text::tr("%1 × %2").arg(instances.size()).arg(size), domains}); + } + return rows.size() == 1 ? Text::tr("Unavailable") : tableText(rows); + } + if (key == "instruction_sets") { + const QStringList groups = value.value.split('\n'); + QStringList lines; + for (const QString &group : groups) { + const QStringList parts = group.split('\t'); + lines.append(groups.size() == 1 ? parts.value(1) + : Text::tr("Logical CPUs %1: %2").arg(parts.value(0), parts.value(1))); + } + return lines.join("\n\n"); + } + if (key == "caches") { + QStringList lines; + for (const QString &raw : value.value.split('\n')) { + const QStringList parts = raw.split('\t'); + const QString kind = parts.value(0); + QString type = kind.section(' ', 1); + if (type == "Data") type = Text::tr("data"); + else if (type == "Instruction") type = Text::tr("instruction"); + else if (type == "Unified") type = Text::tr("unified"); + lines.append(Text::tr("%1 %2: %3 (%4 unique instances)") + .arg(kind.section(' ', 0, 0), type, + QLocale().formattedDataSize(parts.value(1).toLongLong()), parts.value(2))); + } + return lines.join('\n'); + } + return value.value; +} diff --git a/src/devices/enumerator.cpp b/src/devices/enumerator.cpp index 8a48ac8..ae0dcdf 100644 --- a/src/devices/enumerator.cpp +++ b/src/devices/enumerator.cpp @@ -1,196 +1,27 @@ #include "enumerator.h" -#include "cpumetadata.h" -#include "devicepresentation.h" -#include "modulenames.h" -#include "efivariables.h" - +#include "inventory.h" +#include "monitorstate.h" #include -#include +#include #include #include #include -#include -#include #include -#include +#include #include -#include -#include "monitorstate.h" -#include "deviceresources.h" +#include namespace { QString text(const char *value) { - // All values are treated as plain text. Bound the owned copy. return value ? QString::fromUtf8(value, static_cast(strnlen(value, 4096))) : QString(); } -Attribute readAttribute(const QString &path) -{ - Attribute result; - const QByteArray encoded = path.toUtf8(); - const int fd = open(encoded.constData(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW); - if (fd < 0) { - result.error = errno; - result.state = errno == EACCES || errno == EPERM ? ReadState::PermissionDenied - : errno == ENOENT ? ReadState::Unavailable : ReadState::Error; - return result; - } - char buffer[4097]; - ssize_t count; - do { count = read(fd, buffer, sizeof(buffer)); } while (count < 0 && errno == EINTR); - const int readError = errno; - close(fd); - if (count < 0) { - result.error = readError; - result.state = readError == ENODEV ? ReadState::Removed - : readError == EACCES || readError == EPERM ? ReadState::PermissionDenied : ReadState::Error; - } else if (count > 4096) { - result.error = EOVERFLOW; - result.state = ReadState::Error; - } else { - result.state = ReadState::Available; - result.value = QString::fromUtf8(buffer, static_cast(count)).trimmed(); - } - return result; -} } Inventory Enumerator::collect() { - Inventory result; - result.request = request_.load(); - std::unique_ptr context(udev_new(), &udev_unref); - if (!context) { - result.error = tr("Could not create the udev context."); - return result; - } - std::unique_ptr scan( - udev_enumerate_new(context.get()), &udev_enumerate_unref); - if (!scan || udev_enumerate_scan_devices(scan.get()) < 0) { - result.error = tr("Could not enumerate kernel devices."); - return result; - } - // hwdb is an optional local naming database, still accessed through libudev. - std::unique_ptr hwdb(udev_hwdb_new(context.get()), &udev_hwdb_unref); - ModuleNames moduleNames; // Worker-owned context and per-refresh lookup cache. - Attribute cpuInfo; - QHash cpuModels; - bool cpuInfoRead = false; - udev_list_entry *entry = nullptr; - udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(scan.get())) { - if (isInterruptionRequested()) break; - std::unique_ptr raw( - udev_device_new_from_syspath(context.get(), udev_list_entry_get_name(entry)), &udev_device_unref); - if (!raw) { ++result.skipped; continue; } - Device d; - d.path = text(udev_device_get_syspath(raw.get())); - if (!d.path.startsWith("/sys/devices/")) { ++result.skipped; continue; } - d.sysname = text(udev_device_get_sysname(raw.get())); - d.subsystem = text(udev_device_get_subsystem(raw.get())); - d.devtype = text(udev_device_get_devtype(raw.get())); - d.driver = text(udev_device_get_driver(raw.get())); - if (!d.driver.isEmpty()) { - const QString target = QFileInfo(d.path + "/driver/module").symLinkTarget(); - if (target.startsWith("/sys/module/")) d.driverModule = QFileInfo(target).fileName(); - } - if (auto *parent = udev_device_get_parent(raw.get())) - d.parentPath = text(udev_device_get_syspath(parent)); - // Explicit allowlist: no recursive sysfs reads, binary resources or hardware queries. - for (const char *key : {"ID_MODEL_FROM_DATABASE", "ID_MODEL", "NAME", "ID_V4L_PRODUCT", "MODALIAS", "PCI_CLASS", - "PCI_ID", "ID_VENDOR_FROM_DATABASE", "ID_VENDOR", "ID_BUS", - "ID_INPUT_KEYBOARD", "ID_INPUT_MOUSE", "ID_INPUT_TOUCHPAD", - "ID_INPUT_POINTINGSTICK", "ID_INPUT_SWITCH", "PRODUCT", "PHYS", "DEVNUM", - "USEC_INITIALIZED", "ID_DRIVE_THUMB"}) { - const char *value = udev_device_get_property_value(raw.get(), key); - if (value) d.properties.insert(QLatin1String(key), text(value)); - } - if (hwdb && (d.subsystem == "pci" || d.subsystem == "usb") - && (d.properties.value("ID_MODEL_FROM_DATABASE").isEmpty() - || (d.subsystem == "usb" && d.properties.value("ID_VENDOR_FROM_DATABASE").isEmpty()))) { - const QByteArray alias = d.properties.value("MODALIAS").toUtf8(); - if (!alias.isEmpty()) { - udev_list_entry *property = nullptr; - udev_list_entry_foreach(property, udev_hwdb_get_properties_list_entry(hwdb.get(), alias.constData(), 0)) { - const QString key = text(udev_list_entry_get_name(property)); - if ((key == "ID_MODEL_FROM_DATABASE" || key == "ID_VENDOR_FROM_DATABASE") - && d.properties.value(key).isEmpty()) { - d.properties.insert(key, text(udev_list_entry_get_value(property))); - d.propertySources.insert(key, QStringLiteral("hwdb: %1 (%2)").arg(key, d.properties.value("MODALIAS"))); - } - } - } - } - if (d.subsystem == "cpu") { - if (!cpuInfoRead) { - cpuInfo = readCpuInfo(); - if (cpuInfo.state == ReadState::Available) cpuModels = parseCpuModels(cpuInfo.value); - cpuInfoRead = true; - cpuInfo.value.clear(); // Keep only the selected per-processor model fields. - } - Attribute model; - model.state = cpuInfo.state; - model.error = cpuInfo.error; - bool valid = false; - const int number = d.sysname.startsWith("cpu") ? d.sysname.mid(3).toInt(&valid) : -1; - if (valid && cpuModels.contains(number)) model.value = cpuModels.value(number); - else if (model.state == ReadState::Available) model.state = ReadState::Unavailable; - d.attributes.insert("cpu_model", model); - } - struct stat info {}; - const QByteArray path = d.path.toUtf8(); - if (stat(path.constData(), &info) != 0) { ++result.skipped; continue; } - d.incarnation = QStringLiteral("%1:%2:%3") - .arg(static_cast(info.st_dev)).arg(static_cast(info.st_ino)) - .arg(d.properties.value("USEC_INITIALIZED")); - // Only documented text metadata on this device, never on a guessed parent. - QString attribute; - if (d.subsystem == "input" || d.subsystem == "video4linux") attribute = "name"; - else if (d.subsystem == "sound" && d.sysname.startsWith("card")) attribute = "id"; - else if (d.subsystem == "power_supply") attribute = "model_name"; - else if (d.subsystem == "nvme") attribute = "model"; - else if (d.subsystem == "usb" && d.devtype == "usb_device") attribute = "product"; - if (d.subsystem == "hdaudio") { - d.attributes.insert("vendor_name", readAttribute(d.path + "/vendor_name")); - d.attributes.insert("chip_name", readAttribute(d.path + "/chip_name")); - } - if (d.subsystem == "power_supply") { - d.attributes.insert("type", readAttribute(d.path + "/type")); - d.attributes.insert("manufacturer", readAttribute(d.path + "/manufacturer")); - } - if (d.subsystem == "usb" && d.devtype == "usb_device") - d.attributes.insert("manufacturer", readAttribute(d.path + "/manufacturer")); - if (d.subsystem == "drm" && d.sysname.contains('-')) { - d.attributes.insert("status", readAttribute(d.path + "/status")); - d.attributes.insert("enabled", readAttribute(d.path + "/enabled")); - } - if (d.subsystem == "block" && d.devtype == "disk") - d.attributes.insert("queue/rotational", readAttribute(d.path + "/queue/rotational")); - if (!attribute.isEmpty()) d.attributes.insert(attribute, readAttribute(d.path + '/' + attribute)); - if (d.subsystem == "pci" || d.subsystem == "pnp") { - DeviceResources resources; - const int fd = open(path.constData(), O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); - if (fd >= 0) { - resources = collectDeviceResources(fd, d); - close(fd); - } else { - const int error = errno; - resources.issues.append({d.path, {error == EACCES || error == EPERM - ? ReadState::PermissionDenied : error == ENOENT ? ReadState::Unavailable - : error == ENODEV ? ReadState::Removed : ReadState::Error, {}, error}}); - } - d.resources = std::make_shared(std::move(resources)); - } - nameDevice(d); - moduleNames.supplement(d); - struct stat after {}; - if (stat(path.constData(), &after) != 0 || after.st_dev != info.st_dev || after.st_ino != info.st_ino) { - ++result.skipped; - continue; - } - classify(d); - result.devices.append(std::move(d)); - } - if (!isInterruptionRequested()) refinePresentation(result.devices); - if (!isInterruptionRequested()) appendEfiVariables(result); + const quint64 request = request_.load(); + Inventory result = collectInventory([this] { return isInterruptionRequested(); }); + result.request = request; return result; } diff --git a/src/devices/propertytext.cpp b/src/devices/propertytext.cpp index dd9328a..beeaf59 100644 --- a/src/devices/propertytext.cpp +++ b/src/devices/propertytext.cpp @@ -7,29 +7,6 @@ #include #include -QString propertyReadValue(const Attribute &a) -{ - switch (a.state) { - case ReadState::Available: - return a.value.isEmpty() ? QCoreApplication::translate("PropertiesDialog", "Empty value") : a.value; - case ReadState::Unavailable: return QCoreApplication::translate("PropertiesDialog", "Unavailable"); - case ReadState::PermissionDenied: return QCoreApplication::translate("PropertiesDialog", "Permission denied"); - case ReadState::Removed: return QCoreApplication::translate("PropertiesDialog", "Device removed"); - case ReadState::Unsupported: return QCoreApplication::translate("PropertiesDialog", "Unsupported"); - case ReadState::NotApplicable: return QCoreApplication::translate("PropertiesDialog", "Not applicable"); - case ReadState::Error: - switch (a.error) { - case ETIMEDOUT: return QCoreApplication::translate("PropertiesDialog", "Query timed out (errno %1)").arg(a.error); - case EOVERFLOW: return QCoreApplication::translate("PropertiesDialog", "Data exceeds supported limits (errno %1)").arg(a.error); - case EAGAIN: return QCoreApplication::translate("PropertiesDialog", "Temporarily unavailable; retry the query (errno %1)").arg(a.error); - case EINVAL: - case EPROTO: return QCoreApplication::translate("PropertiesDialog", "Invalid data or query (errno %1)").arg(a.error); - case ECANCELED: return QCoreApplication::translate("PropertiesDialog", "Query cancelled (errno %1)").arg(a.error); - default: return QCoreApplication::translate("PropertiesDialog", "Read error (errno %1)").arg(a.error); - } - } - return {}; -} QString detailsPropertyStatus(const PropertyEntry &entry) { if (entry.state == ReadState::Available) @@ -157,17 +134,12 @@ QVector propertyEntries(const DeviceProperties &snapshot, bool re QVector entries; const Device &d = snapshot.device; const bool efi = d.subsystem == "efivarfs"; - const Attribute notApplicable {ReadState::NotApplicable, {}, 0}; const auto add = [&](const QString &id, const QString &label, const QString &value, const QString &source = QString(), bool advanced = false, int tab = 2, ReadState state = ReadState::Available, int error = 0) { entries.append({id, label, value, source, advanced, tab, state, error}); }; const auto raw = [&](const QString &id) { return snapshot.values.value(id); }; - const auto addRaw = [&](const QString &id, const QString &label, int tab, const Attribute &fallback = Attribute()) { - const Attribute a = snapshot.values.value(id, fallback); - add(id, label, propertyReadValue(a), snapshot.sources.value(id), false, tab, a.state, a.error); - }; if (snapshot.storage.applicable) { add("storage/scope", QCoreApplication::translate("Storage", "Storage snapshot"), QCoreApplication::translate("Storage", "Each entity is a separate storage layer; capacities are not added. Health is cached UDisks2 evidence, separate from operational status. Reload never requests SMART updates or self-tests. Deep search includes native metadata only."), {}, false, 3); @@ -184,115 +156,7 @@ QVector propertyEntries(const DeviceProperties &snapshot, bool re add("storage/link/" + QString::number(index++), QCoreApplication::translate("Storage", edge.label), edge.from + "\n→ " + edge.to, edge.source, false, 3); } - add("name", QCoreApplication::translate("PropertiesDialog", "Device name"), deviceDisplayName(d), d.nameSource, false, 0); - add("category", QCoreApplication::translate("PropertiesDialog", "Category"), categoryLabel(d.category), QCoreApplication::translate("PropertiesDialog", "Application grouping"), false, 0); - Attribute manufacturer; - QString manufacturerSource; - if (efi) manufacturer = notApplicable; - else { - for (const QString &id : {QStringLiteral("sysfs/manufacturer"), QStringLiteral("sysfs/vendor_name"), - QStringLiteral("acpi/vendor"), - QStringLiteral("udev/ID_VENDOR_FROM_DATABASE"), QStringLiteral("udev/ID_VENDOR")}) { - const Attribute candidate = raw(id); - if (candidate.state == ReadState::Available && !candidate.value.isEmpty()) { - manufacturer = candidate; manufacturerSource = snapshot.sources.value(id); break; - } - if (candidate.state == ReadState::PermissionDenied || candidate.state == ReadState::Error) { - manufacturer = candidate; manufacturerSource = snapshot.sources.value(id); break; - } - } - if (manufacturer.state == ReadState::Unavailable) { - for (const QString &key : {QStringLiteral("ID_VENDOR_FROM_DATABASE"), QStringLiteral("ID_VENDOR")}) { - if (d.properties.value(key).isEmpty()) continue; - manufacturer = {ReadState::Available, d.properties.value(key), 0}; - manufacturerSource = d.propertySources.value(key, QStringLiteral("udev: %1 (%2)").arg(key, d.path)); - break; - } - } - } - add("manufacturer", QCoreApplication::translate("PropertiesDialog", "Manufacturer"), propertyReadValue(manufacturer), manufacturerSource, false, 0, manufacturer.state, manufacturer.error); - Attribute bus = raw("udev/ID_BUS"); - if (bus.state != ReadState::Available) bus = raw("driver/bus"); - if (efi) bus = notApplicable; - add("bus", QCoreApplication::translate("PropertiesDialog", "Bus / subsystem"), efi ? propertyReadValue(bus) - : bus.state == ReadState::Available ? bus.value + " / " + d.subsystem : d.subsystem, - QCoreApplication::translate("PropertiesDialog", "Device udev metadata and direct driver bus"), false, 0, efi ? ReadState::NotApplicable : ReadState::Available); - add("location", QCoreApplication::translate("PropertiesDialog", "Location"), efi ? d.parentPath : d.path, QCoreApplication::translate("PropertiesDialog", "Kernel path"), false, 0); - QString status; - if (removed) status = QCoreApplication::translate("PropertiesDialog", "Removed or replaced; showing a snapshot."); - else if (snapshot.state != ReadState::Available) status = QCoreApplication::translate("PropertiesDialog", "Present in the last inventory; properties have not been verified."); - else { - status = QCoreApplication::translate("PropertiesDialog", "Present at the last property read."); - if (!efi) { - const Attribute binding = raw("driver/name"); - status += '\n' + (binding.state == ReadState::Available - ? binding.value.isEmpty() ? QCoreApplication::translate("PropertiesDialog", "No directly bound driver; this alone is not a fault.") : QCoreApplication::translate("PropertiesDialog", "Driver bound: %1").arg(binding.value) - : QCoreApplication::translate("PropertiesDialog", "Driver binding: %1").arg(propertyReadValue(binding))); - const Attribute authorization = raw("sysfs/authorized"); - if (authorization.state == ReadState::Available && authorization.value == "0") status += '\n' + QCoreApplication::translate("PropertiesDialog", "USB device/interface is unauthorized."); - else if (authorization.state == ReadState::Available && authorization.value == "1") status += '\n' + QCoreApplication::translate("PropertiesDialog", "USB device/interface is authorized."); - else if (authorization.state == ReadState::PermissionDenied || authorization.state == ReadState::Error) - status += '\n' + QCoreApplication::translate("PropertiesDialog", "USB authorization: %1").arg(propertyReadValue(authorization)); - } - } - add("status", QCoreApplication::translate("PropertiesDialog", "Device status"), status, QCoreApplication::translate("PropertiesDialog", "Observed presence, binding and USB authorization only"), false, 0); - Attribute binding = snapshot.values.value("driver/name", d.driver.isEmpty() ? Attribute() - : Attribute{ReadState::Available, d.driver, 0}); - if (efi) binding = notApplicable; - const QString driverName = !efi && binding.state == ReadState::Available && binding.value.isEmpty() - ? QCoreApplication::translate("PropertiesDialog", "No directly bound driver") : propertyReadValue(binding); - add("driver/name", QCoreApplication::translate("PropertiesDialog", "Bound kernel driver"), driverName, efi ? QString() : d.path + "/driver", false, 0, binding.state, binding.error); - QStringList identifiers; - const Attribute alias = snapshot.values.value("udev/MODALIAS", - d.properties.contains("MODALIAS") - ? Attribute{ReadState::Available, d.properties.value("MODALIAS"), 0} : Attribute()); - if (alias.state == ReadState::Available && !alias.value.isEmpty()) { - // Use the collected alias, never a device-name prefix or a related device's ID. - if ((d.subsystem == "acpi" || d.subsystem == "platform" || d.subsystem == "i2c") - && alias.value.startsWith("acpi:")) { - const QStringList ids = alias.value.mid(5).split(':', Qt::SkipEmptyParts); - if (!ids.isEmpty()) identifiers.append( - QCoreApplication::translate("PropertiesDialog", "ACPI identifiers: %1").arg(ids.join(", "))); - } - identifiers.append(QCoreApplication::translate("PropertiesDialog", "Modalias: %1").arg(alias.value)); - } - for (const QString &key : {QStringLiteral("PCI_ID"), QStringLiteral("PCI_SUBSYS_ID"), QStringLiteral("PRODUCT"), - QStringLiteral("ID_VENDOR_ID"), QStringLiteral("ID_MODEL_ID"), QStringLiteral("ID_SERIAL_SHORT")}) { - const Attribute a = snapshot.values.value("udev/" + key, - d.properties.contains(key) ? Attribute{ReadState::Available, d.properties.value(key), 0} : Attribute()); - if (a.state == ReadState::Available && !a.value.isEmpty()) identifiers.append(key + ": " + a.value); - } - for (const QString &key : {QStringLiteral("vendor"), QStringLiteral("device"), QStringLiteral("idVendor"), - QStringLiteral("idProduct"), QStringLiteral("serial"), QStringLiteral("hid"), QStringLiteral("uid")}) { - const Attribute a = raw("sysfs/" + key); - if (a.state != ReadState::Unavailable && !(a.state == ReadState::Available && a.value.isEmpty())) - identifiers.append(key + ": " + propertyReadValue(a)); - } - add("identifiers", QCoreApplication::translate("PropertiesDialog", "Hardware identifiers"), efi ? propertyReadValue(notApplicable) - : identifiers.isEmpty() ? QCoreApplication::translate("PropertiesDialog", "Unavailable") : identifiers.join('\n'), QCoreApplication::translate("PropertiesDialog", "Selected udev and direct sysfs identifiers"), false, 0, - efi ? ReadState::NotApplicable : identifiers.isEmpty() ? ReadState::Unavailable : ReadState::Available); - add("driver/binding", QCoreApplication::translate("PropertiesDialog", "Bound kernel driver"), driverName, efi ? QString() : d.path + "/driver", false, 1, binding.state, binding.error); - const Attribute moduleFallback = efi || (binding.state == ReadState::Available && binding.value.isEmpty()) - ? notApplicable : Attribute(); - addRaw("driver/bus", QCoreApplication::translate("PropertiesDialog", "Driver bus"), 1, moduleFallback); - addRaw("module/name", QCoreApplication::translate("PropertiesDialog", "Owning kernel module"), 1, moduleFallback); - const Attribute type = snapshot.values.value("module/type", moduleFallback); - add("module/type", QCoreApplication::translate("PropertiesDialog", "Built-in or modular"), type.state == ReadState::Available ? QCoreApplication::translate("PropertiesDialog", "Modular (kernel initstate evidence)") - : type.state == ReadState::NotApplicable || type.state == ReadState::PermissionDenied || type.state == ReadState::Error - ? propertyReadValue(type) : QCoreApplication::translate("PropertiesDialog", "Undetermined; a missing module link/version does not prove a built-in driver."), - snapshot.sources.value("module/type"), false, 1, - type.state == ReadState::NotApplicable || type.state == ReadState::PermissionDenied || type.state == ReadState::Error - ? type.state : ReadState::Available, type.error); - addRaw("module/runtime_version", QCoreApplication::translate("PropertiesDialog", "Running module version"), 1, moduleFallback); - const QStringList fields {"filename", "version", "description", "author", "license", "firmware"}; - const QStringList labels {QCoreApplication::translate("PropertiesDialog", "Installed module filename"), QCoreApplication::translate("PropertiesDialog", "Installed module version"), QCoreApplication::translate("PropertiesDialog", "Installed module description"), - QCoreApplication::translate("PropertiesDialog", "Installed module author"), QCoreApplication::translate("PropertiesDialog", "Installed module license"), QCoreApplication::translate("PropertiesDialog", "Declared firmware names")}; - for (int i = 0; i < fields.size(); ++i) addRaw("module/" + fields[i], labels[i], 1, moduleFallback); - addRaw("sysfs/firmware_rev", QCoreApplication::translate("PropertiesDialog", "Reported device firmware revision"), 1, - d.subsystem == "nvme" ? Attribute() : notApplicable); - add("module/note", QCoreApplication::translate("PropertiesDialog", "Metadata scope"), efi ? propertyReadValue(notApplicable) - : QCoreApplication::translate("PropertiesDialog", "Installed metadata may differ from code already loaded before an update. Declared firmware names do not prove firmware is loaded or report its version."), {}, false, 1, - efi ? ReadState::NotApplicable : ReadState::Available); + entries += commonPropertyEntries({snapshot.device, snapshot.values, snapshot.sources, snapshot.state}, removed); // Interpreted facts plus curated raw values, keeping provenance beside each. add("parent", QCoreApplication::translate("PropertiesDialog", "Parent device path"), d.parentPath.isEmpty() ? QCoreApplication::translate("PropertiesDialog", "Unavailable") : d.parentPath, QCoreApplication::translate("PropertiesDialog", "Recorded ancestry"), efi, 2, d.parentPath.isEmpty() ? ReadState::Unavailable : ReadState::Available); add("raw_name", QCoreApplication::translate("PropertiesDialog", "Original name"), d.name, d.nameSource, true); diff --git a/src/devices/propertytext.h b/src/devices/propertytext.h index 07b9755..99757a9 100644 --- a/src/devices/propertytext.h +++ b/src/devices/propertytext.h @@ -1,22 +1,11 @@ #pragma once #include "deviceproperties.h" +#include "commonproperties.h" -// Pure text presentation shared by Properties and deep search; no widgets. -struct PropertyEntry { - QString id; - QString label; - QString value; - QString source; - bool advanced = false; - int tab = 2; - ReadState state = ReadState::Available; - int error = 0; -}; // Empty suffix means this entry contains an available value. QString detailsPropertyStatus(const PropertyEntry &entry); QString storageFieldValue(const StorageField &field); QString storageDuration(quint64 seconds); // UI boundary: storage entries remain searchable, but never enter Details or its copy output. bool isDetailsProperty(const PropertyEntry &entry, const Device &device); -QString propertyReadValue(const Attribute &attribute); QVector propertyEntries(const DeviceProperties &snapshot, bool removed = false); diff --git a/src/tui/browser.cpp b/src/tui/browser.cpp new file mode 100644 index 0000000..3d11ae2 --- /dev/null +++ b/src/tui/browser.cpp @@ -0,0 +1,271 @@ +#include "browser.h" +#include "browsermenu.h" +#include "systemdialog.h" +#include "systemsnapshot.h" +#include "treeprojection.h" +#include "inventoryfilter.h" +#include "terminaltext.h" +#include "propertiesdialog.h" +#include +#include +#include + +namespace { +using namespace finalcut; +class Browser final : public FDialog { +public: + Browser(Inventory inventory, FWidget *parent) : FDialog(parent), inventory_(std::move(inventory)), + menu_(this, [this](bool connection) { setView(connection); }, + [this](bool show) { showAll_ = show; populate(); }, + [this] { systemInformation(); }) + { + // The dialog supplies the only frame and its caption. An unnamed + // column keeps FINAL CUT from drawing a separate tree headline. + tree_.addColumn(""); + tree_.ignorePadding(); + treeCaption_.ignorePadding(); + tree_.setTreeView(); + filter_.setLabelText("&Filter"); + // Keep the field and its separate label out of initial layout/focus. + filter_.setVisible(false); + filter_.setEnable(false); + filter_.setFocusable(false); + filter_.getLabelObject()->setVisible(false); + filter_.addCallback("changed", [this] { filterChanged(); }); + filter_.addCallback("activate", [this] { + if (query_.isEmpty()) tree_.setFocus(); + else findNext(false, true); + redraw(); + }); + tree_.addCallback("clicked", [this] { properties(); }); + switch_.addCallback("clicked", [this] { setView(!connection_); }); + refresh_.addCallback("clicked", [this] { + Inventory next = collectInventory(); + if (!next.error.isEmpty()) { + FMessageBox::error(this, display(next.error)); + return; + } + inventory_ = std::move(next); + systemSnapshot_.refresh(); + if (!systemTimer_) systemTimer_ = addTimer(200); + populate(); + }); + quit_.addCallback("clicked", [this] { close(); }); + viewKey_.addCallback("activate", [this] { setView(!connection_); }); + menuKey_.addCallback("activate", [this] { + if (!modalOpen_) menu_.open(); + }); + refreshKey_.addCallback("activate", [this] { refresh_.emitCallback("clicked"); }); + quitKey_.addCallback("activate", [this] { close(); }); + findKey_.addCallback("activate", [this] { find(); }); + nextKey_.addCallback("activate", [this] { findNext(false); }); + previousKey_.addCallback("activate", [this] { findNext(true); }); + populate(); + } + ~Browser() override { delTimer(systemTimer_); } +private: + void onTimer(FTimerEvent *event) override + { + // The System Information dialog polls while modal; do not consume its + // completion notification here. Other dialogs can defer publication. + if (!modalOpen_) { + systemSnapshot_.poll(); + if (!systemSnapshot_.collecting() && systemTimer_) { + delTimer(systemTimer_); + systemTimer_ = 0; + } + } + FDialog::onTimer(event); + } + void systemInformation() + { + if (modalOpen_) return; + modalOpen_ = true; + showSystemInformation(systemSnapshot_, this); + modalOpen_ = false; + if (systemSnapshot_.collecting() && !systemTimer_) systemTimer_ = addTimer(200); + } + void setView(bool connection) + { + if (connection_ == connection) return; + connection_ = connection; + populate(); + } + void find() + { + if (modalOpen_) return; + if (!filterVisible_) { + filterVisible_ = true; + filter_.setEnable(true); + filter_.setFocusable(true); + filter_.setVisible(true); + filter_.getLabelObject()->setVisible(true); + adjustSize(); + filter_.show(); + filter_.getLabelObject()->show(); + } + filter_.setFocus(); + redraw(); + } + void filterChanged() + { + const QString query = QString::fromStdWString(filter_.getText().toWString()).trimmed(); + if (query == query_) return; + query_ = query; + populate(); + } + void clearFilter() + { + filter_.setText(""); + filterChanged(); // Also covers toolkit versions where setText emits no change. + tree_.setFocus(); + if (filterVisible_) { + filterVisible_ = false; + filter_.hide(); // FINAL CUT also hides the field's separate label. + filter_.setVisible(false); + filter_.setEnable(false); + filter_.setFocusable(false); + filter_.getLabelObject()->setVisible(false); + adjustSize(); + } + redraw(); + } + void onKeyPress(FKeyEvent *event) override + { + if (event->key() == FKey::Meta_i && !modalOpen_) { + systemInformation(); + event->accept(); + return; + } + if (event->key() == FKey::Ctrl_f && !modalOpen_) { + find(); + event->accept(); + return; + } + if (event->key() == FKey::Escape && !modalOpen_) { + clearFilter(); + event->accept(); + return; + } + FDialog::onKeyPress(event); + } + void findNext(bool previous, bool includeCurrent = false) + { + if (modalOpen_) return; + if (query_.isEmpty()) { find(); return; } + // Traverse actual widget order (including collapsed branches), so header + // sorting is respected. Category/computer rows never count as matches. + QVector items; + std::function visit = [&](FListViewItem *item) { + items.append(item); + for (auto *child : item->getChildren()) + visit(static_cast(child)); + }; + for (auto *item : tree_.getData()) visit(item); + QVector hits; + int current = -1; + for (int i = 0; i < items.size(); ++i) { + if (items[i] == tree_.getCurrentItem()) current = i; + const int index = items[i]->getData(); + if (index >= 0 && index < inventory_.devices.size() + && matches(inventory_.devices.at(index), query_)) hits.append(i); + } + if (hits.isEmpty()) { + status_.setMessage("No matches in current view. F4: change search"); + redraw(); + return; + } + int hit = previous ? int(hits.size()) - 1 : 0; + if (previous) { + for (int i = int(hits.size()) - 1; i >= 0; --i) + if (hits[i] < current || (includeCurrent && hits[i] == current)) { hit = i; break; } + } else { + for (int i = 0; i < hits.size(); ++i) + if (hits[i] > current || (includeCurrent && hits[i] == current)) { hit = i; break; } + } + auto *target = items[hits[hit]]; + if (!tree_.selectItem(target)) { + status_.setMessage("Could not select match. F4: change search"); + redraw(); + return; + } + status_.setMessage(display(QStringLiteral("Match %1/%2. F3: next; F6: previous") + .arg(hit + 1).arg(hits.size()))); + redraw(); + } + void properties() + { + const auto *item = tree_.getCurrentItem(); + if (!item) return; + const int index = item->getData(); + if (index < 0 || index >= inventory_.devices.size()) return; + modalOpen_ = true; + showProperties(inventory_.devices.at(index), this); + modalOpen_ = false; + } + void populate() + { + FString title = connection_ ? "Devices by connection" : "Devices by type"; + if (showAll_) title += " (All Devices)"; + setText(title); + menu_.setState(connection_, showAll_); + switch_.setText(connection_ ? "By &type" : "By &connection"); + const bool editing = filter_.hasFocus(); + const int count = projection_.rebuild(inventory_, connection_, query_, showAll_); + if (editing) filter_.setFocus(); + status_.setMessage(display(query_.isEmpty() ? QStringLiteral("F4: filter; Enter: properties") + : QStringLiteral("%1 matches. Esc: clear filter").arg(count))); + redraw(); + } + void adjustSize() override + { + const auto w = std::max(40, getDesktopWidth()); + const auto h = std::max(12, getDesktopHeight() > 1 ? getDesktopHeight() - 1 : 12); + setGeometry(FPoint{1, 1}, FSize{w, h}, false); + filter_.setGeometry(FPoint{10, 2}, FSize{w - 13, 1}); + // Place the tree's unused header on the outer frame when unfiltered. + // Its native bottom scrollbar sits immediately above the buttons. + tree_.setGeometry(FPoint{2, filterVisible_ ? 5 : 2}, + FSize{w - 2, h - (filterVisible_ ? 6 : 3)}); + treeCaption_.setGeometry(FPoint{3, 2}, + FSize{std::min(w - 5, treeCaption_.getText().getLength()), 1}); + switch_.setGeometry(FPoint{2, int(h) - 3}, FSize{20, 1}); + refresh_.setGeometry(FPoint{25, int(h) - 3}, FSize{12, 1}); + quit_.setGeometry(FPoint{int(w) - 13, int(h) - 3}, FSize{10, 1}); + FDialog::adjustSize(); + } + void initLayout() override { adjustSize(); FDialog::initLayout(); } + Inventory inventory_; + bool connection_ = false; + bool showAll_ = false; + int systemTimer_ = 0; + bool modalOpen_ = false; + bool filterVisible_ = false; + QString query_; + FLineEdit filter_{this}; + DeviceTree tree_{this}; + FLabel treeCaption_{" Device (Enter: expand/collapse branch; properties for leaves) ", this}; + TreeProjection projection_{tree_}; + SystemSnapshot systemSnapshot_; + BrowserMenu menu_; + FButton switch_{this}; + FButton refresh_{"&Refresh", this}; + FButton quit_{"&Quit", this}; + FStatusBar status_{this}; + FStatusKey viewKey_{FKey::F2, "View", &status_}; + FStatusKey findKey_{FKey::F4, "Filter", &status_}; + FStatusKey nextKey_{FKey::F3, "Next", &status_}; + FStatusKey previousKey_{FKey::F6, "Previous", &status_}; + FStatusKey refreshKey_{FKey::F5, "Refresh", &status_}; + FStatusKey menuKey_{FKey::F9, "Menu", &status_}; + FStatusKey quitKey_{FKey::F10, "Quit", &status_}; +}; +} + +int runBrowser(Inventory inventory, finalcut::FApplication &app) +{ + Browser browser(std::move(inventory), &app); + finalcut::FWidget::setMainWidget(&browser); + browser.show(); + return app.exec(); +} diff --git a/src/tui/browser.h b/src/tui/browser.h new file mode 100644 index 0000000..749cb3f --- /dev/null +++ b/src/tui/browser.h @@ -0,0 +1,4 @@ +#pragma once +#include "inventory.h" +#include +int runBrowser(Inventory inventory, finalcut::FApplication &app); diff --git a/src/tui/browsermenu.cpp b/src/tui/browsermenu.cpp new file mode 100644 index 0000000..077230f --- /dev/null +++ b/src/tui/browsermenu.cpp @@ -0,0 +1,49 @@ +#include "browsermenu.h" +#include "systemreport.h" +#include "terminaltext.h" +#include + +using namespace finalcut; + +BrowserMenu::BrowserMenu(FWidget *parent, std::function selectView, + std::function showAll, std::function systemInformation) : FMenu("View", parent) +{ + separator_.setSeparator(); + systemSeparator_.setSeparator(); + system_.setText(display(systemInformationTitle())); + system_.addCallback("clicked", std::move(systemInformation)); + type_.addCallback("clicked", [selectView] { selectView(false); }); + connection_.addCallback("clicked", [selectView] { selectView(true); }); + all_.addCallback("clicked", [this, showAll = std::move(showAll)] { + showAll(all_.isChecked()); + }); +} + +void BrowserMenu::setState(bool connection, bool allDevices) +{ + if (connection) { + type_.unsetChecked(); + connection_.setChecked(); + } else { + connection_.unsetChecked(); + type_.setChecked(); + } + if (allDevices) all_.setChecked(); + else all_.unsetChecked(); +} + +void BrowserMenu::open() +{ + if (isShown()) return; + // Follow FINAL CUT's native dialog-menu opening path. Its close path + // restores the parent dialog and its last focused control, also on Escape. + setPos(FPoint{getParentWidget()->getTermX() + 1, + getParentWidget()->getTermY() + 1}); + setOpenMenu(this); + setVisible(); + show(); + raiseWindow(); + selectFirstItem(); + if (auto *item = getSelectedItem()) item->setFocus(); + redraw(); +} diff --git a/src/tui/browsermenu.h b/src/tui/browsermenu.h new file mode 100644 index 0000000..f828357 --- /dev/null +++ b/src/tui/browsermenu.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +// On-demand native menu: consumes no permanent terminal row. +class BrowserMenu final : public finalcut::FMenu { +public: + BrowserMenu(finalcut::FWidget *parent, std::function selectView, + std::function showAll, std::function systemInformation); + void setState(bool connection, bool allDevices); + void open(); +private: + finalcut::FRadioMenuItem type_{"Devices by &type", this}; + finalcut::FRadioMenuItem connection_{"Devices by &connection", this}; + finalcut::FMenuItem separator_{this}; + finalcut::FCheckMenuItem all_{"&All Devices", this}; + finalcut::FMenuItem systemSeparator_{this}; + finalcut::FMenuItem system_{this}; +}; diff --git a/src/tui/devicetree.cpp b/src/tui/devicetree.cpp new file mode 100644 index 0000000..fa9de29 --- /dev/null +++ b/src/tui/devicetree.cpp @@ -0,0 +1,56 @@ +#include "devicetree.h" + +using namespace finalcut; + +void DeviceTree::onKeyPress(FKeyEvent *event) +{ + auto *item = getCurrentItem(); + if ((event->key() == FKey::Enter || event->key() == FKey::Return) + && item && item->hasChildren()) { + if (item->isExpand()) item->collapse(); + else item->expand(); + // Recompute the native viewport and scrollbars, including when the + // list is horizontally scrolled (Left would otherwise only scroll). + FListView::adjustSize(); + redraw(); + event->accept(); + return; + } + // Native activation emits "clicked", opening properties for leaf devices. + FListView::onKeyPress(event); +} + +void DeviceTree::navigate(FKey key) +{ + // firstPos()/stepForward() are private in released FINAL CUT versions. + // Use the widget's normal keyboard handler to update selection and scrolling. + FKeyEvent event(Event::KeyPress, key); + FListView::onKeyPress(&event); +} + +bool DeviceTree::selectItem(FListViewItem *item, bool focus) +{ + if (!item) return false; + FObject *owner = item->getParent(); + while (owner && owner != this) owner = owner->getParent(); + if (!owner) return false; + + if (focus) setFocus(); + // Reset before expanding branches changes the visible row positions. + navigate(FKey::Home); + for (auto *ancestor = item->getParent(); ancestor != this; + ancestor = ancestor->getParent()) + static_cast(ancestor)->expand(); + redraw(); + + // Native Down events maintain FINAL CUT's viewport and scrollbar state. + // Bound traversal and stop if the widget cannot advance. + const auto count = getCount(); + for (std::size_t i = 0; i < count; ++i) { + auto *current = getCurrentItem(); + if (current == item) return true; + navigate(FKey::Down); + if (getCurrentItem() == current) break; + } + return getCurrentItem() == item; +} diff --git a/src/tui/devicetree.h b/src/tui/devicetree.h new file mode 100644 index 0000000..97b2f25 --- /dev/null +++ b/src/tui/devicetree.h @@ -0,0 +1,16 @@ +#pragma once +#include + +// Native tree navigation shared by search and future selection restoration. +class DeviceTree final : public finalcut::FListView { +public: + explicit DeviceTree(finalcut::FWidget *parent) : FListView(parent) {} + bool selectItem(finalcut::FListViewItem *item, bool focus = true); +protected: + void onKeyPress(finalcut::FKeyEvent *event) override; +private: + // The browser provides the surrounding frame. Keep native item and + // scrollbar rendering; suppress only the redundant inner border. + void drawBorder() override {} + void navigate(finalcut::FKey key); +}; diff --git a/src/tui/inventoryfilter.cpp b/src/tui/inventoryfilter.cpp new file mode 100644 index 0000000..4e6003d --- /dev/null +++ b/src/tui/inventoryfilter.cpp @@ -0,0 +1,19 @@ +#include "inventoryfilter.h" +#include "devicelabel.h" + +// Snapshot-only matching: do not trigger deep collectors or filesystem reads. +bool matches(const Device &d, const QString &query) +{ + const auto contains = [&query](const QString &text) { + return text.contains(query, Qt::CaseInsensitive); + }; + for (const QString &text : {deviceDisplayName(d, true), d.name, d.sysname, + d.path, d.parentPath, d.driver, d.driverModule, d.subsystem, + d.category, categoryLabel(d.category)}) + if (contains(text)) return true; + for (auto it = d.properties.cbegin(); it != d.properties.cend(); ++it) + if (contains(it.value())) return true; + for (auto it = d.attributes.cbegin(); it != d.attributes.cend(); ++it) + if (it->state == ReadState::Available && contains(it->value)) return true; + return false; +} diff --git a/src/tui/inventoryfilter.h b/src/tui/inventoryfilter.h new file mode 100644 index 0000000..cd019a1 --- /dev/null +++ b/src/tui/inventoryfilter.h @@ -0,0 +1,3 @@ +#pragma once +#include "device.h" +bool matches(const Device &device, const QString &query); diff --git a/src/tui/main.cpp b/src/tui/main.cpp new file mode 100644 index 0000000..d0a809c --- /dev/null +++ b/src/tui/main.cpp @@ -0,0 +1,18 @@ +#include "browser.h" +#include +#include +#include + +int main(int argc, char **argv) +{ + // QtCore supplies synchronous utilities and translation context only. + // FINAL CUT exclusively owns terminal setup and event dispatch. + QCoreApplication core(argc, argv); + Inventory inventory = collectInventory(); + if (!inventory.error.isEmpty()) { + std::cerr << inventory.error.toStdString() << '\n'; + return 1; + } + finalcut::FApplication app(argc, argv); + return runBrowser(std::move(inventory), app); +} diff --git a/src/tui/modalcombobox.cpp b/src/tui/modalcombobox.cpp new file mode 100644 index 0000000..2ef6fd3 --- /dev/null +++ b/src/tui/modalcombobox.cpp @@ -0,0 +1,33 @@ +#include "modalcombobox.h" + +using namespace finalcut; + +ModalComboBox::ModalComboBox(FWidget *parent) : FComboBox(parent) +{ + for (auto *child : getChildren()) { + if (!child->isWidget()) continue; + auto *widget = static_cast(child); + if (!widget->isInstanceOf("FDropDownListBox")) continue; + popup_ = widget; + // Public flags API, matching FMenu::setMenuWidget(). Without this, + // FApplication rejects the popup's key, mouse and focus events while + // any dialog is modal. Do not mark it modal or alter the dialog counter. + popup_->setFlags().type.menu_widget = true; + break; + } +} +bool ModalComboBox::isDropDownShown() const +{ + return popup_ && popup_->isShown(); +} +void ModalComboBox::onKeyPress(FKeyEvent *event) +{ + const auto key = event->key(); + if (isDropDownShown() && (key == FKey::Tab || key == FKey::Back_tab)) { + hideDropDown(); + FComboBox::onKeyPress(event); + event->accept(); + return; + } + FComboBox::onKeyPress(event); +} diff --git a/src/tui/modalcombobox.h b/src/tui/modalcombobox.h new file mode 100644 index 0000000..6116a1f --- /dev/null +++ b/src/tui/modalcombobox.h @@ -0,0 +1,14 @@ +#pragma once +#include + +// FINAL CUT 0.9.1's dropdown window needs the same modal-event exemption as +// native menus. Keep this compatibility adjustment out of individual dialogs. +class ModalComboBox : public finalcut::FComboBox { +public: + explicit ModalComboBox(finalcut::FWidget *parent); + bool isDropDownShown() const; +protected: + void onKeyPress(finalcut::FKeyEvent *event) override; +private: + finalcut::FWidget *popup_ = nullptr; // Owned by FComboBox for its lifetime. +}; diff --git a/src/tui/propertiesdialog.cpp b/src/tui/propertiesdialog.cpp new file mode 100644 index 0000000..dcdd555 --- /dev/null +++ b/src/tui/propertiesdialog.cpp @@ -0,0 +1,108 @@ +#include "propertiesdialog.h" +#include "propertypages.h" +#include "propertypagecontrol.h" +#include "propertiesstatus.h" +#include "terminaltext.h" +#include "devicelabel.h" +#include +#include + +namespace { +using namespace finalcut; +class Properties final : public FDialog { +public: + Properties(const Device &device, FWidget *parent) + : FDialog(parent), device_(device), entries_(commonPropertyEntries(inventoryCommonProperties(device))) + { + setModal(); + general_.addCallback("clicked", [this] { setPage(0); }); + driver_.addCallback("clicked", [this] { setPage(1); }); + details_.addCallback("clicked", [this] { setPage(2); }); + close_.addCallback("clicked", [this] { close(); }); + const std::array controls {&text_, &general_, &driver_, &details_, &close_}; + for (auto *control : controls) { + control->addCallback("next-page", [this] { setPage((page_ + 1) % 3); }); + control->addCallback("previous-page", [this] { setPage((page_ + 2) % 3); }); + } + setPage(0); + } +private: + void setPage(int page) + { + page_ = page; + const std::array titles {"General", "Driver", "Details"}; + setText(display(deviceDisplayName(device_) + " - " + titles.at(page))); + const PropertyPage content = page == 2 ? inventoryDetailsPage(device_) : commonPropertyPage(entries_, page); + const QString &text = content.text; + preferredHeight_ = std::clamp(static_cast(text.count('\n')) + 9, 12, 28); + text_.setText(display(text)); + // FINAL CUT 0.9.1 supports native line highlights: no custom drawing. + if (!FTerm::isMonochron()) { + const auto background = text_.getBackgroundColor() == FColor::White + ? FColor::LightGray : FColor::White; + const FColorPair stripe{FColor::Black, background}; + for (int line : content.shadedLines) + text_.addHighlight(static_cast(line), FTextView::FTextHighlight{0, stripe}); + } + text_.scrollToBegin(); + general_.setEnable(page != 0); + driver_.setEnable(page != 1); + details_.setEnable(page != 2); + if (isShown()) { + adjustSize(); + text_.setFocus(); + } + redraw(); + } + void adjustSize() override + { + const auto desktopWidth = getDesktopWidth(); + const auto desktopHeight = getDesktopHeight(); + const auto w = std::min(100, std::max(50, + desktopWidth > 4 ? desktopWidth - 4 : 50)); + const auto h = std::min(preferredHeight_, std::max(10, + desktopHeight > 4 ? desktopHeight - 4 : 10)); + const int x = desktopWidth > w ? int((desktopWidth - w) / 2) + 1 : 1; + const int y = desktopHeight > h ? int((desktopHeight - h) / 2) + 1 : 1; + if (isShown()) { + // FDialog restores uncovered terminal cells when resized or moved. + // Inherited setGeometry bypasses that restoration in FINAL CUT 0.9.1. + // Suppress recursive adjustSize(): children are laid out below. + FDialog::setSize(FSize{w, h}, false); + FDialog::setPos(FPoint{x, y}, false); + } else { + setGeometry(FPoint{x, y}, FSize{w, h}, false); + } + text_.setGeometry(FPoint{2, 2}, FSize{w - 4, h - 6}); + general_.setGeometry(FPoint{2, int(h) - 3}, FSize{11, 1}); + driver_.setGeometry(FPoint{15, int(h) - 3}, FSize{10, 1}); + details_.setGeometry(FPoint{27, int(h) - 3}, FSize{11, 1}); + close_.setGeometry(FPoint{int(w) - 12, int(h) - 3}, FSize{10, 1}); + FDialog::adjustSize(); + } + void initLayout() override + { + adjustSize(); + FDialog::initLayout(); + text_.setFocus(); + } + const Device device_; + const QVector entries_; + std::size_t preferredHeight_ = 12; + int page_ = 0; + PropertyPageControl text_{this}; + PropertyPageControl general_{"&General", this}; + PropertyPageControl driver_{"&Driver", this}; + PropertyPageControl details_{"De&tails", this}; + PropertyPageControl close_{"&Close", this}; + PropertiesStatus status_{FWidget::getStatusBar(), [this](int step) { + setPage((page_ + step + 3) % 3); + }}; +}; +} + +void showProperties(const Device &device, finalcut::FWidget *parent) +{ + Properties dialog(device, parent); + dialog.exec(); +} diff --git a/src/tui/propertiesdialog.h b/src/tui/propertiesdialog.h new file mode 100644 index 0000000..af66f64 --- /dev/null +++ b/src/tui/propertiesdialog.h @@ -0,0 +1,4 @@ +#pragma once +#include "device.h" +#include +void showProperties(const Device &device, finalcut::FWidget *parent); diff --git a/src/tui/propertiesstatus.cpp b/src/tui/propertiesstatus.cpp new file mode 100644 index 0000000..03f2a6c --- /dev/null +++ b/src/tui/propertiesstatus.cpp @@ -0,0 +1,36 @@ +#include "propertiesstatus.h" + +using namespace finalcut; + +PropertiesStatus::PropertiesStatus(FStatusBar *bar, std::function step) : bar_(bar) +{ + if (!bar_) return; + message_ = bar_->getMessage(); + for (std::size_t i = 1; i <= bar_->getCount(); ++i) + saved_.push_back(bar_->getStatusKey(static_cast(i))); + for (auto *key : saved_) { + bar_->delAccelerator(key); + // insert() installs a bar-owned callback; remove it before reinserting + // later, retaining the browser's own callback without duplication. + // FINAL CUT 0.9.1's pointer constraint does not decay references. + key->delCallback("activate", static_cast(bar_)); + } + // clear() leaves the key objects and their connected-bar pointers intact. + bar_->clear(); + previous_ = std::make_unique(FKey::F7, "Previous", bar_); + next_ = std::make_unique(FKey::F8, "Next", bar_); + previous_->addCallback("activate", [step] { step(-1); }); + next_->addCallback("activate", [step] { step(1); }); + bar_->clearMessage(); + bar_->redraw(); +} + +PropertiesStatus::~PropertiesStatus() +{ + if (!bar_) return; + next_.reset(); + previous_.reset(); + for (auto *key : saved_) bar_->insert(key); + bar_->setMessage(message_); + bar_->redraw(); +} diff --git a/src/tui/propertiesstatus.h b/src/tui/propertiesstatus.h new file mode 100644 index 0000000..5b547ad --- /dev/null +++ b/src/tui/propertiesstatus.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include +#include + +// Temporarily use the existing application status bar for a modal dialog. +// Preserve the main window's key objects, callbacks and accelerator ownership. +class PropertiesStatus final { +public: + PropertiesStatus(finalcut::FStatusBar *bar, std::function step); + ~PropertiesStatus(); + PropertiesStatus(const PropertiesStatus &) = delete; + PropertiesStatus &operator=(const PropertiesStatus &) = delete; +private: + finalcut::FStatusBar *bar_; + finalcut::FString message_; + std::vector saved_; + std::unique_ptr previous_; + std::unique_ptr next_; +}; diff --git a/src/tui/propertypagecontrol.h b/src/tui/propertypagecontrol.h new file mode 100644 index 0000000..786b6cf --- /dev/null +++ b/src/tui/propertypagecontrol.h @@ -0,0 +1,21 @@ +#pragma once +#include + +// Reserve F7/F8 for page cycling within tabbed read-only dialogs. +// All other input keeps the native text-view/button behavior. +template +class PropertyPageControl final : public Widget { +public: + using Widget::Widget; +protected: + void onKeyPress(finalcut::FKeyEvent *event) override + { + const auto key = event->key(); + if (key == finalcut::FKey::F7 || key == finalcut::FKey::F8) { + event->accept(); + this->emitCallback(key == finalcut::FKey::F8 ? "next-page" : "previous-page"); + return; + } + Widget::onKeyPress(event); + } +}; diff --git a/src/tui/propertypages.cpp b/src/tui/propertypages.cpp new file mode 100644 index 0000000..260bb9f --- /dev/null +++ b/src/tui/propertypages.cpp @@ -0,0 +1,94 @@ +#include "propertypages.h" +#include "terminaltext.h" +#include +#include + +CommonProperties inventoryCommonProperties(const Device &device) +{ + CommonProperties result; + result.device = device; + // Leave state Unavailable: the shared GUI formatter will correctly describe + // presence in the inventory, rather than assert a verified property read. + for (auto it = device.properties.cbegin(); it != device.properties.cend(); ++it) { + result.values.insert("udev/" + it.key(), {ReadState::Available, it.value(), 0}); + result.sources.insert("udev/" + it.key(), device.propertySources.value(it.key())); + } + for (auto it = device.attributes.cbegin(); it != device.attributes.cend(); ++it) { + // module_description can be an unbound alias candidate; never promote + // this naming hint into observed driver metadata. + if (it.key() == "module_description") continue; + result.values.insert("sysfs/" + it.key(), it.value()); + result.sources.insert("sysfs/" + it.key(), device.path + '/' + it.key()); + } + if (!device.driver.isEmpty()) { + result.values.insert("driver/name", {ReadState::Available, device.driver, 0}); + result.sources.insert("driver/name", device.path + "/driver"); + } + if (!device.driverModule.isEmpty()) { + result.values.insert("module/name", {ReadState::Available, device.driverModule, 0}); + result.sources.insert("module/name", device.path + "/driver/module"); + } + return result; +} + +namespace { +struct Row { QString label; QString value; bool section = false; }; +PropertyPage table(const QVector &rows) +{ + std::size_t width = 0; + for (const Row &row : rows) + if (!row.section) width = std::max(width, finalcut::getColumnWidth(display(row.label))); + PropertyPage result; + int lineNumber = 0; + int fieldNumber = 0; + for (const Row &row : rows) { + if (row.section) { + result.text += '\n' + row.label + '\n'; + lineNumber += 2; + fieldNumber = 0; + continue; + } + const QString value = row.value.isEmpty() ? QStringLiteral("Unavailable") : row.value; + const auto lines = value.split('\n'); + bool first = true; + for (const QString &line : lines) { + const QString label = first ? row.label : QString(); + const auto cells = finalcut::getColumnWidth(display(label)); + result.text += label + QString(static_cast(width - cells), QLatin1Char(' ')) + + " " + line + '\n'; + if (fieldNumber % 2) result.shadedLines.append(lineNumber); + ++lineNumber; + first = false; + } + ++fieldNumber; + } + return result; +} +} + +PropertyPage commonPropertyPage(const QVector &entries, int tab) +{ + QVector rows; + for (const PropertyEntry &entry : entries) + if (entry.tab == tab) rows.append({entry.label, entry.value}); + return table(rows); +} + +PropertyPage inventoryDetailsPage(const Device &device) +{ + QVector rows; + rows.append({"Full kernel path", device.path}); + rows.append({"Parent device path", device.parentPath}); + rows.append({"Original name", device.name}); + rows.append({"Name source", device.nameSource}); + rows.append({"Kernel name", device.sysname}); + rows.append({"Device type", device.devtype}); + rows.append({"Udev properties", {}, true}); + auto keys = device.properties.keys(); + std::sort(keys.begin(), keys.end()); + for (const QString &key : keys) rows.append({key, device.properties.value(key)}); + keys = device.attributes.keys(); + std::sort(keys.begin(), keys.end()); + for (const QString &key : keys) rows.append({key, propertyReadValue(device.attributes.value(key))}); + return table(rows); +} diff --git a/src/tui/propertypages.h b/src/tui/propertypages.h new file mode 100644 index 0000000..40862ca --- /dev/null +++ b/src/tui/propertypages.h @@ -0,0 +1,11 @@ +#pragma once +#include "commonproperties.h" + +// Adapt the inventory without claiming that on-demand properties were read. +CommonProperties inventoryCommonProperties(const Device &device); +struct PropertyPage { + QString text; + QVector shadedLines; // Physical lines of alternating logical fields. +}; +PropertyPage commonPropertyPage(const QVector &entries, int tab); +PropertyPage inventoryDetailsPage(const Device &device); diff --git a/src/tui/systemdialog.cpp b/src/tui/systemdialog.cpp new file mode 100644 index 0000000..d972f91 --- /dev/null +++ b/src/tui/systemdialog.cpp @@ -0,0 +1,196 @@ +#include "systemdialog.h" +#include "systemsnapshot.h" +#include "modalcombobox.h" +#include "systempages/pages.h" +#include "propertypagecontrol.h" +#include "propertiesstatus.h" +#include "terminaltext.h" +#include +#include + +namespace { +using namespace finalcut; +class SystemTextView : public FTextView { +public: + using FTextView::FTextView; +private: + void drawBorder() override {} // Native content/scrollbars, no nested box. +}; +void setContent(FTextView &view, const PropertyPage &content) +{ + view.setText(display(content.text)); + if (!FTerm::isMonochron()) { + const auto background = view.getBackgroundColor() == FColor::White ? FColor::LightGray : FColor::White; + const FColorPair stripe{FColor::Black, background}; + for (int line : content.shadedLines) + view.addHighlight(static_cast(line), FTextView::FTextHighlight{0, stripe}); + } + view.scrollToBegin(); +} +class SystemInformation final : public FDialog { +public: + SystemInformation(SystemSnapshot &snapshot, FWidget *parent) : FDialog(parent), snapshot_(snapshot) + { + setModal(); + text_.ignorePadding(); + ramSummary_.ignorePadding(); + chips_.ignorePadding(); + chips_.unsetEditable(); + chips_.addCallback("row-changed", [this] { + if (updatingChips_ || chips_.getCount() == 0) return; + chip_ = chips_.getItemData(); + if (page_ == 3) { + if (const auto *data = snapshot_.get()) { + // Native row-changed fires while the popup is navigating. + // Update only the table; moving/resizing its owner here + // would disturb the popup's geometry and focus. + setContent(text_, systemRamPage(*data, chip_)); + text_.redraw(); + } + } + }); + chips_.addCallback("clicked", [this] { render(); }); + const auto titles = systemPageTitles(); + for (int i = 0; i < 4; ++i) { + tabs_[i].setText(display(titles.at(i))); + tabs_[i].addCallback("clicked", [this, i] { setPage(i); }); + } + close_.addCallback("clicked", [this] { close(); }); + const std::array controls {&text_, &ramSummary_, &tabs_[0], &tabs_[1], &tabs_[2], &tabs_[3], &chips_, &close_}; + for (auto *control : controls) { + control->addCallback("next-page", [this] { setPage((page_ + 1) % 4); }); + control->addCallback("previous-page", [this] { setPage((page_ + 3) % 4); }); + } + snapshot_.ensure(); + updateChips(); + render(); + if (snapshot_.collecting()) timer_ = addTimer(100); + } + ~SystemInformation() override { delTimer(timer_); } +private: + void onTimer(FTimerEvent *event) override + { + if (snapshot_.poll()) { + updateChips(); + render(); + delTimer(timer_); + timer_ = 0; + } + FDialog::onTimer(event); + } + void updateChips() + { + updatingChips_ = true; + chips_.clear(); + if (const auto *snapshot = snapshot_.get()) { + const auto &devices = snapshot->memoryDevices.devices; + if (chip_ < 0 || chip_ >= devices.size()) chip_ = 0; + for (int i = 0; i < devices.size(); ++i) + chips_.insert(display(systemMemoryTitle(devices.at(i).index)), int(i)); + chips_.setMaxVisibleItems(8); + if (!devices.isEmpty()) chips_.setCurrentItem(static_cast(chip_ + 1)); + } + updatingChips_ = false; + } + void setPage(int page) + { + chips_.hideDropDown(); + page_ = page; + render(); + if (isShown()) text_.setFocus(); + } + void render() + { + setText(display(systemInformationTitle() + " - " + systemPageTitles().at(page_))); + PropertyPage content; + if (const auto *snapshot = snapshot_.get()) { + switch (static_cast(page_)) { + case SystemPage::OperatingSystem: content = systemOperatingSystemPage(*snapshot); break; + case SystemPage::Motherboard: content = systemMotherboardPage(*snapshot); break; + case SystemPage::Processor: content = systemProcessorPage(*snapshot); break; + case SystemPage::Ram: content = systemRamPage(*snapshot, chip_); break; + } + } else content.text = systemCollectingText(); + setContent(text_, content); + const auto *data = snapshot_.get(); + ramLayout_ = page_ == 3 && data; + ramSummaryHeight_ = 0; + if (ramLayout_) { + const auto summary = systemRamSummaryPage(*data); + setContent(ramSummary_, summary); + ramSummaryHeight_ = static_cast(summary.text.count('\n')) + 2; + ramSummary_.setVisible(); + ramSummary_.setFocusable(true); + if (isShown() && !ramSummary_.isShown()) ramSummary_.show(); + } else { + if (ramSummary_.hasFocus()) text_.setFocus(); + ramSummary_.hide(); + ramSummary_.setVisible(false); + ramSummary_.setFocusable(false); + } + const bool showChips = ramLayout_ && data->memoryDevices.status.state == ReadState::Available + && chips_.getCount() > 0; + preferredHeight_ = std::clamp(static_cast(content.text.count('\n')) + + 7 + ramSummaryHeight_ + (showChips ? 1 : 0), 14, 32); + for (int i = 0; i < 4; ++i) tabs_[i].setEnable(i != page_); + chips_.setEnable(showChips); + chips_.setFocusable(showChips); + if (!showChips && chips_.hasFocus()) text_.setFocus(); + if (showChips) { chips_.setVisible(); if (isShown() && !chips_.isShown()) chips_.show(); } + else { chips_.hide(); chips_.setVisible(false); } + if (isShown()) adjustSize(); + redraw(); + } + void adjustSize() override + { + const auto dw = getDesktopWidth(), dh = getDesktopHeight(); + const auto w = std::min(110, std::max(76, dw > 4 ? dw - 4 : 76)); + const auto h = std::min(preferredHeight_, std::max(12, dh > 4 ? dh - 4 : 12)); + const FSize size{w, h}; + const FPoint pos{dw > w ? int((dw - w) / 2) + 1 : 1, dh > h ? int((dh - h) / 2) + 1 : 1}; + if (isShown()) { FDialog::setSize(size, false); FDialog::setPos(pos, false); } + else setGeometry(pos, size, false); + int x = 1; + const std::array widths {20, 15, 14, 7}; + for (int i = 0; i < 4; ++i) { + tabs_[i].setGeometry(FPoint{x, 1}, FSize{widths[i], 1}); + x += int(widths[i]) + 1; + } + std::size_t bodyTop = 4; + if (ramLayout_) { + ramSummary_.setGeometry(FPoint{2, 4}, FSize{w - 2, ramSummaryHeight_}); + if (chips_.isVisible()) { + // Replace the former static Chip heading with the selector. + const auto selectorY = 4 + ramSummaryHeight_; + chips_.setGeometry(FPoint{3, int(selectorY)}, FSize{26, 1}); + bodyTop = selectorY + 1; // Its shadow shares the unused text header. + } else bodyTop += ramSummaryHeight_ - 1; + } + text_.setGeometry(FPoint{2, int(bodyTop)}, FSize{w - 2, h > bodyTop + 1 ? h - bodyTop - 1 : 3}); + close_.setGeometry(FPoint{int(w) - 12, int(h) - 3}, FSize{10, 1}); + FDialog::adjustSize(); + } + void initLayout() override { adjustSize(); FDialog::initLayout(); text_.setFocus(); } + SystemSnapshot &snapshot_; + int page_ = 0; + int chip_ = 0; + int timer_ = 0; + bool updatingChips_ = false; + bool ramLayout_ = false; + std::size_t ramSummaryHeight_ = 0; + std::size_t preferredHeight_ = 14; + PropertyPageControl text_{this}; + PropertyPageControl ramSummary_{this}; + std::array, 4> tabs_ { + PropertyPageControl{this}, PropertyPageControl{this}, + PropertyPageControl{this}, PropertyPageControl{this}}; + PropertyPageControl chips_{this}; + PropertyPageControl close_{"&Close", this}; + PropertiesStatus status_{FWidget::getStatusBar(), [this](int step) { setPage((page_ + step + 4) % 4); }}; +}; +} +void showSystemInformation(SystemSnapshot &snapshot, finalcut::FWidget *parent) +{ + SystemInformation dialog(snapshot, parent); + dialog.exec(); +} diff --git a/src/tui/systemdialog.h b/src/tui/systemdialog.h new file mode 100644 index 0000000..228b0af --- /dev/null +++ b/src/tui/systemdialog.h @@ -0,0 +1,4 @@ +#pragma once +#include +class SystemSnapshot; +void showSystemInformation(SystemSnapshot &snapshot, finalcut::FWidget *parent); diff --git a/src/tui/systempages/motherboardpage.cpp b/src/tui/systempages/motherboardpage.cpp new file mode 100644 index 0000000..9df4503 --- /dev/null +++ b/src/tui/systempages/motherboardpage.cpp @@ -0,0 +1,23 @@ +#include "report.h" + +PropertyPage systemMotherboardPage(const SystemProperties &snapshot) +{ + SystemFields fields; + for (const auto &field : systemFields(SystemPage::Motherboard)) + fields.append({field.second, systemMotherboardValue(snapshot.values.value(field.first))}); + SystemReport report; + report.fields(fields); + const auto &mok = snapshot.mok; + if (mok.status.state != ReadState::Available || mok.certificates.isEmpty()) { + report.fields({{systemMokTitle(), systemMokText(snapshot)}}); + } else { + int index = 0; + for (const auto &certificate : mok.certificates) { + const auto formatted = systemCertificateReport(certificate, ++index, mok.certificates.size()); + report.heading(formatted.title); + report.fields(formatted.fields); + } + if (mok.otherSignatures) report.heading(systemMokOmitted(mok.otherSignatures)); + } + return report.take(); +} diff --git a/src/tui/systempages/operatingsystempage.cpp b/src/tui/systempages/operatingsystempage.cpp new file mode 100644 index 0000000..8a96ee8 --- /dev/null +++ b/src/tui/systempages/operatingsystempage.cpp @@ -0,0 +1,11 @@ +#include "report.h" + +PropertyPage systemOperatingSystemPage(const SystemProperties &snapshot) +{ + SystemFields fields; + for (const auto &field : systemFields(SystemPage::OperatingSystem)) + fields.append({field.second, systemOsValue(snapshot, field.first, snapshot.values.value(field.first))}); + SystemReport report; + report.fields(fields); + return report.take(); +} diff --git a/src/tui/systempages/pages.h b/src/tui/systempages/pages.h new file mode 100644 index 0000000..52298d3 --- /dev/null +++ b/src/tui/systempages/pages.h @@ -0,0 +1,9 @@ +#pragma once +#include "propertypages.h" +#include "systemreport.h" + +PropertyPage systemOperatingSystemPage(const SystemProperties &snapshot); +PropertyPage systemMotherboardPage(const SystemProperties &snapshot); +PropertyPage systemProcessorPage(const SystemProperties &snapshot); +PropertyPage systemRamSummaryPage(const SystemProperties &snapshot); +PropertyPage systemRamPage(const SystemProperties &snapshot, int chip); diff --git a/src/tui/systempages/processorpage.cpp b/src/tui/systempages/processorpage.cpp new file mode 100644 index 0000000..5268237 --- /dev/null +++ b/src/tui/systempages/processorpage.cpp @@ -0,0 +1,22 @@ +#include "report.h" + +PropertyPage systemProcessorPage(const SystemProperties &snapshot) +{ + SystemReport report; + SystemFields fields; + for (const auto &field : systemFields(SystemPage::Processor)) { + if (field.first == "instruction_sets") { + fields.append({field.second, QStringLiteral("Run `cat /proc/cpuinfo` to find out")}); + continue; + } + const QString value = systemProcessorValue(snapshot, field.first, snapshot.values.value(field.first), true); + if (field.first == "core_groups" || field.first == "cache_topology") { + if (!fields.isEmpty()) { report.fields(fields); fields.clear(); } + QVector rows; + for (const auto &line : value.split('\n')) rows.append(line.split('\t')); + report.table(field.second, rows); + } else fields.append({field.second, value}); + } + if (!fields.isEmpty()) report.fields(fields); + return report.take(); +} diff --git a/src/tui/systempages/rampage.cpp b/src/tui/systempages/rampage.cpp new file mode 100644 index 0000000..6d2ba0a --- /dev/null +++ b/src/tui/systempages/rampage.cpp @@ -0,0 +1,26 @@ +#include "report.h" + +PropertyPage systemRamSummaryPage(const SystemProperties &snapshot) +{ + SystemReport report; + SystemFields summary; + for (const auto &field : systemFields(SystemPage::Ram)) + summary.append({field.second, systemRamSummaryValue(snapshot, field.first)}); + report.fields(summary); + return report.take(); +} + +PropertyPage systemRamPage(const SystemProperties &snapshot, int chip) +{ + SystemReport report; + const auto &memory = snapshot.memoryDevices; + if (memory.status.state != ReadState::Available || memory.devices.isEmpty()) { + report.fields({{systemMemoryHeading(), systemMemoryStatus(memory)}}); + } else if (chip >= 0 && chip < memory.devices.size()) { + QVector rows {systemPropertyHeaders()}; + for (const auto &field : systemMemoryFields()) + rows.append({field.second, systemMemoryValue(field.first, memory.devices.at(chip).values.value(field.first))}); + report.table({}, rows); + } + return report.take(); +} diff --git a/src/tui/systempages/report.cpp b/src/tui/systempages/report.cpp new file mode 100644 index 0000000..7bf4d0c --- /dev/null +++ b/src/tui/systempages/report.cpp @@ -0,0 +1,64 @@ +#include "report.h" +#include "terminaltext.h" +#include + +namespace { +// Wrap long prose/features without clipping values or splitting Unicode text. +QStringList wrap(const QString &text) +{ + QStringList result; + for (QString line : text.split('\n')) { + while (finalcut::getColumnWidth(display(line)) > 88) { + int end = std::min(88, line.size()); + const int space = line.lastIndexOf(' ', end); + if (space <= 0) break; // Unbroken paths/identifiers scroll horizontally. + result.append(line.left(space)); + line = line.mid(space + 1); + } + result.append(line); + } + return result; +} +} +void SystemReport::heading(const QString &text) +{ + if (!result_.text.isEmpty()) { result_.text += '\n'; ++line_; } + if (!text.isEmpty()) { result_.text += text + '\n'; ++line_; } +} +void SystemReport::fields(const SystemFields &fields) +{ + QVector rows; + for (const auto &field : fields) rows.append({field.first, field.second}); + table({}, rows); +} +void SystemReport::table(const QString &title, const QVector &rows) +{ + if (!title.isEmpty()) heading(title); + if (rows.isEmpty()) return; + qsizetype columns = 0; + for (const auto &row : rows) columns = std::max(columns, row.size()); + QVector widths(columns, 0); + for (const auto &row : rows) + for (qsizetype c = 0; c < row.size(); ++c) + for (const auto &line : wrap(row[c])) + widths[c] = std::max(widths[c], finalcut::getColumnWidth(display(line))); + for (qsizetype rowIndex = 0; rowIndex < rows.size(); ++rowIndex) { + QVector cells; + qsizetype height = 1; + for (qsizetype c = 0; c < columns; ++c) { + cells.append(wrap(rows[rowIndex].value(c))); + height = std::max(height, cells.constLast().size()); + } + for (qsizetype y = 0; y < height; ++y) { + for (qsizetype c = 0; c < columns; ++c) { + const QString cell = cells[c].value(y); + result_.text += cell; + if (c + 1 < columns) + result_.text += QString(static_cast(widths[c] - finalcut::getColumnWidth(display(cell))) + 2, QLatin1Char(' ')); + } + result_.text += '\n'; + if (rowIndex % 2) result_.shadedLines.append(line_); + ++line_; + } + } +} diff --git a/src/tui/systempages/report.h b/src/tui/systempages/report.h new file mode 100644 index 0000000..98b897d --- /dev/null +++ b/src/tui/systempages/report.h @@ -0,0 +1,16 @@ +#pragma once +#include "pages.h" +#include + +// Plain aligned tables using terminal-column widths, including multiline cells. +// Rendering and scrolling stay with FINAL CUT's native text view. +class SystemReport final { +public: + void fields(const SystemFields &fields); + void table(const QString &title, const QVector &rows); + void heading(const QString &text); + PropertyPage take() { return std::move(result_); } +private: + PropertyPage result_; + int line_ = 0; +}; diff --git a/src/tui/systemsnapshot.cpp b/src/tui/systemsnapshot.cpp new file mode 100644 index 0000000..d06a6dc --- /dev/null +++ b/src/tui/systemsnapshot.cpp @@ -0,0 +1,34 @@ +#include "systemsnapshot.h" + +SystemSnapshot::~SystemSnapshot() +{ + reader_.requestInterruption(); + reader_.wait(); +} +void SystemSnapshot::ensure() +{ + poll(); + if (!result_ && !busy_) refresh(); +} +void SystemSnapshot::refresh() +{ + result_.reset(); + if (busy_) { pending_ = true; return; } + busy_ = true; + reader_.start(); +} +bool SystemSnapshot::poll() +{ + if (!busy_ || !reader_.isFinished()) return false; + reader_.wait(); + busy_ = false; + if (pending_) { + // A newer application refresh superseded this collection. Keep only + // one worker and collect again before publishing a result. + pending_ = false; + refresh(); + return false; + } + result_ = reader_.takeResult(); + return true; +} diff --git a/src/tui/systemsnapshot.h b/src/tui/systemsnapshot.h new file mode 100644 index 0000000..7e68a11 --- /dev/null +++ b/src/tui/systemsnapshot.h @@ -0,0 +1,20 @@ +#pragma once +#include "systemproperties.h" +#include + +// Worker results are consumed only on the terminal thread. No Qt event loop, +// queued callbacks, or dialog pointers are used. +class SystemSnapshot final { +public: + ~SystemSnapshot(); + void ensure(); + void refresh(); + bool poll(); + bool collecting() const { return busy_; } + const SystemProperties *get() const { return result_ ? &*result_ : nullptr; } +private: + SystemPropertiesReader reader_; + std::optional result_; + bool busy_ = false; + bool pending_ = false; +}; diff --git a/src/tui/terminaltext.cpp b/src/tui/terminaltext.cpp new file mode 100644 index 0000000..2bb9f62 --- /dev/null +++ b/src/tui/terminaltext.cpp @@ -0,0 +1,15 @@ +#include "terminaltext.h" +using finalcut::FString; + +// Never pass terminal control characters from firmware/udev through to rendering. +FString display(QString value) +{ + for (qsizetype i = 0; i < value.size(); ++i) { + const QChar c = value.at(i); + const auto category = c.category(); + if ((category == QChar::Other_Control || category == QChar::Other_Format) + && c != '\n' && c != '\t') + value[i] = QChar(0xfffd); + } + return FString(value.toStdWString()); +} diff --git a/src/tui/terminaltext.h b/src/tui/terminaltext.h new file mode 100644 index 0000000..e1dfad7 --- /dev/null +++ b/src/tui/terminaltext.h @@ -0,0 +1,4 @@ +#pragma once +#include +#include +finalcut::FString display(QString value); diff --git a/src/tui/treeprojection.cpp b/src/tui/treeprojection.cpp new file mode 100644 index 0000000..17ece0a --- /dev/null +++ b/src/tui/treeprojection.cpp @@ -0,0 +1,163 @@ +#include "treeprojection.h" +#include "inventoryfilter.h" +#include "terminaltext.h" +#include "devicelabel.h" +#include +#include +#include +#include +#include + +using namespace finalcut; + +// FINAL CUT returns vector iterators. Retain only item addresses between +// insertions: adding siblings may reallocate the containing vector. +FListViewItem *TreeProjection::insertItem(const FString &label, int index, FListViewItem *parent) +{ + if (!parent) { + const auto inserted = tree_.insert(FStringList{label}, int(index)); + auto *item = static_cast(*inserted); + keys_.insert(item, "computer"); + return item; + } + auto &siblings = parent->getParent()->getChildren(); + const auto position = std::find(siblings.begin(), siblings.end(), parent); + if (position == siblings.end()) + throw std::logic_error("Device tree parent is not attached"); + const auto inserted = tree_.insert(FStringList{label}, int(index), position); + auto *item = static_cast(*inserted); + keys_.insert(item, index >= 0 ? "device:" + inventory_->devices.at(index).path + : "group:" + QString::fromStdWString(label.toWString())); + return item; +} + +void TreeProjection::saveState() +{ + auto &state = states_[connection_ ? 1 : 0]; + state = {}; + state.valid = true; + state.selected = keys_.value(tree_.getCurrentItem()); + for (auto it = keys_.cbegin(); it != keys_.cend(); ++it) + if (it.key()->isExpand()) state.expanded.insert(it.value()); +} + +void TreeProjection::restoreState() +{ + const auto &state = states_[connection_ ? 1 : 0]; + if (!state.valid) return; + FListViewItem *selected = nullptr; + for (auto it = keys_.cbegin(); it != keys_.cend(); ++it) { + if (state.expanded.contains(it.value())) it.key()->expand(); + else it.key()->collapse(); + if (it.value() == state.selected) selected = it.key(); + } + tree_.redraw(); + if (selected) tree_.selectItem(selected, false); +} + +int TreeProjection::rebuild(const Inventory &inventory, bool connection, const QString &query, bool showAll) +{ + // A visibility toggle should retain a selected search result if it survives. + const QString selected = initialized_ && connection_ == connection && showAll_ != showAll + ? keys_.value(tree_.getCurrentItem()) : QString(); + if (initialized_ && query_.isEmpty()) saveState(); + initialized_ = true; + connection_ = connection; + showAll_ = showAll; + query_ = query; + inventory_ = &inventory; + int count = 0; + // Rebuild in projection order, even after a header-click sort. FINAL + // CUT sorts on insertion; disable it before consuming returned positions. + tree_.setColumnSort(-1); + keys_.clear(); + tree_.clear(); + const QString hostname = QSysInfo::machineHostName(); + auto *root = insertItem(display(hostname.isEmpty() ? "This computer" : hostname), -1); + const auto &devices = inventory.devices; + QHash lookup; + QVector order; + for (int i = 0; i < devices.size(); ++i) { lookup.insert(devices[i].path, i); order.append(i); } + std::sort(order.begin(), order.end(), [&](int a, int b) { + const int cmp = QString::localeAwareCompare(devices[a].name, devices[b].name); + return cmp ? cmp < 0 : devices[a].path < devices[b].path; + }); + if (!connection) { + for (const Category &category : categories()) { + FListViewItem *group = nullptr; + bool created = false; + for (int i : order) { + const Device &d = devices[i]; + if ((!showAll && d.hidden) || d.category != QLatin1String(category.id) + || (!query.isEmpty() && !matches(d, query))) continue; + if (!created) { + group = insertItem(display(categoryLabel(d.category)), -1, root); + created = true; + } + insertItem(display(deviceDisplayName(d, true)), i, group); + ++count; + } + } + } else { + const auto parents = connectionParents(devices); + QSet included; + for (const Device &d : devices) { + if (!showAll && d.hidden) continue; + QSet visited; + QString path = d.path; + while (lookup.contains(path) && !visited.contains(path)) { + visited.insert(path); included.insert(path); path = parents.value(path); + } + } + if (!query.isEmpty()) { + QSet filtered; + for (const Device &d : devices) { + if (!included.contains(d.path) || !matches(d, query)) continue; + ++count; + QString path = d.path; + QSet visited; + while (included.contains(path) && !visited.contains(path)) { + visited.insert(path); + filtered.insert(path); + path = parents.value(path); + } + } + included = std::move(filtered); + } + QHash nodes; + QSet building; + FListViewItem *efi = nullptr; + bool haveEfi = false; + std::function ensure = [&](int i) -> FListViewItem* { + const Device &d = devices[i]; + if (nodes.contains(d.path)) return nodes.value(d.path); + building.insert(d.path); + auto parent = root; + const QString path = parents.value(d.path); + if (d.subsystem == "efivarfs") { + if (!haveEfi) { + efi = insertItem("UEFI variables (firmware records)", -1, root); + haveEfi = true; + } + parent = efi; + } else if (included.contains(path) && !building.contains(path)) parent = ensure(lookup.value(path)); + auto *item = insertItem(display(deviceDisplayName(d, true)), i, parent); + nodes.insert(d.path, item); building.remove(d.path); + return item; + }; + for (int i : order) if (included.contains(devices[i].path)) ensure(i); + } + root->expand(); + + if (query.isEmpty()) restoreState(); + else { + for (auto *item : keys_.keys()) item->expand(); + tree_.redraw(); + } + if (!selected.isEmpty()) { + for (auto it = keys_.cbegin(); it != keys_.cend(); ++it) + if (it.value() == selected) { tree_.selectItem(it.key(), false); break; } + } + inventory_ = nullptr; + return count; +} diff --git a/src/tui/treeprojection.h b/src/tui/treeprojection.h new file mode 100644 index 0000000..b2d50b2 --- /dev/null +++ b/src/tui/treeprojection.h @@ -0,0 +1,27 @@ +#pragma once +#include "inventory.h" +#include "devicetree.h" +#include +#include + +// Builds frontend projections and preserves the unfiltered state of each view. +// Keys are paths/group identities; no item pointer survives a rebuild. +class TreeProjection final { +public: + explicit TreeProjection(DeviceTree &tree) : tree_(tree) {} + int rebuild(const Inventory &inventory, bool connection, const QString &query, bool showAll); +private: + struct State { QSet expanded; QString selected; bool valid = false; }; + void saveState(); + void restoreState(); + finalcut::FListViewItem *insertItem(const finalcut::FString &label, int index, + finalcut::FListViewItem *parent = nullptr); + DeviceTree &tree_; + const Inventory *inventory_ = nullptr; // Valid only inside rebuild(). + QHash keys_; + std::array states_; + bool initialized_ = false; + bool connection_ = false; + bool showAll_ = false; + QString query_; +}; diff --git a/src/ui/devicemodel.cpp b/src/ui/devicemodel.cpp index 224eeb1..1581e29 100644 --- a/src/ui/devicemodel.cpp +++ b/src/ui/devicemodel.cpp @@ -1,3 +1,4 @@ +#include "inventory.h" #include "devicemodel.h" #include "icontheme.h" #include "devicelabel.h" @@ -337,17 +338,7 @@ void DeviceModel::rebuild() // Some real sysfs parents (e.g. ata5) have no enumerated device record. // Bridge those gaps to the nearest inventoried directory ancestor. This // changes only the projection; keep recorded parentPath metadata intact. - QHash parents; - for (const Device &d : devices_) { - QString path = d.parentPath; - if (!lookup_.contains(path) && path.startsWith("/sys/devices/") - && d.path.startsWith(path + '/')) { - while (path.startsWith("/sys/devices/") && !lookup_.contains(path)) - path.truncate(path.lastIndexOf('/')); - if (!lookup_.contains(path)) path.clear(); - } - parents.insert(d.path, path); - } + const auto parents = connectionParents(devices_); // Hidden inventory ancestors still provide connection context. QSet included; for (const Device *d : members) { diff --git a/src/ui/systemdialog.cpp b/src/ui/systemdialog.cpp index ee577f4..bf28ff6 100644 --- a/src/ui/systemdialog.cpp +++ b/src/ui/systemdialog.cpp @@ -1,4 +1,5 @@ #include "systemdialog.h" +#include "systemreport.h" #include "systempages/rampage.h" #include "systempages/motherboardpage.h" #include "systempages/processorpage.h" @@ -14,7 +15,7 @@ SystemDialog::SystemDialog(QWidget *parent) : QDialog(parent) { - setWindowTitle(tr("System Information")); + setWindowTitle(systemInformationTitle()); resize(680, 650); auto *layout = new QVBoxLayout(this); tabs_ = new QTabWidget(this); @@ -22,16 +23,16 @@ SystemDialog::SystemDialog(QWidget *parent) : QDialog(parent) tabs->setUsesScrollButtons(true); layout->addWidget(tabs, 1); osPage_ = new SystemOperatingSystemPage(tabs); - tabs->addTab(osPage_, tr("Operating System")); + tabs->addTab(osPage_, systemPageTitles().at(0)); motherboardPage_ = new SystemMotherboardPage(tabs); - tabs->addTab(motherboardPage_, tr("Motherboard")); + tabs->addTab(motherboardPage_, systemPageTitles().at(1)); processorPage_ = new SystemProcessorPage(tabs); - tabs->addTab(processorPage_, tr("Processor")); + tabs->addTab(processorPage_, systemPageTitles().at(2)); ramPage_ = new SystemRamPage(tabs); - tabs->addTab(ramPage_, tr("RAM")); + tabs->addTab(ramPage_, systemPageTitles().at(3)); // Use one font-derived caption width for every section, so values align. int captionWidth = 0; captionWidth = qMax(captionWidth, osPage_->captionWidthHint()); diff --git a/src/ui/systempages/motherboardpage.cpp b/src/ui/systempages/motherboardpage.cpp index 8ab550f..7444629 100644 --- a/src/ui/systempages/motherboardpage.cpp +++ b/src/ui/systempages/motherboardpage.cpp @@ -1,30 +1,13 @@ #include "motherboardpage.h" +#include "systemreport.h" #include "propertypage.h" #include "propertytext.h" #include "propertyreport.h" -#include #include #include #include -#include #include -namespace { -QString motherboardValue(const Attribute &value) -{ - return value.state == ReadState::Available ? value.value : propertyReadValue(value); -} -QVector> motherboardFields() -{ - using D = SystemMotherboardPage; - return {{"manufacturer", D::tr("System manufacturer")}, - {"model", D::tr("System model")}, - {"board_vendor", D::tr("Motherboard manufacturer")}, - {"board_model", D::tr("Motherboard model")}, - {"firmware", D::tr("Firmware version")}, - {"firmware_date", D::tr("Firmware date (reported)")}}; -} -} SystemMotherboardPage::SystemMotherboardPage(QWidget *parent) : QWidget(parent) { const auto page = reportPageLayout(this); @@ -34,7 +17,7 @@ SystemMotherboardPage::SystemMotherboardPage(QWidget *parent) : QWidget(parent) summary_->setColumnStretch(1, 1); page.contents->addWidget(fields); int row = 0; - for (const auto &field : motherboardFields()) { + for (const auto &field : systemFields(SystemPage::Motherboard)) { auto *name = new QLabel(field.second, fields); name->setTextFormat(Qt::PlainText); auto *value = propertyValueLabel(tr("Not collected"), fields); @@ -53,7 +36,7 @@ SystemMotherboardPage::SystemMotherboardPage(QWidget *parent) : QWidget(parent) int SystemMotherboardPage::captionWidthHint() const { int width = 0; - for (const auto &field : motherboardFields()) + for (const auto &field : systemFields(SystemPage::Motherboard)) width = qMax(width, QLabel(field.second).sizeHint().width()); for (const QString &caption : {tr("Owner:"), tr("Issuer:"), tr("Expires:")}) width = qMax(width, QLabel(caption).sizeHint().width()); @@ -69,7 +52,7 @@ void SystemMotherboardPage::setSnapshot(const SystemProperties &snapshot) { snapshot_ = snapshot; for (auto it = fields_.cbegin(); it != fields_.cend(); ++it) { - it.value()->setText(motherboardValue(snapshot_.values.value(it.key()))); + it.value()->setText(systemMotherboardValue(snapshot_.values.value(it.key()))); it.value()->setToolTip(propertyToolTip(snapshot_.sources.value(it.key()))); } rebuildMok(); @@ -77,23 +60,14 @@ void SystemMotherboardPage::setSnapshot(const SystemProperties &snapshot) QString SystemMotherboardPage::copyText() const { QStringList lines; - for (const auto &field : motherboardFields()) - lines.append(field.second + ": " + motherboardValue(snapshot_.values.value(field.first))); - lines.append(tr("MOK certificates") + ": " + mokText()); + for (const auto &field : systemFields(SystemPage::Motherboard)) + lines.append(field.second + ": " + systemMotherboardValue(snapshot_.values.value(field.first))); + lines.append(systemMokTitle() + ": " + mokText()); return lines.join('\n'); } QString SystemMotherboardPage::mokText() const { - const MokCertificates &mok = snapshot_.mok; - if (mok.status.state != ReadState::Available) return propertyReadValue(mok.status); - QStringList certificates; - const auto known = [this](const QString &text) { return text.isEmpty() ? tr("Unavailable") : text; }; - for (const MokCertificate &certificate : mok.certificates) - certificates.append(tr("Owner: %1\nIssuer: %2\nExpires: %3") - .arg(known(certificate.subject), known(certificate.issuer), known(certificate.expires))); - if (certificates.isEmpty()) certificates.append(tr("No X.509 certificates in the exposed MOK list")); - if (mok.otherSignatures) certificates.append(tr("%1 non-certificate signatures omitted").arg(mok.otherSignatures)); - return certificates.join("\n\n"); + return systemMokText(snapshot_); } void SystemMotherboardPage::rebuildMok() { @@ -119,11 +93,10 @@ void SystemMotherboardPage::rebuildMok() message(mokText()); return; } - const auto known = [this](const QString &text) { return text.isEmpty() ? tr("Unavailable") : text; }; int index = 0; for (const MokCertificate &certificate : mok.certificates) { - const QString title = mok.certificates.size() == 1 ? tr("MOK Certificate") - : tr("MOK Certificate %1").arg(++index); + const auto report = systemCertificateReport(certificate, ++index, mok.certificates.size()); + const QString title = report.title; auto *box = new QGroupBox(title, mokForm_->parentWidget()); auto *form = new QGridLayout(box); form->setColumnStretch(1, 1); @@ -139,17 +112,10 @@ void SystemMotherboardPage::rebuildMok() form->addWidget(name, certificateRow, 0, Qt::AlignLeading | Qt::AlignTop); form->addWidget(value, certificateRow++, 1); }; - const QString owner = certificate.subjectDisplay.isEmpty() ? certificate.subject : certificate.subjectDisplay; - const QString issuer = !certificate.subject.isEmpty() && certificate.subject == certificate.issuer - ? tr("Same as owner") - : (certificate.issuerDisplay.isEmpty() ? certificate.issuer : certificate.issuerDisplay); - const QDateTime expires = QDateTime::fromString(certificate.expires, Qt::ISODate); - const QString date = expires.isValid() - ? tr("%1 UTC").arg(QLocale().toString(expires.toUTC(), "d MMM yyyy, HH:mm:ss")) : known(certificate.expires); - field(tr("Owner:"), known(owner), certificate.subject); - field(tr("Issuer:"), known(issuer), certificate.issuer); - field(tr("Expires:"), date, certificate.expires); + field(report.fields.at(0).first, report.fields.at(0).second, certificate.subject); + field(report.fields.at(1).first, report.fields.at(1).second, certificate.issuer); + field(report.fields.at(2).first, report.fields.at(2).second, certificate.expires); } if (mok.otherSignatures) - message(tr("%1 non-certificate signatures omitted").arg(mok.otherSignatures)); + message(systemMokOmitted(mok.otherSignatures)); } diff --git a/src/ui/systempages/operatingsystempage.cpp b/src/ui/systempages/operatingsystempage.cpp index 0518044..8037e70 100644 --- a/src/ui/systempages/operatingsystempage.cpp +++ b/src/ui/systempages/operatingsystempage.cpp @@ -1,14 +1,12 @@ #include "operatingsystempage.h" +#include "systemreport.h" #include "propertypage.h" #include "propertytext.h" #include "propertyreport.h" #include #include #include -#include #include -#include -#include SystemOperatingSystemPage::SystemOperatingSystemPage(QWidget *parent) : QWidget(parent) { @@ -35,20 +33,20 @@ SystemOperatingSystemPage::SystemOperatingSystemPage(QWidget *parent) : QWidget( }; auto *os = form_; - field(os, "hostname", tr("Hostname")); - field(os, "os", tr("Linux distribution")); + field(os, "hostname", systemFieldCaption(SystemPage::OperatingSystem, "hostname")); + field(os, "os", systemFieldCaption(SystemPage::OperatingSystem, "os")); auto *separator = new QFrame(os->parentWidget()); separator->setFrameShape(QFrame::HLine); separator->setFrameShadow(QFrame::Sunken); os->addWidget(separator, os->rowCount(), 0, 1, 2); - field(os, "architecture", tr("Kernel architecture")); - field(os, "kernel", tr("Kernel version")); - field(os, "build", tr("Kernel build")); - field(os, "boot", tr("Boot mode")); - field(os, "secure_boot", tr("Secure Boot")); - field(os, "virtualization", tr("Virtualization environment")); - field(os, "uptime", tr("Uptime")); - field(os, "gpu_rebar", tr("Resizable BAR (GPU)")); + field(os, "architecture", systemFieldCaption(SystemPage::OperatingSystem, "architecture")); + field(os, "kernel", systemFieldCaption(SystemPage::OperatingSystem, "kernel")); + field(os, "build", systemFieldCaption(SystemPage::OperatingSystem, "build")); + field(os, "boot", systemFieldCaption(SystemPage::OperatingSystem, "boot")); + field(os, "secure_boot", systemFieldCaption(SystemPage::OperatingSystem, "secure_boot")); + field(os, "virtualization", systemFieldCaption(SystemPage::OperatingSystem, "virtualization")); + field(os, "uptime", systemFieldCaption(SystemPage::OperatingSystem, "uptime")); + field(os, "gpu_rebar", systemFieldCaption(SystemPage::OperatingSystem, "gpu_rebar")); } int SystemOperatingSystemPage::captionWidthHint() const @@ -63,59 +61,7 @@ void SystemOperatingSystemPage::setCaptionWidth(int width) } QString SystemOperatingSystemPage::displayValue(const QString &key, const Attribute &value) const { - if (key == "virtualization" && value.state == ReadState::Unavailable) - return tr("Unknown (no hypervisor identity reported)"); - if (key == "gpu_rebar") { - if (snapshot_.gpuRebarStatus.state != ReadState::Available) - return propertyReadValue(snapshot_.gpuRebarStatus); - if (snapshot_.gpuRebar.isEmpty()) return tr("No PCI display controller reported"); - const auto size = [this](const Attribute &value) { - if (value.state != ReadState::Available) return propertyReadValue(value); - const qulonglong bytes = value.value.toULongLong(); - if (bytes <= static_cast(std::numeric_limits::max())) - return QLocale().formattedDataSize(static_cast(bytes), 0, QLocale::DataSizeIecFormat); - return tr("%1 bytes").arg(QLocale().toString(bytes)); - }; - QStringList devices; - for (const GpuResizableBar &gpu : snapshot_.gpuRebar) { - QStringList lines {tr("GPU [%1]").arg(gpu.pciAddress)}; - if (gpu.status.state == ReadState::Unavailable) lines.append(tr("Capability: Not exposed by kernel")); - else if (gpu.status.state != ReadState::Available) lines.append(propertyReadValue(gpu.status)); - else { - lines.append(tr("Capability: Supported")); - QList bars = gpu.supportedSizes.keys(); - std::sort(bars.begin(), bars.end()); - for (int bar : bars) { - const Attribute bitmap = gpu.supportedSizes.value(bar); - QString supported = propertyReadValue(bitmap); - if (bitmap.state == ReadState::Available) { - const qulonglong bits = bitmap.value.toULongLong(nullptr, 16); - QStringList sizes; - for (int bit = 0; bit < 44; ++bit) - if (bits & (1ULL << bit)) sizes.append(size({ReadState::Available, QString::number(1ULL << (bit + 20)), 0})); - supported = sizes.join(", "); - } - lines.append(tr("BAR %1 — Current allocation: %2; supported sizes: %3") - .arg(bar).arg(size(gpu.currentSizes.value(bar)), supported)); - } - } - lines.append(tr("BIOS setting: Unknown")); - devices.append(lines.join('\n')); - } - return devices.join("\n\n"); - } - - if (value.state != ReadState::Available) return propertyReadValue(value); - if (key == "secure_boot") return value.value == "enabled" ? tr("Enabled") : tr("Disabled"); - if (key == "boot") - return value.value == "uefi" ? tr("UEFI") - : tr("UEFI not exposed (legacy boot or restricted environment)"); - if (key == "virtualization") return tr("Reported hypervisor: %1").arg(value.value); - if (key == "uptime") { - const qulonglong seconds = value.value.toULongLong(); - return tr("%1 days, %2 hours, %3 minutes").arg(seconds / 86400).arg(seconds / 3600 % 24).arg(seconds / 60 % 60); - } - return value.value; + return systemOsValue(snapshot_, key, value); } void SystemOperatingSystemPage::setSnapshot(const SystemProperties &snapshot) { diff --git a/src/ui/systempages/processorpage.cpp b/src/ui/systempages/processorpage.cpp index 780abb7..a29a485 100644 --- a/src/ui/systempages/processorpage.cpp +++ b/src/ui/systempages/processorpage.cpp @@ -1,4 +1,5 @@ #include "processorpage.h" +#include "systemreport.h" #include "propertypage.h" #include "propertytext.h" #include "propertyreport.h" @@ -9,11 +10,8 @@ #include #include #include -#include #include #include -#include -#include SystemProcessorPage::SystemProcessorPage(QWidget *parent) : QWidget(parent) { @@ -51,12 +49,12 @@ SystemProcessorPage::SystemProcessorPage(QWidget *parent) : QWidget(parent) }; auto *processor = form_; - field(processor, "cpu", tr("CPU model(s)")); - field(processor, "sockets", tr("Sockets")); - field(processor, "cores", tr("Physical cores")); - field(processor, "logical", tr("Logical CPUs")); - field(processor, "smt", tr("SMT/HT")); - const QString featuresCaption = tr("Instruction sets / CPU features"); + field(processor, "cpu", systemFieldCaption(SystemPage::Processor, "cpu")); + field(processor, "sockets", systemFieldCaption(SystemPage::Processor, "sockets")); + field(processor, "cores", systemFieldCaption(SystemPage::Processor, "cores")); + field(processor, "logical", systemFieldCaption(SystemPage::Processor, "logical")); + field(processor, "smt", systemFieldCaption(SystemPage::Processor, "smt")); + const QString featuresCaption = systemFieldCaption(SystemPage::Processor, "instruction_sets"); auto *featuresLabel = new QLabel(featuresCaption, processor->parentWidget()); captionsWidgets_.append(featuresLabel); featuresCopy_ = new QPushButton(tr("Copy"), processor->parentWidget()); @@ -70,8 +68,8 @@ SystemProcessorPage::SystemProcessorPage(QWidget *parent) : QWidget(parent) connect(featuresCopy_, &QPushButton::clicked, this, [this] { QApplication::clipboard()->setText(displayValue("instruction_sets", snapshot_.values.value("instruction_sets"))); }); - field(processor, "core_groups", tr("Core groups (online CPUs)")); - field(processor, "cache_topology", tr("Cache hierarchy (online CPUs)")); + field(processor, "core_groups", systemFieldCaption(SystemPage::Processor, "core_groups")); + field(processor, "cache_topology", systemFieldCaption(SystemPage::Processor, "cache_topology")); } int SystemProcessorPage::captionWidthHint() const { @@ -85,151 +83,7 @@ void SystemProcessorPage::setCaptionWidth(int width) } QString SystemProcessorPage::displayValue(const QString &key, const Attribute &value, bool compact) const { - if (key == "logical") { - const Attribute online = snapshot_.values.value("online"); - if (value.state == ReadState::Available && online.state == ReadState::Available && value.value == online.value) - return value.value; - return tr("%1 present; %2 online").arg(propertyReadValue(value), propertyReadValue(online)); - } - if (value.state != ReadState::Available) return propertyReadValue(value); - if (key == "smt") { - if (value.value == "on") return tr("Enabled (kernel policy)"); - if (value.value == "off") return tr("Disabled"); - if (value.value == "forceoff") return tr("Disabled by system policy"); - if (value.value == "notsupported") return tr("Not supported"); - if (value.value == "notimplemented") return tr("Status reporting not implemented by kernel"); - return tr("Unknown (%1)").arg(value.value); - } - // Tab-separated cells are shared by the native views and Copy All. - const auto tableText = [](const QVector &rows) { - QStringList lines; - for (const auto &row : rows) lines.append(row.join('\t')); - return lines.join('\n'); - }; - if (key == "core_groups") { - QVector rows {{tr("Core group"), tr("Cores / threads"), tr("Maximum frequency"), tr("Logical CPUs")}}; - const Attribute sockets = snapshot_.values.value("sockets"); - const bool showPackage = sockets.state != ReadState::Available || sockets.value.toUInt() != 1; - for (const CpuCoreGroup &group : snapshot_.cpuGroups) { - if (group.members.isEmpty()) continue; - const auto &first = group.members.constFirst(); - const QString kind = first.kind == 1 ? tr("Performance cores") - : first.kind == 2 ? tr("Efficiency cores") : tr("Cores (type unspecified)"); - QSet cpus; - QMap threads; - qulonglong minimum = std::numeric_limits::max(), maximum = 0; - Attribute frequencyError; - bool frequencyComplete = true; - for (const auto &member : group.members) { - cpus.insert(member.cpu); - ++threads[member.core]; - if (member.maximumKhz.state != ReadState::Available) { - if (frequencyComplete) frequencyError = member.maximumKhz; - frequencyComplete = false; - } else { - const qulonglong khz = member.maximumKhz.value.toULongLong(); - minimum = qMin(minimum, khz); - maximum = qMax(maximum, khz); - } - } - const auto ghz = [](qulonglong khz) { return QLocale().toString(khz / 1000000.0, 'g', 6); }; - const QString frequency = !frequencyComplete ? propertyReadValue(frequencyError) - : minimum == maximum ? tr("%1 GHz").arg(ghz(maximum)) - : tr("%1–%2 GHz").arg(ghz(minimum), ghz(maximum)); - const QString name = showPackage ? tr("%1 (package %2)").arg(kind, first.package) : kind; - rows.append({name, tr("%1 / %2").arg(threads.size()).arg(cpus.size()), frequency, cpuListText(cpus)}); - } - return rows.size() == 1 ? tr("Unavailable") : tableText(rows); - } - if (key == "cache_topology") { - // Keep each sharing domain explicit; aggregate equal-sized cache instances. - QMap> kinds; - for (const auto &cache : snapshot_.cpuCaches) - kinds[QStringLiteral("%1/%2/%3").arg(cache.level).arg(cache.type).arg(cache.bytes)].append(cache); - QMap cpuCores; - QMap> coreCpus; - QSet onlineCpus; - for (const auto &group : snapshot_.cpuGroups) { - for (const auto &member : group.members) { - cpuCores.insert(member.cpu, member.core); - coreCpus[member.core].insert(member.cpu); - onlineCpus.insert(member.cpu); - } - } - QVector rows {{tr("Cache"), tr("Configuration"), tr("Sharing (logical CPUs)")}}; - for (auto it = kinds.cbegin(); it != kinds.cend(); ++it) { - auto instances = it.value(); - std::sort(instances.begin(), instances.end(), [](const auto &a, const auto &b) { - return *std::min_element(a.cpus.cbegin(), a.cpus.cend()) - < *std::min_element(b.cpus.cbegin(), b.cpus.cend()); - }); - const auto &cache = instances.constFirst(); - const QString type = cache.type == "Data" ? tr("data") - : cache.type == "Instruction" ? tr("instruction") : tr("unified"); - QStringList sharing; - QSet privateCpus; - bool allPrivate = true; - for (const auto &instance : instances) { - sharing.append(cpuListText(instance.cpus)); - allPrivate = allPrivate && instance.cpus.size() == 1; - privateCpus.unite(instance.cpus); - } - QString domains = allPrivate ? tr("Private to CPUs %1").arg(cpuListText(privateCpus)) - : instances.size() == 1 ? tr("Shared by CPUs %1").arg(sharing.constFirst()) - : tr("CPU groups %1").arg(sharing.join("; ")); - if (compact && !onlineCpus.isEmpty()) { - // Require an exact match to a known core's online threads. Unknown - // or offline sharing IDs keep the explicit kernel sharing sets. - bool perCore = true; - QSet seenCores; - for (const auto &instance : instances) { - const int cpu = *instance.cpus.cbegin(); - const QString core = cpuCores.value(cpu); - if (!cpuCores.contains(cpu) || instance.cpus != coreCpus.value(core) - || seenCores.contains(core)) { - perCore = false; - break; - } - seenCores.insert(core); - } - if (perCore) - domains = privateCpus == onlineCpus ? tr("Private per core") - : tr("Private per core · CPUs %1").arg(cpuListText(privateCpus)); - else if (instances.size() == 1 && cache.cpus == onlineCpus) - domains = tr("Shared by all online cores"); - } - const QString size = QLocale().formattedDataSize(static_cast(cache.bytes), 0, QLocale::DataSizeIecFormat); - rows.append({tr("L%1 %2").arg(cache.level).arg(type), - instances.size() == 1 ? size : tr("%1 × %2").arg(instances.size()).arg(size), domains}); - } - return rows.size() == 1 ? tr("Unavailable") : tableText(rows); - } - if (key == "instruction_sets") { - const QStringList groups = value.value.split('\n'); - QStringList lines; - for (const QString &group : groups) { - const QStringList parts = group.split('\t'); - lines.append(groups.size() == 1 ? parts.value(1) - : tr("Logical CPUs %1: %2").arg(parts.value(0), parts.value(1))); - } - return lines.join("\n\n"); - } - if (key == "caches") { - QStringList lines; - for (const QString &raw : value.value.split('\n')) { - const QStringList parts = raw.split('\t'); - const QString kind = parts.value(0); - QString type = kind.section(' ', 1); - if (type == "Data") type = tr("data"); - else if (type == "Instruction") type = tr("instruction"); - else if (type == "Unified") type = tr("unified"); - lines.append(tr("%1 %2: %3 (%4 unique instances)") - .arg(kind.section(' ', 0, 0), type, - QLocale().formattedDataSize(parts.value(1).toLongLong()), parts.value(2))); - } - return lines.join('\n'); - } - return value.value; + return systemProcessorValue(snapshot_, key, value, compact); } void SystemProcessorPage::setSnapshot(const SystemProperties &snapshot) { diff --git a/src/ui/systempages/rampage.cpp b/src/ui/systempages/rampage.cpp index 03a2cc5..3a9cc24 100644 --- a/src/ui/systempages/rampage.cpp +++ b/src/ui/systempages/rampage.cpp @@ -1,33 +1,15 @@ #include "rampage.h" +#include "systemreport.h" #include "propertypage.h" #include "propertytext.h" #include "propertyreport.h" #include "reporttable.h" #include #include -#include #include #include #include -#include -namespace { -QVector> memoryFields() -{ - using D = SystemRamPage; - return {{"PRESENT", D::tr("Presence")}, {"SIZE", D::tr("Capacity (total)")}, - {"VOLATILE_SIZE", D::tr("Volatile capacity")}, {"NON_VOLATILE_SIZE", D::tr("Nonvolatile capacity")}, - {"LOCATOR", D::tr("Device locator")}, {"BANK_LOCATOR", D::tr("Bank locator")}, - {"TYPE", D::tr("Memory type")}, {"FORM_FACTOR", D::tr("Form factor")}, - {"SPEED_MTS", D::tr("Rated transfer rate")}, {"CONFIGURED_SPEED_MTS", D::tr("Configured transfer rate")}, - {"MANUFACTURER", D::tr("Manufacturer")}, {"PART_NUMBER", D::tr("Part number")}}; -} -QString memoryScope() -{ - return SystemRamPage::tr("Firmware-reported records; these do not establish removable modules, physical slots, " - "memory channels, upgradeability or active ECC. Transfer rates are firmware declarations, not live measurements."); -} -} SystemRamPage::SystemRamPage(QWidget *parent) : QWidget(parent) { @@ -45,8 +27,8 @@ SystemRamPage::SystemRamPage(QWidget *parent) : QWidget(parent) summary_->addWidget(value, row, 1); return value; }; - physical_ = field(0, tr("Physical RAM")); - usable_ = field(1, tr("Usable RAM")); + physical_ = field(0, systemFieldCaption(SystemPage::Ram, "physical_memory")); + usable_ = field(1, systemFieldCaption(SystemPage::Ram, "memory")); auto *memory = new QWidget(fields); memoryForm_ = new QGridLayout(memory); memoryForm_->setContentsMargins(0, 0, 0, 0); @@ -58,9 +40,9 @@ SystemRamPage::SystemRamPage(QWidget *parent) : QWidget(parent) int SystemRamPage::captionWidthHint() const { int width = 0; - for (const QString &caption : {tr("Physical RAM"), tr("Usable RAM")}) + for (const QString &caption : {systemFieldCaption(SystemPage::Ram, "physical_memory"), systemFieldCaption(SystemPage::Ram, "memory")}) width = qMax(width, QLabel(caption).sizeHint().width()); - for (const auto &field : memoryFields()) + for (const auto &field : systemMemoryFields()) width = qMax(width, QLabel(field.second).sizeHint().width()); return width; } @@ -85,56 +67,30 @@ void SystemRamPage::setSnapshot(const SystemProperties &snapshot) } QString SystemRamPage::summaryValue(const QString &key) const { - const Attribute value = snapshot_.values.value(key); - if (value.state != ReadState::Available) return propertyReadValue(value); - constexpr qulonglong gib = 1024ULL * 1024 * 1024; - constexpr qulonglong mib = 1024ULL * 1024; - const qulonglong bytes = value.value.toULongLong(); - const int precision = key == "physical_memory" && bytes % gib == 0 ? 0 : 2; - QString text = tr("%1 GiB").arg(QLocale().toString(bytes / static_cast(gib), 'f', precision)); - const Attribute physical = snapshot_.values.value("physical_memory"); - if (key == "memory" && physical.state == ReadState::Available) { - const qulonglong installed = physical.value.toULongLong(); - if (installed >= bytes) - text += tr(" (%1 MiB is system reserved)") - .arg(QLocale().toString((installed - bytes) / static_cast(mib), 'f', 0)); - } - return text; + return systemRamSummaryValue(snapshot_, key); } QString SystemRamPage::devicesText() const { - QStringList lines {memoryScope()}; + QStringList lines {systemMemoryScope()}; const auto &memory = snapshot_.memoryDevices; - if (memory.status.state != ReadState::Available) lines.append(propertyReadValue(memory.status)); - else if (memory.devices.isEmpty()) lines.append(tr("No memory-device records reported")); + if (memory.status.state != ReadState::Available || memory.devices.isEmpty()) + lines.append(systemMemoryStatus(memory)); else for (const FirmwareMemoryDevice &device : memory.devices) { lines.append(tr("Memory device %1 (firmware record %2)").arg(device.index + 1).arg(device.index)); - for (const auto &field : memoryFields()) + for (const auto &field : systemMemoryFields()) lines.append(field.second + ": " + memoryValue(field.first, device.values.value(field.first))); } return lines.join('\n'); } QString SystemRamPage::copyText() const { - return tr("Physical RAM") + ": " + summaryValue("physical_memory") + '\n' - + tr("Usable RAM") + ": " + summaryValue("memory") + '\n' + return systemFieldCaption(SystemPage::Ram, "physical_memory") + ": " + summaryValue("physical_memory") + '\n' + + systemFieldCaption(SystemPage::Ram, "memory") + ": " + summaryValue("memory") + '\n' + tr("RAM") + ": " + devicesText(); } QString SystemRamPage::memoryValue(const QString &key, const Attribute &value) const { - if (key == "PRESENT" && value.state == ReadState::Unavailable) - return tr("Unknown (no explicit presence flag)"); - if (value.state != ReadState::Available) return propertyReadValue(value); - if (key == "PRESENT") return value.value == "0" ? tr("No device reported present") : tr("Device reported present"); - if (key == "SPEED_MTS" || key == "CONFIGURED_SPEED_MTS") - return tr("%1 MT/s").arg(QLocale().toString(value.value.toULongLong())); - if (key == "SIZE" || key == "VOLATILE_SIZE" || key == "NON_VOLATILE_SIZE") { - const qulonglong bytes = value.value.toULongLong(); - if (bytes <= static_cast(std::numeric_limits::max())) - return QLocale().formattedDataSize(static_cast(bytes), 2, QLocale::DataSizeIecFormat); - return tr("%1 bytes").arg(QLocale().toString(bytes)); - } - return value.value; + return systemMemoryValue(key, value); } void SystemRamPage::rebuildMemory() { @@ -142,17 +98,16 @@ void SystemRamPage::rebuildMemory() delete item->widget(); delete item; } - auto *heading = new QLabel(tr("Memory Devices"), memoryForm_->parentWidget()); + auto *heading = new QLabel(systemMemoryHeading(), memoryForm_->parentWidget()); QFont font = heading->font(); font.setBold(true); heading->setFont(font); - heading->setToolTip(memoryScope()); + heading->setToolTip(systemMemoryScope()); memoryForm_->addWidget(heading, 0, 0); const FirmwareMemoryInformation &memory = snapshot_.memoryDevices; if (memory.status.state != ReadState::Available || memory.devices.isEmpty()) { - auto *message = new QLabel(memory.status.state != ReadState::Available - ? propertyReadValue(memory.status) : tr("No memory-device records reported"), memoryForm_->parentWidget()); + auto *message = new QLabel(systemMemoryStatus(memory), memoryForm_->parentWidget()); message->setTextFormat(Qt::PlainText); message->setWordWrap(true); - message->setToolTip(snapshot_.sources.value("memory_devices") + '\n' + memoryScope()); + message->setToolTip(snapshot_.sources.value("memory_devices") + '\n' + systemMemoryScope()); memoryForm_->addWidget(message, 1, 0); return; } @@ -168,28 +123,28 @@ void SystemRamPage::rebuildMemory() // obsolete widths, which QTabWidget includes in the whole stack's minimum. auto *table = new ReportTable(chips); table->setColumnCount(2); - table->setHorizontalHeaderLabels({tr("Property"), tr("Value")}); - table->setRowCount(memoryFields().size()); + table->setHorizontalHeaderLabels(systemPropertyHeaders()); + table->setRowCount(systemMemoryFields().size()); layout->addWidget(table); memoryForm_->addWidget(chips, 1, 0); for (const FirmwareMemoryDevice &device : memory.devices) { - const int tab = tabs->addTab(tr("Chip %1").arg(device.index + 1)); + const int tab = tabs->addTab(systemMemoryTitle(device.index)); tabs->setTabToolTip(tab, tr("Memory device %1 (firmware record %2)") - .arg(device.index + 1).arg(device.index) + '\n' + memoryScope()); + .arg(device.index + 1).arg(device.index) + '\n' + systemMemoryScope()); } const auto showChip = [this, table](int index) { const auto &devices = snapshot_.memoryDevices.devices; if (index < 0 || index >= devices.size()) return; const auto &device = devices.at(index); table->clearContents(); - table->setHorizontalHeaderLabels({tr("Property"), tr("Value — Chip %1").arg(device.index + 1)}); - const auto fields = memoryFields(); + table->setHorizontalHeaderLabels({systemPropertyHeaders().at(0), tr("Value — Chip %1").arg(device.index + 1)}); + const auto fields = systemMemoryFields(); const QString prefix = QStringLiteral("MEMORY_DEVICE_%1_").arg(device.index); for (int row = 0; row < fields.size(); ++row) { const auto &field = fields[row]; auto *name = new QTableWidgetItem(field.second); auto *value = new QTableWidgetItem(memoryValue(field.first, device.values.value(field.first))); - const QString hint = QStringLiteral("libudev: dmi/id ") + prefix + field.first + '\n' + memoryScope(); + const QString hint = QStringLiteral("libudev: dmi/id ") + prefix + field.first + '\n' + systemMemoryScope(); name->setToolTip(hint); value->setToolTip(hint); table->setItem(row, 0, name); table->setItem(row, 1, value); diff --git a/tests/system-report.cpp b/tests/system-report.cpp new file mode 100644 index 0000000..06a5fb5 --- /dev/null +++ b/tests/system-report.cpp @@ -0,0 +1,52 @@ +#include "systemreport.h" +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + QLocale::setDefault(QLocale::c()); + int failures = 0; + const auto check = [&](bool ok, const char *message) { + if (!ok) { std::fprintf(stderr, "%s\n", message); ++failures; } + }; + const auto available = [](const QString &text) { return Attribute{ReadState::Available, text, 0}; }; + SystemProperties snapshot; + check(systemMemoryValue("PRESENT", {}) == "Unknown (no explicit presence flag)", + "Missing firmware presence does not imply an empty slot"); + check(systemMemoryValue("SIZE", {ReadState::PermissionDenied, {}, EACCES}) == "Permission denied", + "Permission failure is not reported as zero capacity"); + snapshot.values["physical_memory"] = available("8589934592"); + snapshot.values["memory"] = available("17179869184"); + check(!systemRamSummaryValue(snapshot, "memory").contains("reserved"), + "Inconsistent firmware capacity must not yield negative reserved memory"); + snapshot.values["online"] = available("4"); + check(systemProcessorValue(snapshot, "logical", available("8")) == "8 present; 4 online", + "Offline CPUs remain distinguished from online CPUs"); + check(systemOsValue(snapshot, "gpu_rebar", {}).contains("Unavailable"), + "Unavailable ReBAR metadata must not become a disabled verdict"); + check(systemOsValue(snapshot, "virtualization", {}).startsWith("Unknown"), + "Missing hypervisor identity must not prove bare metal"); + snapshot.mok.status = available("present"); + MokCertificate certificate; + certificate.subject = "CN=Fixture"; + certificate.issuer = certificate.subject; + certificate.subjectDisplay = "Fixture"; + certificate.expires = "2030-01-02T03:04:05Z"; + const auto cert = systemCertificateReport(certificate, 1, 1); + check(cert.title == "MOK Certificate" && cert.fields[1].second == "Same as owner" + && cert.fields[2].second.endsWith(" UTC"), "Certificate presentation preserves identity and UTC scope"); + // SMT fixture: two cores, each with two logical CPUs and a private cache. + QVector members; + for (int cpu = 0; cpu < 4; ++cpu) + members.append({cpu, "0", QString::number(cpu % 2), 0, available("4000000")}); + snapshot.cpuGroups.append({members}); + snapshot.cpuCaches = {{1, "Data", 32768, {0, 2}}, {1, "Data", 32768, {1, 3}}}; + const QString compact = systemProcessorValue(snapshot, "cache_topology", available("present"), true); + const QString detailed = systemProcessorValue(snapshot, "cache_topology", available("present"), false); + check(compact.contains("Private per core") && detailed.contains("0,2") && detailed.contains("1,3"), + "Compact SMT topology retains explicit domains in detailed output"); + return failures ? 1 : 0; +}