From 5068895bd6f80f9e4686a851931d9d5b986397d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Hallenbarter?= Date: Thu, 22 Jan 2026 14:04:12 +0000 Subject: [PATCH] init --- .gitignore | 6 + LICENSE | 21 +++ README.md | 134 ++++++++++++++ build.sh | 99 +++++++++++ config/default.conf | 120 +++++++++++++ generate_javadoc.sh | 96 ++++++++++ install.sh | 16 ++ jtop.sh | 54 ++++++ report.md | 88 +++++++++ src/jtop/App.java | 142 +++++++++++++++ src/jtop/Isystem/IBatteryInfo.java | 52 ++++++ src/jtop/Isystem/ICpuInfo.java | 32 ++++ src/jtop/Isystem/IDiskInfo.java | 31 ++++ src/jtop/Isystem/IMemoryInfo.java | 50 ++++++ src/jtop/Isystem/INetworkInfo.java | 27 +++ src/jtop/Isystem/IPathInfo.java | 33 ++++ src/jtop/Isystem/ITemperatureInfo.java | 19 ++ src/jtop/Isystem/IUptime.java | 22 +++ src/jtop/Main.java | 23 +++ src/jtop/config/Config.java | 141 +++++++++++++++ src/jtop/core/IRefreshable.java | 11 ++ src/jtop/core/InfoType.java | 9 + src/jtop/core/ProcessRow.java | 47 +++++ src/jtop/core/ProcessSorter.java | 96 ++++++++++ src/jtop/core/ProcessState.java | 68 +++++++ src/jtop/core/ProcessTableRenderer.java | 163 +++++++++++++++++ src/jtop/core/RefreshThread.java | 52 ++++++ src/jtop/core/ShowProcesses.java | 167 +++++++++++++++++ src/jtop/system/Feature.java | 40 +++++ src/jtop/system/FeatureResolver.java | 36 ++++ src/jtop/system/OperatingSystem.java | 36 ++++ src/jtop/system/SystemInfoFactory.java | 61 +++++++ src/jtop/system/freebsd/FreeBsdFeatures.java | 28 +++ src/jtop/system/freebsd/PathInfo.java | 53 ++++++ src/jtop/system/linux/BatteryInfo.java | 160 +++++++++++++++++ src/jtop/system/linux/CpuInfo.java | 111 ++++++++++++ src/jtop/system/linux/DiskInfo.java | 54 ++++++ src/jtop/system/linux/LinuxFeatures.java | 35 ++++ src/jtop/system/linux/MemoryInfo.java | 178 +++++++++++++++++++ src/jtop/system/linux/NetworkInfo.java | 53 ++++++ src/jtop/system/linux/PathInfo.java | 90 ++++++++++ src/jtop/system/linux/SystemSampler.java | 68 +++++++ src/jtop/system/linux/TemperatureInfo.java | 89 ++++++++++ src/jtop/system/linux/Uptime.java | 39 ++++ src/jtop/system/mac/MacFeatures.java | 28 +++ src/jtop/system/mac/PathInfo.java | 53 ++++++ src/jtop/terminal/Header.java | 50 ++++++ src/jtop/terminal/InputHandler.java | 134 ++++++++++++++ src/jtop/terminal/TerminalSize.java | 57 ++++++ 49 files changed, 3272 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 build.sh create mode 100644 config/default.conf create mode 100755 generate_javadoc.sh create mode 100755 install.sh create mode 100755 jtop.sh create mode 100644 report.md create mode 100644 src/jtop/App.java create mode 100644 src/jtop/Isystem/IBatteryInfo.java create mode 100644 src/jtop/Isystem/ICpuInfo.java create mode 100644 src/jtop/Isystem/IDiskInfo.java create mode 100644 src/jtop/Isystem/IMemoryInfo.java create mode 100644 src/jtop/Isystem/INetworkInfo.java create mode 100644 src/jtop/Isystem/IPathInfo.java create mode 100644 src/jtop/Isystem/ITemperatureInfo.java create mode 100644 src/jtop/Isystem/IUptime.java create mode 100644 src/jtop/Main.java create mode 100644 src/jtop/config/Config.java create mode 100644 src/jtop/core/IRefreshable.java create mode 100644 src/jtop/core/InfoType.java create mode 100644 src/jtop/core/ProcessRow.java create mode 100644 src/jtop/core/ProcessSorter.java create mode 100644 src/jtop/core/ProcessState.java create mode 100644 src/jtop/core/ProcessTableRenderer.java create mode 100644 src/jtop/core/RefreshThread.java create mode 100644 src/jtop/core/ShowProcesses.java create mode 100644 src/jtop/system/Feature.java create mode 100644 src/jtop/system/FeatureResolver.java create mode 100644 src/jtop/system/OperatingSystem.java create mode 100644 src/jtop/system/SystemInfoFactory.java create mode 100644 src/jtop/system/freebsd/FreeBsdFeatures.java create mode 100644 src/jtop/system/freebsd/PathInfo.java create mode 100644 src/jtop/system/linux/BatteryInfo.java create mode 100644 src/jtop/system/linux/CpuInfo.java create mode 100644 src/jtop/system/linux/DiskInfo.java create mode 100644 src/jtop/system/linux/LinuxFeatures.java create mode 100644 src/jtop/system/linux/MemoryInfo.java create mode 100644 src/jtop/system/linux/NetworkInfo.java create mode 100644 src/jtop/system/linux/PathInfo.java create mode 100644 src/jtop/system/linux/SystemSampler.java create mode 100644 src/jtop/system/linux/TemperatureInfo.java create mode 100644 src/jtop/system/linux/Uptime.java create mode 100644 src/jtop/system/mac/MacFeatures.java create mode 100644 src/jtop/system/mac/PathInfo.java create mode 100644 src/jtop/terminal/Header.java create mode 100644 src/jtop/terminal/InputHandler.java create mode 100644 src/jtop/terminal/TerminalSize.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b24921c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +.vscode/ +*.class +*TestFile.java +*.jar +doc/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..121366e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Jürg Georg Hallenbarter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..71c0e07 --- /dev/null +++ b/README.md @@ -0,0 +1,134 @@ +# jtop - A Java-based System Monitoring Tool + +\============================================= + +Note: This project is not related to the Python-based jtop for NVIDIA Jetson devices ([https://rnext.it/jetson\_stats/](https://rnext.it/jetson_stats/)) + +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![Java Version](https://img.shields.io/badge/Java-21%2B-orange.svg)](https://www.java.com/en/) +[![Platform](https://img.shields.io/badge/Platform-Linux-brightgreen.svg)](https://www.linux.org/) +[![Status](https://img.shields.io/badge/Status-Alpha-red.svg)](https://en.wikipedia.org/wiki/Software_release_life_cycle#Alpha) + +## Overview + +jtop is a lightweight, terminal-based system monitoring tool written in Java. It aims to replicate the basic functionality of the `top` command, providing a modular interface for easy system monitoring. + +## Features + +* Resource-efficient design +* No external build tools required +* Modular and tab-based interface +* Supports Java 21+ + +## Target Platform + +* Linux (primary target) +* Other platforms (e.g. macOS, freeBSD) may be supported in the future, pending compatibility testing and development. + +## Getting Started + +### Prerequisites + +* Java 21+ installed on your system +* Linux platform + +### Building and Running Locally + +1. Clone the repository: + + ```bash + git clone https://github.com/JGH0/jtop.git + cd jtop + ``` + +2. Compile the code: + + ```bash + javac src/*.java + ``` + +3. Run the application: + + ```bash + java src/Main.java + ``` + +### System-wide Installation + +To install jtop globally so that it can be run from any terminal: + +1. Clone the repository (if not done already): + + ```bash + git clone https://github.com/JGH0/jtop.git + cd jtop + ``` + +2. Make the build and install scripts executable: + + ```bash + chmod +x build.sh install.sh + ``` + +3. Build the project using the provided build script: + + ```bash + ./build.sh + ``` + +4. Install globally (requires root privileges): + + ```bash + sudo ./install.sh + ``` + +5. Run jtop from anywhere: + + ```bash + jtop + ``` + +**Notes:** + +* The `build.sh` script compiles all Java source files and creates an executable `jtop.jar`. +* The `install.sh` script copies `jtop.jar` to `/usr/local/lib/jtop` and installs a wrapper script in `/usr/local/bin` for easy execution. + +### Usage + +jtop provides a simple and intuitive interface for system monitoring. Use the following keys to navigate: + +* `j`/`k`: Scroll up/down +* `Enter`: Scroll entire row +* `q` or `Ctrl+C`: Quit + +## Contributing + +We welcome contributions from the community! To contribute: + +1. Fork the repository. +2. Create a new branch for your feature or bug fix. +3. Make your changes with proper testing. +4. Submit a pull request detailing your modifications. + +### Developer Documentation + +Developer documentation is generated using JavaDoc. To generate and view the documentation: + +```bash +chmod +x generate_javadoc.sh +./generate_javadoc.sh +``` + +This will create a `docs/` folder with HTML documentation you can view in your browser. + +## License + +jtop is licensed under the MIT License. See [LICENSE](LICENSE) for details. + +## Author + +* Jürg Georg Hallenbarter + +## Note + +jtop is currently in **Alpha** stage, which means it is still a work-in-progress and may contain bugs or incomplete features. Use at your own risk! diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..5d470b0 --- /dev/null +++ b/build.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# --- Build script for jtop --- +# This script compiles Java sources into a JAR. +# If JDK is missing, it prompts the user to install it using the detected package manager. + +set -e + +JTOP_NAME="jtop" +SRC_DIR="src" +BIN_DIR="bin" +JAR_FILE="${JTOP_NAME}.jar" +MAIN_CLASS="${JTOP_NAME}.Main" + +# --- Detect package manager --- +detect_package_manager() { + if command -v pacman >/dev/null 2>&1; then + echo "pacman" + elif command -v apt >/dev/null 2>&1; then + echo "apt" + elif command -v dnf >/dev/null 2>&1; then + echo "dnf" + elif command -v yum >/dev/null 2>&1; then + echo "yum" + elif command -v zypper >/dev/null 2>&1; then + echo "zypper" + elif command -v brew >/dev/null 2>&1; then + echo "brew" + elif command -v apk >/dev/null 2>&1; then + echo "apk" + elif command -v emerge >/dev/null 2>&1; then + echo "emerge" + + else + echo "" + fi +} + +# --- Generate JDK install command --- +jdk_install_command() { + local pm="$1" + case "$pm" in + pacman) echo "pacman -Sy --noconfirm jdk-openjdk" ;; + apt) echo "apt update && apt install -y openjdk-21-jdk || apt install -y default-jdk" ;; + dnf) echo "dnf install -y java-21-openjdk-devel || dnf install -y java-latest-openjdk-devel" ;; + yum) echo "yum install -y java-21-openjdk-devel || yum install -y java-latest-openjdk-devel" ;; + zypper) echo "zypper install -y java-21-openjdk-devel || zypper install -y java-latest-openjdk-devel" ;; + brew) echo "brew install openjdk" ;; + apk) echo "apk add openjdk21" ;; + emerge) echo "emerge dev-java/openjdk-bin" ;; + *) echo "" ;; + esac +} + +# --- Check for javac --- +if ! command -v javac >/dev/null 2>&1; then + echo "Java Development Kit (JDK) not found." + PM=$(detect_package_manager) + + if [[ -z "$PM" ]]; then + echo "Please install the latest JDK manually and rerun this script." + exit 1 + fi + + CMD=$(jdk_install_command "$PM") + if [[ $EUID -ne 0 && "$PM" != "brew" ]]; then + echo "Please run this script as root to install the JDK, or install it manually." + exit 1 + fi + + # Prompt the user + read -rp "Do you want to run the following command to install the JDK? [$CMD] (y/n): " ANSWER + case "$ANSWER" in + y|Y) + echo "Installing JDK..." + eval "$CMD" + echo "JDK installed successfully." + ;; + *) + echo "JDK installation cancelled. Please install manually and rerun." + exit 1 + ;; + esac +fi + +# --- Build process --- +echo "Cleaning previous build..." +rm -rf "$BIN_DIR" "$JAR_FILE" +mkdir -p "$BIN_DIR" + +echo "Compiling Java sources..." +# The -d flag preserves the package directory structure +find "$SRC_DIR" -name "*.java" > sources.txt +javac -d "$BIN_DIR" @sources.txt +rm sources.txt + +echo "Creating JAR file..." +jar cfe "$JAR_FILE" "$MAIN_CLASS" -C "$BIN_DIR" . + +echo "Build completed successfully: ${JAR_FILE}" \ No newline at end of file diff --git a/config/default.conf b/config/default.conf new file mode 100644 index 0000000..d124e3b --- /dev/null +++ b/config/default.conf @@ -0,0 +1,120 @@ +# /$$ +# | $$ +# /$$ /$$$$$$ /$$$$$$ /$$$$$$ +# |__/|_ $$_/ /$$__ $$ /$$__ $$ +# /$$ | $$ | $$ \ $$| $$ \ $$ +# | $$ | $$ /$$| $$ | $$| $$ | $$ +# | $$ | $$$$/| $$$$$$/| $$$$$$$/ +# | $$ \___/ \______/ | $$____/ +# /$$ | $$ | $$ +#| $$$$$$/ | $$ +# \______/ |__/ +# jtop - default.conf +# ================================= + +# --- Table Layout --- +table.header.content = PID,NAME,PATH,USER,CPU,MEMORY,DISK_READ,DISK_WRITE,NETWORK # header content available: PID, NAME, PATH, USER, CPU, MEMORY, DISK_READ, DISK_WRITE, NETWORK + +# --- Sorting --- +table.sorting.ASC = false # default sorting order false for "DESC" and true for "ASC" +table.sorting.default.header = "PID" # default sorting header available: "PID", "NAME", "PATH", "USER", "CPU", "MEMORY", "DISK_READ", "DISK_WRITE", "NETWORK" + +# --- CPU Column --- +table.value.CPU.accuracy = 3 # decimal places for CPU usage + +# --- Memory Column --- +table.value.MEMORY.accuracy = 3 # decimal places for memory + +# --- Network Column --- +table.value.NETWORK.format = "Mb" # valid: b,B,Kb,KB,Mb,MB,Gb,GB,Tb,TB +table.value.NETWORK.accuracy = 3 # decimal places for network speed +table.value.NETWORK.scientificNotation = false # use scientitic notation instead of standart notation + +# --- Disk I/O Columns --- +table.value.DISK_READ.format = "Mb" # valid: b,B,Kb,KB,Mb,MB,Gb,GB,Tb,TB +table.value.DISK_READ.accuracy = 3 # decimal places for disk reaq speed +table.value.DISK_WRITE.format = "Mb" # valid: b,B,Kb,KB,Mb,MB,Gb,GB,Tb,TB +table.value.DISK_WRITE.accuracy = 3 # decimal places for disk write speed + +# --- Keybinding Config --- +footer.text.keyBindings = "Use j/k to scroll, Enter to scroll entire row, 'q' or Ctrl+C to quit" + +# --- Design Config --- +# ANSI color codes let you style terminal output (text & background). +# Codes start with "\033[" and end with "m". Combine multiple codes by adding them. + +header.color = "\033[47m" + "\033[30m" # White background, black text +footer.color = "\033[41m" + "\033[37m" # Red background, white text +table.color = "\033[40m" + "\033[37m" # Black background, white text + +# --- Base Color Codes --- +# Foreground (Text): 30–37 +# Background: 40–47 +# Bright Foreground: 90–97 +# Bright Background: 100–107 + +# --- Foreground Colors --- +# \033[30m = Black +# \033[31m = Red +# \033[32m = Green +# \033[33m = Yellow +# \033[34m = Blue +# \033[35m = Magenta +# \033[36m = Cyan +# \033[37m = White + +# --- Background Colors --- +# \033[40m = Black +# \033[41m = Red +# \033[42m = Green +# \033[43m = Yellow +# \033[44m = Blue +# \033[45m = Magenta +# \033[46m = Cyan +# \033[47m = White + +# --- Bright Foreground Colors --- +# \033[90m = Bright Black (Gray) +# \033[91m = Bright Red +# \033[92m = Bright Green +# \033[93m = Bright Yellow +# \033[94m = Bright Blue +# \033[95m = Bright Magenta +# \033[96m = Bright Cyan +# \033[97m = Bright White + +# --- Bright Background Colors --- +# \033[100m = Bright Black (Gray) +# \033[101m = Bright Red +# \033[102m = Bright Green +# \033[103m = Bright Yellow +# \033[104m = Bright Blue +# \033[105m = Bright Magenta +# \033[106m = Bright Cyan +# \033[107m = Bright White + +# --- Text Formatting (Styles) --- +# \033[0m = Reset all styles and colors +# \033[1m = Bold / Bright text +# \033[2m = Dim text +# \033[3m = Italic (may not work in all terminals) +# \033[4m = Underline +# \033[5m = Blink (slow) +# \033[6m = Blink (rapid) +# \033[7m = Reverse (swap foreground and background) +# \033[8m = Hidden / Conceal text +# \033[9m = Strikethrough +# \033[21m = Double underline (rarely supported) +# \033[22m = Normal intensity (cancel bold/dim) + +# --- Combining Styles --- +# Example: bold + underline + red text +# footer.color = "\033[1m" + "\033[4m" + "\033[31m" + +# --- 256-Color and TrueColor (Modern Terminals like Kitty) --- +# 256-Color Foreground: \033[38;5;{n}m (0–255) +# 256-Color Background: \033[48;5;{n}m +# TrueColor (RGB) Foreground: \033[38;2;R;G;B m +# TrueColor (RGB) Background: \033[48;2;R;G;B m +# Example: = orange text +# footer.color = "\033[38;2;255;128;0m" \ No newline at end of file diff --git a/generate_javadoc.sh b/generate_javadoc.sh new file mode 100755 index 0000000..426ed39 --- /dev/null +++ b/generate_javadoc.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# --- Generate JavaDoc for jtop --- +# This script generates developer documentation using javadoc. +# If JDK is missing, it prompts the user to install it using the detected package manager. + +set -e + +SRC_DIR="src" +DOC_DIR="doc" + +# --- Detect package manager --- +detect_package_manager() { + if command -v pacman >/dev/null 2>&1; then + echo "pacman" + elif command -v apt >/dev/null 2>&1; then + echo "apt" + elif command -v dnf >/dev/null 2>&1; then + echo "dnf" + elif command -v yum >/dev/null 2>&1; then + echo "yum" + elif command -v zypper >/dev/null 2>&1; then + echo "zypper" + elif command -v brew >/dev/null 2>&1; then + echo "brew" + elif command -v apk >/dev/null 2>&1; then + echo "apk" + elif command -v emerge >/dev/null 2>&1; then + echo "emerge" + else + echo "" + fi +} + +# --- Generate JDK install command --- +jdk_install_command() { + local pm="$1" + case "$pm" in + pacman) echo "pacman -Sy --noconfirm jdk-openjdk" ;; + apt) echo "apt update && apt install -y openjdk-21-jdk || apt install -y default-jdk" ;; + dnf) echo "dnf install -y java-21-openjdk-devel || dnf install -y java-latest-openjdk-devel" ;; + yum) echo "yum install -y java-21-openjdk-devel || yum install -y java-latest-openjdk-devel" ;; + zypper) echo "zypper install -y java-21-openjdk-devel || zypper install -y java-latest-openjdk-devel" ;; + brew) echo "brew install openjdk" ;; + apk) echo "apk add openjdk21" ;; + emerge) echo "emerge dev-java/openjdk-bin" ;; + *) echo "" ;; + esac +} + +# --- Check for javadoc --- +if ! command -v javadoc >/dev/null 2>&1; then + echo "Java Development Kit (JDK) with javadoc not found." + PM=$(detect_package_manager) + + if [[ -z "$PM" ]]; then + echo "Please install the latest JDK manually and rerun this script." + exit 1 + fi + + CMD=$(jdk_install_command "$PM") + if [[ $EUID -ne 0 && "$PM" != "brew" ]]; then + echo "Please rerun this script with sudo if you want automatic JDK installation." + exit 1 + fi + + # Prompt the user + read -rp "Do you want to run the following command to install the JDK? [$CMD] (y/n): " ANSWER + case "$ANSWER" in + y|Y) + echo "Installing JDK..." + eval "$CMD" + echo "JDK installed successfully." + ;; + *) + echo "JDK installation cancelled. Please install manually and rerun." + exit 1 + ;; + esac +fi + +# --- Generate JavaDoc --- +echo "Generating JavaDoc..." +rm -rf "$DOC_DIR" +mkdir -p "$DOC_DIR" + +if javadoc -quiet -d "$DOC_DIR" \ + -sourcepath "$SRC_DIR" \ + -subpackages jtop \ + -private \ + -author \ + -version > /dev/null 2>&1; then + echo -e "\033[32mJavaDoc generated successfully!" + echo -e "\033[0m→ Open file://$PWD/$DOC_DIR/index.html to view it." +else + echo -e "\033[31mJavaDoc generation failed (check your paths or source)." +fi \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..89cdbe2 --- /dev/null +++ b/install.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Install script for system-wide usage + +# Ensure running as root +if [[ $EUID -ne 0 ]]; then + echo "Please run as root to install globally." + exit 1 +fi + +# Create installation directories +mkdir -p /usr/local/lib/jtop +cp jtop.jar /usr/local/lib/jtop/ +cp jtop.sh /usr/local/bin/jtop +chmod +x /usr/local/bin/jtop + +echo "jtop installed successfully! You can now run 'jtop' from anywhere." \ No newline at end of file diff --git a/jtop.sh b/jtop.sh new file mode 100755 index 0000000..2a0c4cf --- /dev/null +++ b/jtop.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# jtop launcher with self-update via GitHub + rebuild (requires sudo for update) + +JTOP_DIR="/usr/local/lib/jtop" +JTOP_JAR="$JTOP_DIR/jtop.jar" +GIT_REPO="https://github.com/JGH0/jtop.git" + +update_jtop() { + # Check for sudo/root + if [[ $EUID -ne 0 ]]; then + echo "jtop --update requires root privileges. Please run with sudo." + exit 1 + fi + + echo "Updating jtop from GitHub..." + + TMP_DIR=$(mktemp -d) + echo "Cloning repository into $TMP_DIR..." + if ! git clone --depth 1 "$GIT_REPO" "$TMP_DIR"; then + echo "Failed to clone repository." + rm -rf "$TMP_DIR" + exit 1 + fi + + echo "Building jtop..." + pushd "$TMP_DIR" >/dev/null + if ! ./build.sh; then + echo "Build failed." + popd >/dev/null + rm -rf "$TMP_DIR" + exit 1 + fi + + echo "Installing new version..." + if ! ./install.sh; then + echo "Install failed." + popd >/dev/null + rm -rf "$TMP_DIR" + exit 1 + fi + + popd >/dev/null + rm -rf "$TMP_DIR" + echo "Update completed successfully!" + exit 0 +} + +# Handle --update argument +if [[ "$1" == "--update" ]]; then + update_jtop +fi + +# Run jtop normally +java -jar "$JTOP_JAR" "$@" \ No newline at end of file diff --git a/report.md b/report.md new file mode 100644 index 0000000..82cd514 --- /dev/null +++ b/report.md @@ -0,0 +1,88 @@ +# jtop Project Report + +## Overview + +**jtop** is a terminal-based system monitoring tool written in Java. It provides a lightweight alternative to the `top` command, featuring a modular and extensible interface. + +## Features Implemented + +* CPU, memory, disk, and network usage monitoring +* Process list with scrollable interface +* Sorting by CPU, memory, PID, name, path, and other headers +* Temperature monitoring (via `/sys/class/hwmon` or `/sys/class/thermal`) +* Terminal-size adaptive display +* Keyboard navigation: `j/k` to scroll, `Enter` for page scroll, `q` or `Ctrl+C` to quit + +## Class Structure + +* `Main` — Entry point; sets up the terminal and refresh loop +* `ShowProcesses` — Collects and manages running process data +* `ProcessRow` — Represents a single process entry +* `ProcessTableRenderer` — Draws the process table in the terminal +* `ProcessSorter` — Provides comparators for sorting +* `MemoryInfo`, `CpuInfo`, `DiskInfo`, `NetworkInfo`, `TemperatureInfo`, `Uptime`, etc. — System metrics modules +* `Header` — Displays header information +* `InputHandler` — Reads and handles keyboard and mouse input +* `TerminalSize` — Detects terminal dimensions +* `RefreshThread` — Handles background refresh of process data +* `PathInfo` — Retrieves process path and name + +## Design Considerations + +* Modular architecture with clear separation of data collection, rendering, and input handling +* Uses Java 21+ features and the ProcessHandle API +* Layout adapts dynamically to terminal size to prevent overflow +* Graceful handling of missing or inaccessible process information +* Keyboard navigation inspired by tools like `less` and `top` + +## Usage + +Compile and run: + +```bash +javac src/*.java +java src/Main.java +``` + +For system-wide installation, use the included `build.sh` and `install.sh` scripts. + +**Keyboard shortcuts:** + +* `j/k`: scroll up/down +* `Enter`: scroll one page +* `q` or `Ctrl+C`: quit + +## Code Quality + +* Clear separation of concerns +* Proper encapsulation of fields +* Use of constructors and method overloading where appropriate +* Aggregation/composition used in `ShowProcesses` and rendering classes +* Interfaces used via `Comparator` for sorting +* Inheritance applied through `Thread` extension in `RefreshThread` + +## Notes + +* JavaDoc documentation can be generated using `./generate_javadoc.sh` +* The project is in an **alpha stage** — some metrics may not work on all hardware +* Tested primarily on Linux systems with Intel and AMD CPUs + +## TODOs / Known Issues + +### Tab View +* Planned implementation of a tab system to allow grouping of process information + +### Config +* Config file support is partially implemented — some fields (e.g., `table.header.content`) are not yet parsed or applied +* Configs are not yet accessible in system-wide installations + +### Performance / Design +* On some terminals with custom themes, colors may display incorrectly +* Cursor animations can cause slight lag during refresh +* Table caching occasionally fails, causing unnecessary redraws and performance drops +* On low-performance machines, keyboard input may experience minor delay + +## Author + +* **Jürg Georg Hallenbarter** +* Version 1.0 — Date: 2025-10-24 \ No newline at end of file diff --git a/src/jtop/App.java b/src/jtop/App.java new file mode 100644 index 0000000..ea99187 --- /dev/null +++ b/src/jtop/App.java @@ -0,0 +1,142 @@ +package jtop; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import jtop.config.Config; +import jtop.core.InfoType; +import jtop.core.RefreshThread; +import jtop.core.ShowProcesses; +import jtop.terminal.InputHandler; +import jtop.terminal.TerminalSize; + +/** + * Core application class that initializes and coordinates all major components + * of the terminal-based process monitor. + *

+ * This class is responsible for: + *

+ * + */ +public class App { + + /** Utility for determining terminal dimensions. */ + private final TerminalSize terminalSize = new TerminalSize(); + + /** Holds configuration data for runtime settings. */ + private final Config config = new Config(); + + /** Handles process retrieval and display logic. */ + private final ShowProcesses showProcesses; + + /** Shared flag for synchronizing display refreshes. */ + private final AtomicBoolean refresh = new AtomicBoolean(true); + + /** + * Constructs a new {@code App} instance and initializes the main process display. + *

+ * By default, it prepares a {@link ShowProcesses} object displaying + * PID, process name, user, CPU usage, and memory usage. + *

+ */ + public App() { + showProcesses = new ShowProcesses( + InfoType.PID, + InfoType.NAME, + InfoType.USER, + InfoType.CPU, + InfoType.MEMORY + ); + } + + /** + * Starts the main application loop. + *

+ * This method: + *

+ * + * When the loop ends, terminal settings are restored and mouse reporting is disabled. + * + * @throws Exception if an I/O or threading error occurs + */ + public void run() throws Exception { + enableRawMode(); + enableMouseReporting(); + + try { + showProcesses.draw(); // initial draw + + // Start background refresh + Thread refreshThread = new RefreshThread(showProcesses, refresh); + refreshThread.setDaemon(true); + refreshThread.start(); + + // Handle user input + new InputHandler(showProcesses, refresh, terminalSize).start(); + + } finally { + disableMouseReporting(); + restoreTerminal(); + } + } + + /** + * Enables raw input mode on the terminal. + *

+ * This disables canonical input processing and echoing, + * allowing single keypress handling without pressing Enter. + *

+ * + * @throws IOException if the process builder fails + * @throws InterruptedException if the command execution is interrupted + */ + private void enableRawMode() throws IOException, InterruptedException { + new ProcessBuilder("sh", "-c", "stty raw -echo + * This re-enables standard input behavior and character echoing. + *

+ * + * @throws IOException if the process builder fails + * @throws InterruptedException if the command execution is interrupted + */ + private void restoreTerminal() throws IOException, InterruptedException { + new ProcessBuilder("sh", "-c", "stty sane + * Allows the application to receive and interpret mouse events + * such as clicks and scrolls in the terminal. + *

+ */ + private void enableMouseReporting() { + System.out.print("\u001B[?1000h"); + System.out.flush(); + } + + /** + * Disables mouse reporting mode. + *

+ * Ensures that the terminal stops sending mouse event sequences + * once the program exits or cleans up. + *

+ */ + private void disableMouseReporting() { + System.out.print("\u001B[?1000l"); + System.out.flush(); + } +} \ No newline at end of file diff --git a/src/jtop/Isystem/IBatteryInfo.java b/src/jtop/Isystem/IBatteryInfo.java new file mode 100644 index 0000000..fdf0fb5 --- /dev/null +++ b/src/jtop/Isystem/IBatteryInfo.java @@ -0,0 +1,52 @@ +package jtop.Isystem; + +/** + * Interface for retrieving battery information. + *

+ * Allows platform-specific implementations to provide battery status, charge, + * voltage, energy, and power readings. + */ +public interface IBatteryInfo { + + /** + * Gets the current battery charge percentage (0–100). + * + * @return battery percentage, or -1 if unavailable + */ + int getBatteryPercentage(); + + /** + * Gets the current battery status (e.g., Charging, Discharging, Full). + * + * @return battery status string, or null if unavailable + */ + String getBatteryStatus(); + + /** + * Gets the current battery voltage in volts. + * + * @return voltage in volts, or -1 if unavailable + */ + double getVoltage(); + + /** + * Gets the current battery energy in watt-hours. + * + * @return energy in Wh, or -1 if unavailable + */ + double getEnergy(); + + /** + * Gets the current power draw in watts. + * + * @return power in W, or -1 if unavailable + */ + double getPower(); + + /** + * Returns whether the system has a battery. + * + * @return true if battery is present and readable, false otherwise + */ + boolean hasBattery(); +} \ No newline at end of file diff --git a/src/jtop/Isystem/ICpuInfo.java b/src/jtop/Isystem/ICpuInfo.java new file mode 100644 index 0000000..4c56447 --- /dev/null +++ b/src/jtop/Isystem/ICpuInfo.java @@ -0,0 +1,32 @@ +package jtop.Isystem; + +/** + * Interface for retrieving CPU usage information in a cross-platform way. + *

+ * Implementations can provide system-specific logic for Linux, macOS, Windows, or FreeBSD. + */ +public interface ICpuInfo { + + /** + * Computes the CPU usage percentage of a specific process. + * + * @param pid the process ID + * @return CPU usage as a percentage, or -1 if unavailable + */ + double getCpuPercent(long pid); + + /** + * Retrieves the system load average. + * + * @return the load average string, or null if unavailable + */ + String getLoadAverage(); + + /** + * Computes the overall CPU usage percentage over a sample period. + * + * @param sampleMs the sample duration in milliseconds + * @return the CPU usage as a percentage over the sample period, or -1 if unavailable + */ + double getCpuUsage(long sampleMs); +} diff --git a/src/jtop/Isystem/IDiskInfo.java b/src/jtop/Isystem/IDiskInfo.java new file mode 100644 index 0000000..2e4f185 --- /dev/null +++ b/src/jtop/Isystem/IDiskInfo.java @@ -0,0 +1,31 @@ +package jtop.Isystem; + +import java.io.IOException; +import java.util.Map; + +/** + * Interface for providing information about disk usage and I/O statistics for mounted devices. + *

+ * Implementations should retrieve disk statistics and provide a map of device names to their + * read/write counts. + *

+ */ +public interface IDiskInfo { + + /** + * Retrieves disk I/O statistics for all block devices. + *

+ * Each entry in the returned map contains the device name as the key, + * and an array of two long values as the value: + *

+ * + * + * @return a {@link Map} where the key is the device name and the value is a + * long array containing [reads, writes] + * @throws IOException if disk statistics cannot be read + */ + Map getDiskStats() throws IOException; +} diff --git a/src/jtop/Isystem/IMemoryInfo.java b/src/jtop/Isystem/IMemoryInfo.java new file mode 100644 index 0000000..c74f2e0 --- /dev/null +++ b/src/jtop/Isystem/IMemoryInfo.java @@ -0,0 +1,50 @@ +package jtop.Isystem; + +import java.io.IOException; + +/** + * Provides methods to gather memory usage statistics. + *

+ * Implementations typically read Linux /proc files to determine: + *

+ * + */ +public interface IMemoryInfo { + + /** + * Returns the memory usage percentage of a process. + * + * @param pid the process ID + * @return memory usage percentage of the process + * @throws IOException if the /proc file cannot be read or is malformed + */ + double getMemoryPercent(long pid) throws IOException; + + /** + * Returns the overall memory usage percentage of the system. + * + * @return memory usage percentage of the system + * @throws IOException if /proc/meminfo cannot be read + */ + double getMemoryUsage() throws IOException; + + /** + * Returns total system memory in bytes. + * + * @return total memory in bytes + * @throws IOException if /proc/meminfo cannot be read + */ + long getTotalMemoryBytes() throws IOException; + + /** + * Returns used memory in bytes (total minus available memory). + * + * @return used memory in bytes + * @throws IOException if /proc/meminfo cannot be read + */ + long getAvailableMemoryBytes() throws IOException; +} \ No newline at end of file diff --git a/src/jtop/Isystem/INetworkInfo.java b/src/jtop/Isystem/INetworkInfo.java new file mode 100644 index 0000000..cee655c --- /dev/null +++ b/src/jtop/Isystem/INetworkInfo.java @@ -0,0 +1,27 @@ +package jtop.Isystem; + +import java.io.IOException; +import java.util.Map; + +/** + * Provides methods to collect network usage statistics. + *

+ * Reads from the Linux file /proc/net/dev to retrieve + * the number of bytes received (RX) and transmitted (TX) per network interface. + *

+ */ +public interface INetworkInfo { + + /** + * Retrieves the current network usage for all network interfaces. + * + * @return a map where the key is the interface name (e.g., "eth0") and + * the value is a long array of size 2: + * + * @throws IOException if /proc/net/dev cannot be read + */ + Map getNetworkUsage() throws IOException; +} \ No newline at end of file diff --git a/src/jtop/Isystem/IPathInfo.java b/src/jtop/Isystem/IPathInfo.java new file mode 100644 index 0000000..652136d --- /dev/null +++ b/src/jtop/Isystem/IPathInfo.java @@ -0,0 +1,33 @@ +package jtop.Isystem; + +/** + * Provides utilities to retrieve process path information. + *

+ * Implementations may use OS-specific mechanisms to fetch details about + * a running process, including its command (full path) and executable name. + *

+ */ +public interface IPathInfo { + + /** + * Returns the name of the executable for the given process ID. + *

+ * For example, if the command is "/usr/bin/java", this should return "java". + *

+ * + * @param pid the process ID + * @return the executable name, or "Unknown" if the process does not exist + */ + String getName(long pid); + + /** + * Returns the full command path of the executable for the given process ID. + *

+ * For example, "/usr/bin/java". + *

+ * + * @param pid the process ID + * @return the full command path, or "Unknown" if the process does not exist + */ + String getPath(long pid); +} diff --git a/src/jtop/Isystem/ITemperatureInfo.java b/src/jtop/Isystem/ITemperatureInfo.java new file mode 100644 index 0000000..564cf39 --- /dev/null +++ b/src/jtop/Isystem/ITemperatureInfo.java @@ -0,0 +1,19 @@ +package jtop.Isystem; + +import java.io.IOException; +import java.util.Map; + +/** + * Interface for providing system temperature information. + */ +public interface ITemperatureInfo { + + /** + * Retrieves a map of all detected temperatures on the system. + * + * @return Map where the key is a sensor name + * and the value is the temperature in °C. + * @throws IOException if the sensor directories cannot be read + */ + Map getTemperatures() throws IOException; +} \ No newline at end of file diff --git a/src/jtop/Isystem/IUptime.java b/src/jtop/Isystem/IUptime.java new file mode 100644 index 0000000..ac7639b --- /dev/null +++ b/src/jtop/Isystem/IUptime.java @@ -0,0 +1,22 @@ +package jtop.Isystem; + +/** + * Interface for retrieving system uptime information. + *

+ * Provides the uptime in various units such as seconds, minutes, hours, or days. + */ +public interface IUptime { + + /** + * Gets the system uptime in the specified format. + * + * @param timeFormat a character indicating the desired unit: + * 's' for seconds, + * 'm' for minutes, + * 'h' for hours, + * 'd' for days. + * @return the system uptime in the requested unit. + * @throws Exception if reading /proc/uptime fails or if the timeFormat is invalid. + */ + double getSystemUptime(char timeFormat) throws Exception; +} diff --git a/src/jtop/Main.java b/src/jtop/Main.java new file mode 100644 index 0000000..db5618c --- /dev/null +++ b/src/jtop/Main.java @@ -0,0 +1,23 @@ +package jtop; + +/** + * Entry point for the jtop system monitoring application. + *

+ * This class initializes the application and starts the main execution loop. + * All setup and execution logic is delegated to {@link App}. + */ +public class Main { + + /** + * Launches the jtop application. + *

+ * Initializes all necessary components, including process monitoring, + * terminal rendering, and input handling. + * + * @param args Command-line arguments (currently ignored) + * @throws Exception If system information cannot be read or if thread operations fail + */ + public static void main(String[] args) throws Exception { + new App().run(); + } +} \ No newline at end of file diff --git a/src/jtop/config/Config.java b/src/jtop/config/Config.java new file mode 100644 index 0000000..bc107c9 --- /dev/null +++ b/src/jtop/config/Config.java @@ -0,0 +1,141 @@ +package jtop.config; +import java.io.FileReader; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +/** + * Represents configuration options for the application. + *

+ * Handles reading, parsing, and providing configuration values + * such as refresh intervals, color modes, sorting options, and lists of items. + * Supports inline comments and ANSI escape sequences in configuration values. + *

+ */ +public class Config { + + /** Stores all loaded configuration properties. */ + private final Properties properties = new Properties(); + + /** + * Constructs a {@code Config} instance using the default configuration file. + * The default path is {@code config/default.conf}. + *

+ * If the file cannot be loaded, an error message is printed to {@code stderr}. + *

+ */ + public Config() { + String filePath = "config/default.conf"; + try (FileReader reader = new FileReader(filePath)) { + properties.load(reader); + } catch (IOException e) { + System.err.println("Could not load config file: " + filePath); + e.printStackTrace(); + } + } + + /** + * Constructs a {@code Config} instance using a custom configuration file path. + * + * @param filePath the path to the configuration file + */ + public Config(String filePath) { + try (FileReader reader = new FileReader(filePath)) { + properties.load(reader); + } catch (IOException e) { + System.err.println("Could not load config file: " + filePath); + e.printStackTrace(); + } + } + + /** + * Cleans and normalizes a raw configuration value. + *

+ * Cleaning includes: + *

+ *
    + *
  • Removing inline comments starting with '#'
  • + *
  • Removing quotes and '+' signs
  • + *
  • Replacing escape sequences (033 or \033) with ANSI escape character
  • + *
  • Trimming whitespace and spaces immediately after escape codes
  • + *
+ * + * @param value the raw configuration string + * @return the cleaned value, or {@code null} if the input was {@code null} + */ + private String cleanValue(String value) { + if (value == null) return null; + + int commentIndex = value.indexOf('#'); + if (commentIndex != -1) { + value = value.substring(0, commentIndex); + } + + value = value.replace("\"", "").replace("+", ""); + value = value.replaceAll("\\\\?033", "\u001B"); + value = value.replaceAll("(\u001B\\[[0-9;]*m)\\s+", "$1"); + return value.strip(); + } + + /** + * Retrieves a configuration value as a string. + * + * @param key the configuration key + * @param defaultValue the value to return if the key is missing or empty + * @return the string value associated with the key, or {@code defaultValue} if not found + */ + public String getString(String key, String defaultValue) { + String value = cleanValue(properties.getProperty(key, defaultValue)); + return (value != null) ? value : defaultValue; + } + + /** + * Retrieves a configuration value as an integer. + * + * @param key the configuration key + * @param defaultValue the value to return if the key is missing, invalid, or unparsable + * @return the integer value associated with the key, or {@code defaultValue} if not found or invalid + */ + public int getInt(String key, int defaultValue) { + String value = cleanValue(properties.getProperty(key)); + if (value != null) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + System.err.println("Invalid int for key " + key + ": " + value); + } + } + return defaultValue; + } + + /** + * Retrieves a configuration value as a boolean. + * + * @param key the configuration key + * @param defaultValue the value to return if the key is missing + * @return the boolean value associated with the key, or {@code defaultValue} if not found + */ + public boolean getBoolean(String key, boolean defaultValue) { + String value = cleanValue(properties.getProperty(key)); + return (value != null) ? Boolean.parseBoolean(value) : defaultValue; + } + + /** + * Retrieves a configuration value as a list of strings. + * + * @param key the configuration key + * @param separator the string used to split the value into multiple elements + * @param defaultValue the value to return if the key is missing + * @return a list of trimmed string values, or {@code defaultValue} if not found + */ + public List getList(String key, String separator, List defaultValue) { + String value = cleanValue(properties.getProperty(key)); + if (value != null) { + String[] parts = value.split(separator); + for (int i = 0; i < parts.length; i++) parts[i] = parts[i].strip(); + return Arrays.asList(parts); + } + return defaultValue; + } +} \ No newline at end of file diff --git a/src/jtop/core/IRefreshable.java b/src/jtop/core/IRefreshable.java new file mode 100644 index 0000000..de966e8 --- /dev/null +++ b/src/jtop/core/IRefreshable.java @@ -0,0 +1,11 @@ +package jtop.core; +/** + * Interface for objects that can be refreshed periodically. + */ +public interface IRefreshable { + /** + * Refresh the object. + * Implementations should update internal state or display as needed. + */ + void refresh(); +} \ No newline at end of file diff --git a/src/jtop/core/InfoType.java b/src/jtop/core/InfoType.java new file mode 100644 index 0000000..0983970 --- /dev/null +++ b/src/jtop/core/InfoType.java @@ -0,0 +1,9 @@ +package jtop.core; +/** + * Enum that defines different system information types + * (e.g., CPU, Memory, Disk, Network). + * Used for switching between data displays or processing logic. + */ +public enum InfoType { + PID, NAME, PATH, USER, CPU, MEMORY, DISK_READ, DISK_WRITE, NETWORK +} \ No newline at end of file diff --git a/src/jtop/core/ProcessRow.java b/src/jtop/core/ProcessRow.java new file mode 100644 index 0000000..4e2b985 --- /dev/null +++ b/src/jtop/core/ProcessRow.java @@ -0,0 +1,47 @@ +package jtop.core; +/** + * Represents a single process entry in the system. + *

+ * Holds basic information about a process including its ID, name, executable path, + * owner, CPU usage, and memory usage. + *

+ */ +public class ProcessRow { + + /** Process ID (PID) */ + public long pid; + + /** Name of the executable (e.g., "java") */ + public String name; + + /** Full path of the executable (e.g., "/usr/bin/java") */ + public String path; + + /** User or owner of the process */ + public String user; + + /** CPU usage as a percentage string (e.g., "12.5") */ + public String cpu; + + /** Memory usage as a percentage string (e.g., "8.3") */ + public String memory; + + /** + * Constructs a ProcessRow instance. + * + * @param pid the process ID + * @param name the process executable name + * @param path the full path to the process executable + * @param user the owner of the process + * @param cpu the CPU usage as a string + * @param memory the memory usage as a string + */ + public ProcessRow(long pid, String name, String path, String user, String cpu, String memory) { + this.pid = pid; + this.name = name; + this.path = path; + this.user = user; + this.cpu = cpu; + this.memory = memory; + } +} \ No newline at end of file diff --git a/src/jtop/core/ProcessSorter.java b/src/jtop/core/ProcessSorter.java new file mode 100644 index 0000000..29144aa --- /dev/null +++ b/src/jtop/core/ProcessSorter.java @@ -0,0 +1,96 @@ +package jtop.core; + +import java.util.Comparator; +import java.util.Optional; + +import jtop.Isystem.ICpuInfo; +import jtop.Isystem.IMemoryInfo; +import jtop.Isystem.IPathInfo; +import jtop.system.Feature; +import jtop.system.SystemInfoFactory; + +/** + * Provides sorting utilities for processes. + *

+ * Generates comparators to sort {@link ProcessHandle} instances based on + * PID, name, path, user, CPU usage, or memory usage. Supports ascending + * and descending order. + *

+ */ +public class ProcessSorter { + + /** + * Returns a comparator for processes based on the specified sort type. + * + * @param sortBy the {@link InfoType} to sort by (PID, NAME, CPU, MEMORY, etc.) + * @param ascending true for ascending order, false for descending + * @return a {@link Comparator} for {@link ProcessHandle} + */ + public static Comparator getComparator(InfoType sortBy, boolean ascending) { + // create interface instances from factory + Optional pathOpt = SystemInfoFactory.getFeature(Feature.PROCESS); + Optional cpuOpt = SystemInfoFactory.getFeature(Feature.CPU); + Optional memOpt = SystemInfoFactory.getFeature(Feature.MEMORY); + + return (a, b) -> { + int cmp = 0; + try { + switch (sortBy) { + case PID -> cmp = Long.compare(a.pid(), b.pid()); + case NAME -> cmp = safeCompare( + pathOpt.map(p -> p.getName(a.pid())).orElse(""), + pathOpt.map(p -> p.getName(b.pid())).orElse("") + ); + case PATH -> cmp = safeCompare( + pathOpt.map(p -> p.getPath(a.pid())).orElse(""), + pathOpt.map(p -> p.getPath(b.pid())).orElse("") + ); + case USER -> cmp = safeCompare( + a.info().user().orElse(""), + b.info().user().orElse("") + ); + case CPU -> cmp = Double.compare( + cpuOpt.map(c -> safeCpu(c, a.pid())).orElse(0.0), + cpuOpt.map(c -> safeCpu(c, b.pid())).orElse(0.0) + ); + case MEMORY -> cmp = Double.compare( + memOpt.map(m -> safeMemory(m, a.pid())).orElse(0.0), + memOpt.map(m -> safeMemory(m, b.pid())).orElse(0.0) + ); + + default -> cmp = 0; + } + } catch (Exception ignored) { } + return ascending ? cmp : -cmp; + }; + } + + /** + * Compares two strings in a case-insensitive manner, treating null as empty. + * + * @param a first string + * @param b second string + * @return comparison result + */ + private static int safeCompare(String a, String b) { + if (a == null) a = ""; + if (b == null) b = ""; + return a.compareToIgnoreCase(b); + } + + private static double safeMemory(IMemoryInfo mem, long pid) { + try { + return mem.getMemoryPercent(pid); + } catch (Exception e) { + return 0.0; + } + } + + private static double safeCpu(ICpuInfo cpu, long pid) { + try { + return cpu.getCpuPercent(pid); + } catch (Exception e) { + return 0.0; + } + } +} \ No newline at end of file diff --git a/src/jtop/core/ProcessState.java b/src/jtop/core/ProcessState.java new file mode 100644 index 0000000..ed29f45 --- /dev/null +++ b/src/jtop/core/ProcessState.java @@ -0,0 +1,68 @@ +package jtop.core; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; + +/** + * Utility class for retrieving and interpreting a process's current state. + *

+ * Reads the process status from /proc/[pid]/stat and maps + * the one-letter state code to a human-readable description. + *

+ *

+ * Typical Linux process states: + *

+ *
    + *
  • R - Running
  • + *
  • S - Sleeping
  • + *
  • D - Disk Sleep
  • + *
  • T - Stopped
  • + *
  • Z - Zombie
  • + *
  • X - Dead
  • + *
+ */ +public class ProcessState { + + /** + * Retrieves the current state of the specified process. + *

+ * Reads /proc/[pid]/stat and extracts the third field, + * which represents the process state as a single-character code. + *

+ * + * @param pid the process ID whose state should be retrieved + * @return a human-readable description of the process state, or "?" if unavailable + */ + public static String getState(long pid) { + String path = "/proc/" + pid + "/stat"; + try (BufferedReader reader = new BufferedReader(new FileReader(path))) { + String[] parts = reader.readLine().split("\\s+"); + // Field 3 is the process state (R, S, D, T, Z, etc.) + if (parts.length > 2) { + return parseState(parts[2]); + } + } catch (IOException e) { + return "?"; + } + return "?"; + } + + /** + * Converts the short one-letter state code from /proc/[pid]/stat + * into a descriptive string. + * + * @param s the single-character state code (e.g. "R", "S", "Z") + * @return the full human-readable process state + */ + private static String parseState(String s) { + switch (s) { + case "R": return "Running"; + case "S": return "Sleeping"; + case "D": return "Disk Sleep"; + case "T": return "Stopped"; + case "Z": return "Zombie"; + case "X": return "Dead"; + default: return s; + } + } +} \ No newline at end of file diff --git a/src/jtop/core/ProcessTableRenderer.java b/src/jtop/core/ProcessTableRenderer.java new file mode 100644 index 0000000..381b503 --- /dev/null +++ b/src/jtop/core/ProcessTableRenderer.java @@ -0,0 +1,163 @@ +package jtop.core; + +import java.util.ArrayList; +import java.util.List; + +import jtop.config.Config; +import jtop.terminal.Header; +import jtop.terminal.TerminalSize; +import jtop.system.linux.SystemSampler; + +/** + * Responsible for rendering the process table in the terminal. + *

+ * This class handles: + *

+ *
    + *
  • Color formatting for header, footer, and table rows.
  • + *
  • Column alignment based on terminal width and cell size.
  • + *
  • Displaying keybindings and scrolling status.
  • + *
+ */ +public class ProcessTableRenderer { + private final String tableColor; + private final String headerColor; + private final String footerColor; + private final String clearStyling; + private final String sortingArrowColor; + private static String keyBindings = ""; + + private final int cellWidth; + private final int pageSize; + private final SystemSampler sampler; + + /** + * Initializes the table renderer with configuration and layout settings. + * + * @param config configuration object containing color settings and footer text + * @param cellWidth width of each column in characters + * @param pageSize number of rows visible at a time + * @param sampler cached system information (CPU, memory, temps) + */ + public ProcessTableRenderer(Config config, int cellWidth, int pageSize, SystemSampler sampler) { + this.tableColor = config.getString("table.color", "\033[40m\033[37m"); + this.headerColor = config.getString("header.color", "\033[47m\033[30m"); + this.footerColor = config.getString("footer.color", "\033[41m\033[37m"); + this.clearStyling = "\033[0m"; + this.sortingArrowColor = "\033[31m"; + this.keyBindings = config.getString("footer.text.keybindings", + "Use j/k to scroll, Enter to scroll entire row, 'q' or Ctrl+C to quit"); + this.cellWidth = cellWidth; + this.pageSize = pageSize; + this.sampler = sampler; + } + + /** + * Returns the number of lines used by the header and footer. + * + * @return the number of lines used by the header and footer + */ + public static int getHeaderAndFooterLength() { + int length = 0; + length += Header.getRowsCount(); // header (system information) + length += 1; // table header (PID, NAME, etc.) + length += "-- Showing abc-def of xyz --".length() / TerminalSize.getColumns() + 1; // footer + length += keyBindings.length() / TerminalSize.getColumns() + 1; // keybindings text + return length; + } + + /** + * Draws the process table on the terminal. + * + * @param processes the list of processes to display + * @param infoTypes the columns to show (PID, NAME, CPU, etc.) + * @param sortBy the column currently used for sorting + * @param sortAsc true if sorting ascending, false if descending + * @param scrollIndex starting index for visible rows + * @param uptime system uptime in hours (cached) + * @param load system load average (cached) + */ + public void draw(List processes, List infoTypes, InfoType sortBy, boolean sortAsc, int scrollIndex, + double uptime, String load) { + TerminalSize terminalSize = new TerminalSize(); + int total = processes.size(); + int end = Math.min(scrollIndex + pageSize, total); + + // Clear screen + System.out.print("\033[H\033[2J"); + System.out.flush(); + + // Draw header with cached SystemSampler + Header.draw(sampler, uptime, load); + + // Print table header + printHeader(infoTypes, sortBy, sortAsc); + + // Print visible process rows + for (int i = scrollIndex; i < end; i++) { + printProcessRow(processes.get(i), infoTypes); + } + + // Print footer + String spaces = " ".repeat(Math.max(0, (terminalSize.getColumns() - 25) / 2)); + System.out.printf("\r%s%s-- Showing %d-%d of %d --%s\n", + spaces, footerColor, scrollIndex + 1, end, total, clearStyling); + System.out.print("\r" + keyBindings); + } + + /** + * Prints the table header with sorting indicators. + */ + private void printHeader(List infoTypes, InfoType sortBy, boolean sortAsc) { + List headers = new ArrayList<>(); + for (InfoType type : infoTypes) { + String name = type.name(); + if (type == InfoType.CPU || type == InfoType.MEMORY) name += " %"; + if (type == sortBy) name += sortAsc ? " ^" : " v"; + headers.add(name); + } + printRow(headerColor, headers); + } + + /** + * Prints a single row of process data. + */ + private void printProcessRow(ProcessRow row, List infoTypes) { + List cells = new ArrayList<>(); + for (InfoType type : infoTypes) { + switch (type) { + case PID -> cells.add(String.valueOf(row.pid)); + case NAME -> cells.add(row.name); + case PATH -> cells.add(row.path); + case USER -> cells.add(row.user); + case CPU -> cells.add(row.cpu); + case MEMORY -> cells.add(row.memory); + case DISK_READ -> cells.add("TODO_R"); + case DISK_WRITE -> cells.add("TODO_W"); + case NETWORK -> cells.add("TODO_NET"); + default -> cells.add("?"); + } + } + printRow("", cells); + } + + /** + * Prints a row with the given color and cells. + */ + private void printRow(String color, List cells) { + StringBuilder sb = new StringBuilder(); + for (String c : cells) { + sb.append(String.format("%-" + cellWidth + "s", truncate(c, cellWidth))); + } + System.out.println("\r" + tableColor + color + sb + clearStyling); + } + + /** + * Truncates a string to the given width. + */ + private String truncate(String s, int width) { + if (s == null) return ""; + if (s.length() > width - 1) return s.substring(0, width - 1); + return s; + } +} \ No newline at end of file diff --git a/src/jtop/core/RefreshThread.java b/src/jtop/core/RefreshThread.java new file mode 100644 index 0000000..1b2e090 --- /dev/null +++ b/src/jtop/core/RefreshThread.java @@ -0,0 +1,52 @@ +package jtop.core; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Background thread that periodically refreshes a {@link IRefreshable} component. + *

+ * The thread wakes up at a fixed interval (2 seconds) and calls {@link IRefreshable#refresh()}. + * Refreshing only occurs if the {@link AtomicBoolean} flag is set to {@code true}. + *

+ * This thread runs as a daemon, allowing the application to exit gracefully. + */ +public class RefreshThread extends Thread { + private final IRefreshable refreshable; + private final AtomicBoolean refresh; + + /** + * Constructs a new RefreshThread. + * + * @param refreshable the component to refresh periodically + * @param refresh atomic boolean flag controlling whether a refresh should occur + */ + public RefreshThread(IRefreshable refreshable, AtomicBoolean refresh) { + this.refreshable = refreshable; + this.refresh = refresh; + setDaemon(true); + } + + /** + * Main loop of the thread. + *

+ * Sleeps for 2 seconds between updates and refreshes the target object + * if the {@code refresh} flag is set to {@code true}. + *

+ * Exits cleanly when interrupted. + */ + @Override + public void run() { + while (!isInterrupted()) { + try { + Thread.sleep(2000); + if (refresh.get()) { + refreshable.refresh(); + } + } catch (InterruptedException e) { + // Stop thread on interrupt + return; + } catch (Exception e) { + // Log or ignore other exceptions + } + } + } +} \ No newline at end of file diff --git a/src/jtop/core/ShowProcesses.java b/src/jtop/core/ShowProcesses.java new file mode 100644 index 0000000..7bbf8d8 --- /dev/null +++ b/src/jtop/core/ShowProcesses.java @@ -0,0 +1,167 @@ +package jtop.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jtop.Isystem.ICpuInfo; +import jtop.Isystem.IMemoryInfo; +import jtop.Isystem.IPathInfo; +import jtop.Isystem.IUptime; +import jtop.Isystem.ITemperatureInfo; +import jtop.config.Config; +import jtop.terminal.TerminalSize; +import jtop.system.Feature; +import jtop.system.SystemInfoFactory; +import jtop.system.linux.SystemSampler; + +/** + * Core class responsible for managing, sorting, and displaying running processes. + */ +public class ShowProcesses implements IRefreshable { + private final List infoTypes; + private final Config config = new Config(); + + private InfoType sortBy = InfoType.CPU; + private boolean sortAsc = config.getBoolean("table.sorting.ASC", false); + + private int scrollIndex = 0; + private int pageSize; + private int cellWidth; + + private List cachedProcesses = new ArrayList<>(); + + // system sampler for cached CPU, memory, temps + private final SystemSampler sampler = new SystemSampler(); + + /** + * Constructs a ShowProcesses instance with the specified columns to display. + */ + public ShowProcesses(InfoType... infos) { + infoTypes = List.of(infos); + } + + /** + * Refreshes the cached list of process rows and system sampler. + */ + public void refreshProcesses() throws Exception { + // Fetch system features + IUptime uptimeInfo = SystemInfoFactory.getFeature(Feature.UPTIME).map(f -> (IUptime) f).orElse(null); + ICpuInfo cpuInfo = SystemInfoFactory.getFeature(Feature.CPU).map(f -> (ICpuInfo) f).orElse(null); + IMemoryInfo memoryInfo = SystemInfoFactory.getFeature(Feature.MEMORY).map(f -> (IMemoryInfo) f).orElse(null); + ITemperatureInfo tempInfo = SystemInfoFactory.getFeature(Feature.TEMPERATURE).map(f -> (ITemperatureInfo) f).orElse(null); + IPathInfo pathInfo = SystemInfoFactory.getFeature(Feature.PROCESS).map(f -> (IPathInfo) f).orElse(null); + + if (pathInfo instanceof jtop.system.linux.PathInfo pi) { + pi.clearCache(); + } + + // Update system sampler + sampler.refresh(cpuInfo, memoryInfo, tempInfo); + + // cache memory usage per process + Map memCache = memoryInfo != null ? new HashMap<>() : null; + + // fetch all processes and sort + List processes = new ArrayList<>(ProcessHandle.allProcesses().toList()); + processes.sort(ProcessSorter.getComparator(sortBy, sortAsc)); + + List rows = new ArrayList<>(processes.size()); + + for (ProcessHandle ph : processes) { + long pid = ph.pid(); + try { + String name = pathInfo != null ? safe(pathInfo.getName(pid)) : "?"; + String path = pathInfo != null ? safe(pathInfo.getPath(pid)) : "?"; + String user = ph.info().user().orElse("Unknown"); + + String cpuPercent = cpuInfo != null ? String.valueOf(safeCpu(cpuInfo, pid)) : "?"; + + String memPercent; + if (memoryInfo != null) { + Double cached = memCache.get(pid); + if (cached == null) { + double val = safeMemory(memoryInfo, pid); + memCache.put(pid, val); + memPercent = String.valueOf(val); + } else { + memPercent = String.valueOf(cached); + } + } else { + memPercent = "?"; + } + + rows.add(new ProcessRow(pid, name, path, user, cpuPercent, memPercent)); + } catch (Exception ignored) {} + } + + cachedProcesses = rows; + } + + /** + * Draws the process table to the terminal using cached system sampler. + */ + public void draw() throws Exception { + TerminalSize terminalSize = new TerminalSize(); + this.pageSize = terminalSize.getRows() - ProcessTableRenderer.getHeaderAndFooterLength(); + this.cellWidth = terminalSize.getColumns() / infoTypes.size(); + + if (cachedProcesses.isEmpty()) { + refreshProcesses(); + } + + double uptime = 0.0; + String load = "?"; + + try { + IUptime uptimeInfo = SystemInfoFactory.getFeature(Feature.UPTIME).map(f -> (IUptime) f).orElse(null); + ICpuInfo cpuInfo = SystemInfoFactory.getFeature(Feature.CPU).map(f -> (ICpuInfo) f).orElse(null); + if (uptimeInfo != null) uptime = uptimeInfo.getSystemUptime('h'); + if (cpuInfo != null) load = cpuInfo.getLoadAverage(); + } catch (Exception ignored) {} + + new ProcessTableRenderer(config, cellWidth, pageSize, sampler) + .draw(cachedProcesses, infoTypes, sortBy, sortAsc, scrollIndex, uptime, load); + } + + public void scrollUp() { if (scrollIndex > 0) scrollIndex--; } + + public void scrollDown() { + if (scrollIndex + pageSize < cachedProcesses.size()) scrollIndex++; + } + + public void changeSortByClick(int charPosition) throws Exception { + int columnIndex = charPosition / cellWidth; + changeSort(columnIndex); + } + + public void changeSort(int columnIndex) throws Exception { + if (columnIndex >= 0 && columnIndex < infoTypes.size()) { + InfoType newSort = infoTypes.get(columnIndex); + sortAsc = (sortBy == newSort) ? !sortAsc : true; + sortBy = newSort; + refreshProcesses(); + } + } + + private String safe(String s) { return s != null ? s : "?"; } + + private static double safeCpu(ICpuInfo cpu, long pid) { + try { return cpu.getCpuPercent(pid); } catch (Exception e) { return 0.0; } + } + + private static double safeMemory(IMemoryInfo mem, long pid) { + try { return mem.getMemoryPercent(pid); } catch (Exception e) { return 0.0; } + } + + @Override + public void refresh() { + try { + refreshProcesses(); + draw(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/jtop/system/Feature.java b/src/jtop/system/Feature.java new file mode 100644 index 0000000..9597319 --- /dev/null +++ b/src/jtop/system/Feature.java @@ -0,0 +1,40 @@ +package jtop.system; + +/** + * Represents a system feature that can be dynamically implemented for different operating systems. + *

+ * Each feature stores the name of its default implementation class, which is used + * by {@link SystemInfoFactory} to instantiate the appropriate OS-specific implementation. + *

+ */ +public enum Feature { + CPU("CpuInfo"), + MEMORY("MemoryInfo"), + DISK("DiskInfo"), + NETWORK("NetworkInfo"), + TEMPERATURE("TemperatureInfo"), + BATTERY("BatteryInfo"), + UPTIME("Uptime"), + PROCESS("PathInfo"); + + /** Name of the implementation class for this feature. */ + private final String implClassName; + + /** + * Constructs a feature with its associated implementation class name. + * + * @param implClassName the default class name implementing this feature + */ + Feature(String implClassName) { + this.implClassName = implClassName; + } + + /** + * Returns the implementation class name associated with this feature. + * + * @return the class name of the implementation + */ + public String getImplementationClassName() { + return implClassName; + } +} \ No newline at end of file diff --git a/src/jtop/system/FeatureResolver.java b/src/jtop/system/FeatureResolver.java new file mode 100644 index 0000000..f1e268f --- /dev/null +++ b/src/jtop/system/FeatureResolver.java @@ -0,0 +1,36 @@ +package jtop.system; + +import java.util.EnumSet; + +import jtop.system.linux.LinuxFeatures; +import jtop.system.freebsd.FreeBsdFeatures; +import jtop.system.mac.MacFeatures; + +/** + * Resolves which system features are supported on a given operating system. + *

+ * Provides a centralized way to query the supported {@link Feature}s for + * Linux, FreeBSD, and macOS without hardcoding OS-specific logic elsewhere. + *

+ */ +public final class FeatureResolver { + + /** + * Private constructor to prevent instantiation. + */ + private FeatureResolver() {} + + /** + * Returns the set of features supported by the given operating system. + * + * @param os the {@link OperatingSystem} to query + * @return an {@link EnumSet} of {@link Feature} representing supported features + */ + public static EnumSet supported(OperatingSystem os) { + return switch (os) { + case LINUX -> LinuxFeatures.SUPPORTED; + case FREEBSD -> FreeBsdFeatures.SUPPORTED; + case MAC -> MacFeatures.SUPPORTED; + }; + } +} \ No newline at end of file diff --git a/src/jtop/system/OperatingSystem.java b/src/jtop/system/OperatingSystem.java new file mode 100644 index 0000000..4738ec0 --- /dev/null +++ b/src/jtop/system/OperatingSystem.java @@ -0,0 +1,36 @@ +package jtop.system; + +/** + * Enum representing supported operating systems. + *

+ * Provides a method to detect the current operating system at runtime. + *

+ */ +public enum OperatingSystem { + /** Linux operating system. */ + LINUX, + /** FreeBSD operating system. */ + FREEBSD, + /** macOS operating system. */ + MAC; + + /** + * Detects the current operating system. + *

+ * Uses the system property {@code os.name} to determine the OS. + * Returns the corresponding {@link OperatingSystem} enum value. + *

+ * + * @return the detected {@link OperatingSystem} + * @throws UnsupportedOperationException if the OS is not supported + */ + public static OperatingSystem detect() { + String os = System.getProperty("os.name").toLowerCase(); + + if (os.contains("linux")) return LINUX; + if (os.contains("freebsd")) return FREEBSD; + if (os.contains("mac")) return MAC; + + throw new UnsupportedOperationException("Unsupported OS: " + os); + } +} \ No newline at end of file diff --git a/src/jtop/system/SystemInfoFactory.java b/src/jtop/system/SystemInfoFactory.java new file mode 100644 index 0000000..499aa3b --- /dev/null +++ b/src/jtop/system/SystemInfoFactory.java @@ -0,0 +1,61 @@ +package jtop.system; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Factory to provide system information implementations dynamically + * based on the current OS and available features. + *

+ * Fully enum-driven: no need to manually add getters for each feature. + */ +public final class SystemInfoFactory { + + private static final OperatingSystem OS = OperatingSystem.detect(); + private static final Set SUPPORTED_FEATURES = FeatureResolver.supported(OS); + + private SystemInfoFactory() {} + + /** + * Returns an implementation of the requested feature if available for this OS. + * + * @param feature the feature to request + * @return Optional containing the implementation, empty if not supported + */ + @SuppressWarnings("unchecked") + public static Optional getFeature(Feature feature) { + if (!SUPPORTED_FEATURES.contains(feature)) return Optional.empty(); + + String className = String.format( + "jtop.system.%s.%s", + OS.name().toLowerCase(), + feature.getImplementationClassName() + ); + + try { + Class clazz = Class.forName(className); + return Optional.of((T) clazz.getDeclaredConstructor().newInstance()); + } catch (Exception e) { + System.err.println("Failed to load " + className + ": " + e.getMessage()); + return Optional.empty(); + } + } + + /** + * Returns all supported features for this OS. + * Can be used to dynamically display available features in UI or CLI. + */ + public static Set supportedFeatures() { + return Collections.unmodifiableSet(SUPPORTED_FEATURES); + } + + /** + * Returns a map of all available features to their implementations. + * Features not supported on this OS are skipped. + */ + public static Map allAvailableFeatures() { + return SUPPORTED_FEATURES.stream() + .flatMap(f -> getFeature(f).map(inst -> Map.entry(f, inst)).stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} diff --git a/src/jtop/system/freebsd/FreeBsdFeatures.java b/src/jtop/system/freebsd/FreeBsdFeatures.java new file mode 100644 index 0000000..0528dfa --- /dev/null +++ b/src/jtop/system/freebsd/FreeBsdFeatures.java @@ -0,0 +1,28 @@ +package jtop.system.freebsd; + +import java.util.EnumSet; +import jtop.system.Feature; + +/** + * Defines the set of system features supported on FreeBSD. + *

+ * Each operating system has its own feature class (e.g., {@link jtop.system.linux.LinuxFeatures}) + * that lists which features are implemented and available. + *

+ */ +public final class FreeBsdFeatures { + + /** + * The set of features currently supported on FreeBSD. + *

+ * This is used by {@link jtop.system.FeatureResolver} to determine at runtime + * which features can be instantiated via {@link jtop.system.SystemInfoFactory}. + *

+ */ + public static final EnumSet SUPPORTED = EnumSet.of( + Feature.PROCESS + ); + + /** Private constructor to prevent instantiation of this utility class. */ + private FreeBsdFeatures() {} +} \ No newline at end of file diff --git a/src/jtop/system/freebsd/PathInfo.java b/src/jtop/system/freebsd/PathInfo.java new file mode 100644 index 0000000..076c203 --- /dev/null +++ b/src/jtop/system/freebsd/PathInfo.java @@ -0,0 +1,53 @@ +package jtop.system.freebsd; + +import java.util.Optional; +import jtop.Isystem.IPathInfo; + +/** + * Provides utilities to retrieve process path information. + *

+ * Uses {@link ProcessHandle} to fetch details about a running process, + * including its command (full path) and executable name. + *

+ */ +public class PathInfo implements IPathInfo { + + /** + * Returns the name of the executable for the given process ID. + *

+ * For example, if the command is "/usr/bin/java", this will return "java". + *

+ * + * @param pid the process ID + * @return the executable name, or "Unknown" if the process does not exist + */ + @Override + public String getName(long pid) { + Optional ph = ProcessHandle.of(pid); + if (ph.isPresent()) { + ProcessHandle.Info info = ph.get().info(); + String command = info.command().orElse("Unknown"); + return command.substring(command.lastIndexOf("/") + 1); + } + return "Unknown"; + } + + /** + * Returns the full command path of the executable for the given process ID. + *

+ * For example, "/usr/bin/java". + *

+ * + * @param pid the process ID + * @return the full command path, or "Unknown" if the process does not exist + */ + @Override + public String getPath(long pid) { + Optional ph = ProcessHandle.of(pid); + if (ph.isPresent()) { + ProcessHandle.Info info = ph.get().info(); + return info.command().orElse("Unknown"); + } + return "Unknown"; + } +} \ No newline at end of file diff --git a/src/jtop/system/linux/BatteryInfo.java b/src/jtop/system/linux/BatteryInfo.java new file mode 100644 index 0000000..63c31bd --- /dev/null +++ b/src/jtop/system/linux/BatteryInfo.java @@ -0,0 +1,160 @@ +package jtop.system.linux; + +import java.io.IOException; +import java.nio.file.*; +import java.util.Optional; +import jtop.Isystem.IBatteryInfo; + +/** + * Utility class for retrieving battery information on Linux systems. + *

+ * Automatically detects the battery directory under + * /sys/class/power_supply/ (e.g. BAT0, BAT1) + * and exposes percentage, status, voltage, energy, and power readings. + *

+ * Implements {@link IBatteryInfo} so it can be used in a cross-platform interface-based design. + */ +public class BatteryInfo implements IBatteryInfo { + + /** Path to the power supply directory on Linux */ + private static final Path POWER_SUPPLY_PATH = Path.of("/sys/class/power_supply"); + + /** Path to the detected battery directory (e.g., BAT0) */ + private Path batteryPath; + + /** + * Constructs a BatteryInfo instance and tries to detect the battery path. + */ + public BatteryInfo() { + batteryPath = detectBatteryPath(); + } + + /** + * Detects the first available battery path under /sys/class/power_supply/. + * + * @return Path to the battery directory, or null if not found + */ + private Path detectBatteryPath() { + if (!Files.isDirectory(POWER_SUPPLY_PATH)) return null; + + try (var stream = Files.list(POWER_SUPPLY_PATH)) { + Optional battery = stream + .filter(p -> p.getFileName().toString().startsWith("BAT")) + .findFirst(); + return battery.orElse(null); + } catch (IOException e) { + return null; + } + } + + /** + * Reads a value from a given file path inside the battery directory. + * + * @param filename the name of the file to read + * @return the file's trimmed string contents, or null if unavailable + */ + private String readBatteryFile(String filename) { + if (batteryPath == null) return null; + + Path file = batteryPath.resolve(filename); + if (!Files.exists(file)) return null; + + try { + return Files.readString(file).trim(); + } catch (IOException e) { + return null; + } + } + + /** + * Gets the current battery charge percentage (0–100). + * + * @return battery percentage, or -1 if unavailable + */ + @Override + public int getBatteryPercentage() { + String content = readBatteryFile("capacity"); + if (content == null) return -1; + + try { + return Integer.parseInt(content); + } catch (NumberFormatException e) { + return -1; + } + } + + /** + * Gets the current battery status (e.g. Charging, Discharging, Full). + * + * @return status string, or "Unknown" if unavailable + */ + @Override + public String getBatteryStatus() { + String status = readBatteryFile("status"); + return status != null ? status : "Unknown"; + } + + /** + * Gets the current voltage in volts (if available). + * + * @return voltage in volts, or -1 if unavailable + */ + @Override + public double getVoltage() { + String content = readBatteryFile("voltage_now"); + if (content == null) return -1; + + try { + // value is usually in microvolts + return Double.parseDouble(content) / 1_000_000.0; + } catch (NumberFormatException e) { + return -1; + } + } + + /** + * Gets the current energy in watt-hours (if available). + * + * @return energy in Wh, or -1 if unavailable + */ + @Override + public double getEnergy() { + String content = readBatteryFile("energy_now"); + if (content == null) return -1; + + try { + // value is usually in microwatt-hours + return Double.parseDouble(content) / 1_000_000.0; + } catch (NumberFormatException e) { + return -1; + } + } + + /** + * Gets the current power draw in watts (if available). + * + * @return power in W, or -1 if unavailable + */ + @Override + public double getPower() { + String content = readBatteryFile("power_now"); + if (content == null) return -1; + + try { + // value is usually in microwatts + return Double.parseDouble(content) / 1_000_000.0; + } catch (NumberFormatException e) { + return -1; + } + } + + /** + * Checks whether the system has a readable battery directory. + * + * @return true if battery is present and readable, false otherwise + */ + @Override + public boolean hasBattery() { + return batteryPath != null && Files.exists(batteryPath); + } +} diff --git a/src/jtop/system/linux/CpuInfo.java b/src/jtop/system/linux/CpuInfo.java new file mode 100644 index 0000000..e423125 --- /dev/null +++ b/src/jtop/system/linux/CpuInfo.java @@ -0,0 +1,111 @@ +package jtop.system.linux; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import jtop.Isystem.ICpuInfo; + +/** + * Provides CPU usage information and statistics for the system and individual processes. + *

+ * Reads data from the /proc filesystem on Linux: + *

+ *
    + *
  • /proc/[pid]/stat for per-process CPU usage
  • + *
  • /proc/stat for overall CPU usage
  • + *
  • /proc/loadavg for system load average
  • + *
+ */ +public class CpuInfo implements ICpuInfo { + + /** Number of decimal places to round CPU percentage values. */ + private static final int DECIMALS = 3; + + /** + * Computes the CPU usage percentage of a specific process. + * + * @param pid the process ID + * @return CPU usage as a percentage, or -1 if unavailable + */ + @Override + public double getCpuPercent(long pid) { + try { + String stat = Files.readString(Paths.get("/proc/" + pid + "/stat")); + String[] parts = stat.split("\\s+"); + + long utime = Long.parseLong(parts[13]); + long stime = Long.parseLong(parts[14]); + long totalTime = utime + stime; + + double uptimeSeconds = new Uptime().getSystemUptime('s'); + + double percent = (100d * totalTime / uptimeSeconds) / Runtime.getRuntime().availableProcessors(); + + double factor = Math.pow(10, DECIMALS); + return Math.round(percent * factor) / factor; + } catch (Exception e) { + return -1; + } + } + + /** + * Retrieves the system load average as reported by /proc/loadavg. + * + * @return the load average string, or null if unavailable + */ + @Override + public String getLoadAverage() { + try { + return Files.readString(Path.of("/proc/loadavg")).trim(); + } catch (IOException e) { + return null; + } + } + + /** + * Computes the overall CPU usage percentage over a sample period. + * + * @param sampleMs the sample duration in milliseconds + * @return the CPU usage as a percentage over the sample period, or -1 if unavailable + */ + @Override + public double getCpuUsage(long sampleMs) { + try { + long[] first = readCpuStat(); + Thread.sleep(sampleMs); + long[] second = readCpuStat(); + + long idle1 = first[3]; + long idle2 = second[3]; + long total1 = Arrays.stream(first).sum(); + long total2 = Arrays.stream(second).sum(); + + long totalDelta = total2 - total1; + long idleDelta = idle2 - idle1; + return 100.0 * (totalDelta - idleDelta) / totalDelta; + } catch (Exception e) { + return -1; + } + } + + /** + * Reads the system-wide CPU statistics from /proc/stat. + * + * @return an array of CPU time values (user, nice, system, idle, etc.), or null if unavailable + */ + private long[] readCpuStat() { + try (BufferedReader br = Files.newBufferedReader(Path.of("/proc/stat"))) { + String[] parts = br.readLine().trim().split("\\s+"); + long[] vals = new long[parts.length - 1]; + for (int i = 1; i < parts.length; i++) { + vals[i - 1] = Long.parseLong(parts[i]); + } + return vals; + } catch (IOException e) { + return null; + } + } +} diff --git a/src/jtop/system/linux/DiskInfo.java b/src/jtop/system/linux/DiskInfo.java new file mode 100644 index 0000000..48a49c1 --- /dev/null +++ b/src/jtop/system/linux/DiskInfo.java @@ -0,0 +1,54 @@ +package jtop.system.linux; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +import jtop.Isystem.IDiskInfo; + +/** + * Provides information about disk usage and I/O statistics for mounted devices. + *

+ * Reads data from /proc/diskstats and parses the number of + * reads and writes for each block device. The results can be used to + * monitor disk I/O activity or calculate total/used storage if combined + * with filesystem information. + *

+ */ +public class DiskInfo implements IDiskInfo { + + /** + * Retrieves disk I/O statistics for all block devices. + *

+ * Each entry in the returned map contains the device name as the key, + * and an array of two long values as the value: + *

+ *
    + *
  • index 0 - number of reads completed
  • + *
  • index 1 - number of writes completed
  • + *
+ * + * @return a {@link Map} where the key is the device name and the value is a + * long array containing [reads, writes] + * @throws IOException if reading /proc/diskstats fails + */ + @Override + public Map getDiskStats() throws IOException { + Map map = new LinkedHashMap<>(); + try (BufferedReader br = Files.newBufferedReader(Path.of("/proc/diskstats"))) { + String line; + while ((line = br.readLine()) != null) { + String[] parts = line.trim().split("\\s+"); + if (parts.length < 14) continue; // Skip incomplete lines + String device = parts[2]; + long reads = Long.parseLong(parts[3]); + long writes = Long.parseLong(parts[7]); + map.put(device, new long[]{reads, writes}); + } + } + return map; + } +} \ No newline at end of file diff --git a/src/jtop/system/linux/LinuxFeatures.java b/src/jtop/system/linux/LinuxFeatures.java new file mode 100644 index 0000000..5da0746 --- /dev/null +++ b/src/jtop/system/linux/LinuxFeatures.java @@ -0,0 +1,35 @@ +package jtop.system.linux; + +import java.util.EnumSet; +import jtop.system.Feature; + +/** + * Defines the set of system features supported on Linux. + *

+ * Each operating system has its own feature class (e.g., {@link jtop.system.freebsd.FreeBsdFeatures}) + * that lists which features are implemented and available. + *

+ */ +public final class LinuxFeatures { + + /** + * The set of features currently supported on Linux. + *

+ * This is used by {@link jtop.system.FeatureResolver} to determine at runtime + * which features can be instantiated via {@link jtop.system.SystemInfoFactory}. + *

+ */ + public static final EnumSet SUPPORTED = EnumSet.of( + Feature.CPU, + Feature.MEMORY, + Feature.DISK, + Feature.NETWORK, + Feature.TEMPERATURE, + Feature.BATTERY, + Feature.UPTIME, + Feature.PROCESS + ); + + /** Private constructor to prevent instantiation of this utility class. */ + private LinuxFeatures() {} +} \ No newline at end of file diff --git a/src/jtop/system/linux/MemoryInfo.java b/src/jtop/system/linux/MemoryInfo.java new file mode 100644 index 0000000..bd80079 --- /dev/null +++ b/src/jtop/system/linux/MemoryInfo.java @@ -0,0 +1,178 @@ +package jtop.system.linux; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import jtop.Isystem.IMemoryInfo; + +/** + * Provides methods to gather memory usage statistics. + *

+ * Reads Linux /proc files to determine: + *

+ *
    + *
  • Total and available system memory
  • + *
  • Memory usage percentage by the system
  • + *
  • Memory usage percentage of a specific process (by PID)
  • + *
+ * + *

+ * Performance notes: + *

    + *
  • /proc/meminfo is cached for a short time window
  • + *
  • No regex usage
  • + *
  • No temporary Maps or Lists
  • + *
+ *

+ */ +public class MemoryInfo implements IMemoryInfo { + + /** Typical memory page size on Linux in bytes. */ + private static final long PAGE_SIZE = 4096; + + /** Cache validity in milliseconds. */ + private static final long MEMINFO_CACHE_MS = 500; + + private static long lastRead; + + private static long memTotalKb; + private static long memAvailableKb; + private static long memFreeKb; + private static long buffersKb; + private static long cachedKb; + private static long sReclaimableKb; + private static long shmemKb; + + /** + * Returns the memory usage percentage of a process. + * + * @param pid the process ID + * @return memory usage percentage of the process + * @throws IOException if the /proc file cannot be read or is malformed + */ + @Override + public double getMemoryPercent(long pid) throws IOException { + readMemInfoCached(); + + Path statmPath = Path.of("/proc", String.valueOf(pid), "statm"); + if (!Files.exists(statmPath)) { + throw new IOException("Process with PID " + pid + " does not exist"); + } + + String statm = Files.readString(statmPath).trim(); + int space = statm.indexOf(' '); + if (space < 0) { + throw new IOException("Unexpected format in /proc/" + pid + "/statm"); + } + + long rssPages = Long.parseLong(statm.substring(space + 1).trim().split(" ")[0]); + long processKb = (rssPages * PAGE_SIZE) / 1024; + + double percent = (processKb / (double) memTotalKb) * 100.0; + return round(percent, 3); + } + + /** + * Returns the overall memory usage percentage of the system. + * + * @return memory usage percentage of the system + * @throws IOException if /proc/meminfo cannot be read + */ + @Override + public double getMemoryUsage() throws IOException { + readMemInfoCached(); + + long free = memFreeKb + + buffersKb + + cachedKb + + sReclaimableKb + - shmemKb; + + double usedPercent = 100.0 * (memTotalKb - free) / memTotalKb; + return round(usedPercent, 2); + } + + /** + * Returns total system memory in bytes. + * + * @return total memory in bytes + * @throws IOException if /proc/meminfo cannot be read + */ + @Override + public long getTotalMemoryBytes() throws IOException { + readMemInfoCached(); + return memTotalKb * 1024; + } + + /** + * Returns used memory in bytes (total minus available memory). + * + * @return used memory in bytes + * @throws IOException if /proc/meminfo cannot be read + */ + @Override + public long getAvailableMemoryBytes() throws IOException { + readMemInfoCached(); + return (memTotalKb - memAvailableKb) * 1024; + } + + /** + * Reads /proc/meminfo and caches values for a short time window. + */ + private static void readMemInfoCached() throws IOException { + long now = System.currentTimeMillis(); + if (now - lastRead < MEMINFO_CACHE_MS) { + return; + } + + try (BufferedReader br = Files.newBufferedReader(Path.of("/proc/meminfo"))) { + String line; + while ((line = br.readLine()) != null) { + if (line.startsWith("MemTotal:")) { + memTotalKb = parseKb(line); + } else if (line.startsWith("MemAvailable:")) { + memAvailableKb = parseKb(line); + } else if (line.startsWith("MemFree:")) { + memFreeKb = parseKb(line); + } else if (line.startsWith("Buffers:")) { + buffersKb = parseKb(line); + } else if (line.startsWith("Cached:")) { + cachedKb = parseKb(line); + } else if (line.startsWith("SReclaimable:")) { + sReclaimableKb = parseKb(line); + } else if (line.startsWith("Shmem:")) { + shmemKb = parseKb(line); + } + } + } + + lastRead = now; + } + + /** + * Parses a line of /proc/meminfo and returns the value in kB. + */ + private static long parseKb(String line) { + int i = line.indexOf(':') + 1; + while (line.charAt(i) == ' ') { + i++; + } + + long val = 0; + while (i < line.length() && Character.isDigit(line.charAt(i))) { + val = val * 10 + (line.charAt(i++) - '0'); + } + return val; + } + + /** + * Rounds a double value to the given number of decimal places. + * + * @return returns the value to the desired length + */ + private static double round(double val, int decimals) { + double factor = Math.pow(10, decimals); + return Math.round(val * factor) / factor; + } +} \ No newline at end of file diff --git a/src/jtop/system/linux/NetworkInfo.java b/src/jtop/system/linux/NetworkInfo.java new file mode 100644 index 0000000..8bd344c --- /dev/null +++ b/src/jtop/system/linux/NetworkInfo.java @@ -0,0 +1,53 @@ +package jtop.system.linux; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +import jtop.Isystem.INetworkInfo; + +/** + * Provides methods to collect network usage statistics. + *

+ * Reads from the Linux file /proc/net/dev to retrieve + * the number of bytes received (RX) and transmitted (TX) per network interface. + *

+ */ +public class NetworkInfo implements INetworkInfo { + + /** + * Retrieves the current network usage for all network interfaces. + * + * @return a map where the key is the interface name (e.g., "eth0") and + * the value is a long array of size 2: + *
    + *
  • index 0: bytes received (RX)
  • + *
  • index 1: bytes transmitted (TX)
  • + *
+ * @throws IOException if /proc/net/dev cannot be read + */ + @Override + public Map getNetworkUsage() throws IOException { + Map map = new LinkedHashMap<>(); + + try (BufferedReader br = Files.newBufferedReader(Path.of("/proc/net/dev"))) { + // Skip header lines + br.lines().skip(2).forEach(line -> { + String[] parts = line.split(":"); + if (parts.length < 2) return; + + String iface = parts[0].trim(); + String[] nums = parts[1].trim().split("\\s+"); + long rx = Long.parseLong(nums[0]); + long tx = Long.parseLong(nums[8]); + + map.put(iface, new long[]{rx, tx}); + }); + } + + return map; + } +} \ No newline at end of file diff --git a/src/jtop/system/linux/PathInfo.java b/src/jtop/system/linux/PathInfo.java new file mode 100644 index 0000000..b719450 --- /dev/null +++ b/src/jtop/system/linux/PathInfo.java @@ -0,0 +1,90 @@ +package jtop.system.linux; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import jtop.Isystem.IPathInfo; + +/** + * Provides utilities to retrieve process path information. + *

+ * Uses {@link ProcessHandle} to fetch details about a running process, + * including its command (full path) and executable name. + *

+ * + *

+ * Performance notes: + *

    + *
  • Results are cached per PID
  • + *
  • ProcessHandle is queried only once per PID
  • + *
+ *

+ */ +public class PathInfo implements IPathInfo { + + private static final String UNKNOWN = "Unknown"; + + /** Cache full command path per PID */ + private final Map pathCache = new HashMap<>(); + + /** Cache executable name per PID */ + private final Map nameCache = new HashMap<>(); + + /** + * Returns the name of the executable for the given process ID. + * + * @param pid the process ID + * @return the executable name, or "Unknown" if the process does not exist + */ + @Override + public String getName(long pid) { + String cached = nameCache.get(pid); + if (cached != null) { + return cached; + } + + String path = getPath(pid); + if (UNKNOWN.equals(path)) { + return UNKNOWN; + } + + int idx = path.lastIndexOf('/'); + String name = idx >= 0 ? path.substring(idx + 1) : path; + + nameCache.put(pid, name); + return name; + } + + /** + * Returns the full command path of the executable for the given process ID. + * + * @param pid the process ID + * @return the full command path, or "Unknown" if the process does not exist + */ + @Override + public String getPath(long pid) { + String cached = pathCache.get(pid); + if (cached != null) { + return cached; + } + + Optional ph = ProcessHandle.of(pid); + if (ph.isEmpty()) { + return UNKNOWN; + } + + String path = ph.get().info().command().orElse(UNKNOWN); + pathCache.put(pid, path); + + return path; + } + + /** + * Clears cached entries. + * Should be called periodically to remove dead PIDs. + */ + public void clearCache() { + pathCache.clear(); + nameCache.clear(); + } +} diff --git a/src/jtop/system/linux/SystemSampler.java b/src/jtop/system/linux/SystemSampler.java new file mode 100644 index 0000000..3991939 --- /dev/null +++ b/src/jtop/system/linux/SystemSampler.java @@ -0,0 +1,68 @@ +package jtop.system.linux; + +import jtop.Isystem.ICpuInfo; +import jtop.Isystem.IMemoryInfo; +import jtop.Isystem.ITemperatureInfo; + +import java.io.IOException; +import java.util.Map; + +/** + * Caches CPU, memory, and temperature readings to avoid repeated blocking IO. + */ +public class SystemSampler { + + private double lastCpuUsage; + private double lastMemPercent; + private Map lastTemps; + private double totalMemoryBytes = 0; + + /** + * Initialize total memory once. + */ + public void initTotalMemory(IMemoryInfo mem) { + try { + totalMemoryBytes = mem.getTotalMemoryBytes(); + } catch (IOException e) { + totalMemoryBytes = 0; + System.err.println("Failed to read total memory: " + e.getMessage()); + } + } + + /** + * Refreshes cached CPU, memory, and temperature info. + * If total memory was not initialized, it will attempt to cache it now. + */ + public void refresh(ICpuInfo cpu, IMemoryInfo mem, ITemperatureInfo temps) { + try { + lastCpuUsage = cpu.getCpuUsage(100); // CPU usage snapshot + } catch (Exception e) { + lastCpuUsage = 0; + } + + try { + lastMemPercent = mem.getMemoryUsage(); + + // Ensure total memory is cached + if (totalMemoryBytes == 0) { + totalMemoryBytes = mem.getTotalMemoryBytes(); + } + } catch (Exception e) { + lastMemPercent = 0; + if (totalMemoryBytes == 0) { + totalMemoryBytes = 0; + } + } + + try { + lastTemps = temps != null ? temps.getTemperatures() : Map.of(); + } catch (Exception e) { + lastTemps = Map.of(); + } + } + + public double getCpu() { return lastCpuUsage; } + public double getMem() { return lastMemPercent; } + public Map getTemps() { return lastTemps; } + public double getTotalMemoryBytes() { return totalMemoryBytes; } +} \ No newline at end of file diff --git a/src/jtop/system/linux/TemperatureInfo.java b/src/jtop/system/linux/TemperatureInfo.java new file mode 100644 index 0000000..203975d --- /dev/null +++ b/src/jtop/system/linux/TemperatureInfo.java @@ -0,0 +1,89 @@ +package jtop.system.linux; + +import java.io.IOException; +import java.nio.file.*; +import java.util.*; +import jtop.Isystem.ITemperatureInfo; + +/** + * Provides system temperature readings from hardware sensors. + *

+ * Temperature sources: + *

    + *
  • Primary: /sys/class/hwmon
  • + *
  • Fallback: /sys/class/thermal
  • + *
+ * Each temperature is returned in degrees Celsius. + */ +public class TemperatureInfo implements ITemperatureInfo { + + /** + * Retrieves a map of all detected temperatures on the system. + * + * @return Map where the key is a sensor name (e.g., "coretemp:Core 0") + * and the value is the temperature in °C. + * @throws IOException if the sensor directories cannot be read + */ + @Override + public Map getTemperatures() throws IOException { + Map temps = new LinkedHashMap<>(); + + // --- Primary source: /sys/class/hwmon --- + Path hwmonBase = Path.of("/sys/class/hwmon"); + if (Files.isDirectory(hwmonBase)) { + try (DirectoryStream hwmons = Files.newDirectoryStream(hwmonBase)) { + for (Path hwmon : hwmons) { + String name = readTrimmed(hwmon.resolve("name"), "hwmon"); + + try (DirectoryStream files = Files.newDirectoryStream(hwmon, "temp*_input")) { + for (Path tempFile : files) { + String base = tempFile.getFileName().toString().replace("_input", ""); + String label = readTrimmed(hwmon.resolve(base + "_label"), base); + double value = readTempMilliC(tempFile); + temps.put(name + ":" + label, value); + } + } catch (IOException ignored) { + // Ignore unreadable hwmon entries + } + } + } + } + + // --- Fallback: /sys/class/thermal --- + if (temps.isEmpty()) { + Path thermalBase = Path.of("/sys/class/thermal"); + if (Files.isDirectory(thermalBase)) { + try (DirectoryStream zones = Files.newDirectoryStream(thermalBase, "thermal_zone*")) { + for (Path zone : zones) { + Path typeFile = zone.resolve("type"); + Path tempFile = zone.resolve("temp"); + if (Files.exists(typeFile) && Files.exists(tempFile)) { + String type = readTrimmed(typeFile, "zone"); + double temp = readTempMilliC(tempFile); + temps.put(type, temp); + } + } + } + } + } + + return temps; + } + + private String readTrimmed(Path path, String fallback) { + try { + return Files.exists(path) ? Files.readString(path).trim() : fallback; + } catch (IOException e) { + return fallback; + } + } + + private double readTempMilliC(Path path) { + try { + String str = Files.readString(path).trim(); + return Double.parseDouble(str) / 1000.0; + } catch (IOException | NumberFormatException e) { + return Double.NaN; + } + } +} \ No newline at end of file diff --git a/src/jtop/system/linux/Uptime.java b/src/jtop/system/linux/Uptime.java new file mode 100644 index 0000000..24958c5 --- /dev/null +++ b/src/jtop/system/linux/Uptime.java @@ -0,0 +1,39 @@ +package jtop.system.linux; +import java.nio.file.Files; +import java.nio.file.Path; + +import jtop.Isystem.IUptime; + +/** + * Utility class for retrieving system uptime information. + *

+ * Reads the uptime from /proc/uptime and returns it + * in various units such as seconds, minutes, hours, or days. + */ +public class Uptime implements IUptime{ + + /** + * Gets the system uptime in the specified format. + * + * @param timeFormat a character indicating the desired unit: + * 's' for seconds, + * 'm' for minutes, + * 'h' for hours, + * 'd' for days. + * @return the system uptime in the requested unit. + * @throws Exception if reading /proc/uptime fails + * or if the timeFormat is invalid. + */ + public double getSystemUptime(char timeFormat) throws Exception { + String content = Files.readString(Path.of("/proc/uptime")); + double seconds = Double.parseDouble(content.split(" ")[0]); + + return switch (timeFormat) { + case 's' -> seconds; + case 'm' -> seconds / 60; + case 'h' -> seconds / 3600; + case 'd' -> seconds / 86400; + default -> throw new IllegalArgumentException("Invalid time format: " + timeFormat); + }; + } +} \ No newline at end of file diff --git a/src/jtop/system/mac/MacFeatures.java b/src/jtop/system/mac/MacFeatures.java new file mode 100644 index 0000000..39d5b32 --- /dev/null +++ b/src/jtop/system/mac/MacFeatures.java @@ -0,0 +1,28 @@ +package jtop.system.mac; + +import java.util.EnumSet; +import jtop.system.Feature; + +/** + * Defines the set of system features supported on macOS. + *

+ * Each operating system has its own feature class (e.g., {@link jtop.system.linux.LinuxFeatures}) + * that lists which features are implemented and available. + *

+ */ +public final class MacFeatures { + + /** + * The set of features currently supported on macOS. + *

+ * This is used by {@link jtop.system.FeatureResolver} to determine at runtime + * which features can be instantiated via {@link jtop.system.SystemInfoFactory}. + *

+ */ + public static final EnumSet SUPPORTED = EnumSet.of( + Feature.PROCESS + ); + + /** Private constructor to prevent instantiation of this utility class. */ + private MacFeatures() {} +} \ No newline at end of file diff --git a/src/jtop/system/mac/PathInfo.java b/src/jtop/system/mac/PathInfo.java new file mode 100644 index 0000000..5583422 --- /dev/null +++ b/src/jtop/system/mac/PathInfo.java @@ -0,0 +1,53 @@ +package jtop.system.mac; + +import java.util.Optional; +import jtop.Isystem.IPathInfo; + +/** + * Provides utilities to retrieve process path information. + *

+ * Uses {@link ProcessHandle} to fetch details about a running process, + * including its command (full path) and executable name. + *

+ */ +public class PathInfo implements IPathInfo { + + /** + * Returns the name of the executable for the given process ID. + *

+ * For example, if the command is "/usr/bin/java", this will return "java". + *

+ * + * @param pid the process ID + * @return the executable name, or "Unknown" if the process does not exist + */ + @Override + public String getName(long pid) { + Optional ph = ProcessHandle.of(pid); + if (ph.isPresent()) { + ProcessHandle.Info info = ph.get().info(); + String command = info.command().orElse("Unknown"); + return command.substring(command.lastIndexOf("/") + 1); + } + return "Unknown"; + } + + /** + * Returns the full command path of the executable for the given process ID. + *

+ * For example, "/usr/bin/java". + *

+ * + * @param pid the process ID + * @return the full command path, or "Unknown" if the process does not exist + */ + @Override + public String getPath(long pid) { + Optional ph = ProcessHandle.of(pid); + if (ph.isPresent()) { + ProcessHandle.Info info = ph.get().info(); + return info.command().orElse("Unknown"); + } + return "Unknown"; + } +} \ No newline at end of file diff --git a/src/jtop/terminal/Header.java b/src/jtop/terminal/Header.java new file mode 100644 index 0000000..81ff206 --- /dev/null +++ b/src/jtop/terminal/Header.java @@ -0,0 +1,50 @@ +package jtop.terminal; + +import java.util.Map; + +import jtop.system.linux.SystemSampler; + +public class Header { + + private static final String RESET = "\033[0m"; + private static final String HEADER_BG = "\033[44m"; + private static final String HEADER_FG = "\033[97m"; + + // draw header using cached SystemSampler values + public static void draw(SystemSampler sampler, double uptime, String load) { + try { + double cpuUsage = sampler.getCpu(); + double memPercent = sampler.getMem(); + double totalMem = sampler.getTotalMemoryBytes(); // you can cache this too + double usedMem = totalMem * (memPercent / 100.0); + + StringBuilder sb = new StringBuilder(); + sb.append(HEADER_BG).append(HEADER_FG); + sb.append(String.format(" Uptime: %.1fh ", uptime)); + sb.append(String.format("| Load: %s ", load)); + sb.append(String.format("| CPU: %.1f%% ", cpuUsage)); + sb.append(String.format("| Mem: %.1f%% (%.1f/%.1f GB) ", + memPercent, usedMem / 1e9, totalMem / 1e9)); + + Map temps = sampler.getTemps(); + if (temps != null) { + int count = 0; + for (Map.Entry entry : temps.entrySet()) { + sb.append(String.format("| %s: %.1f°C ", entry.getKey(), entry.getValue())); + if (++count >= 3) break; + } + } + + int terminalWidth = TerminalSize.getColumns(); + String line = sb.length() > terminalWidth ? sb.substring(0, terminalWidth - 1) : sb.toString(); + System.out.println(line + RESET); + + } catch (Exception e) { + System.out.println(HEADER_BG + HEADER_FG + " Header error: " + e.getMessage() + RESET); + } + } + + public static int getRowsCount() { + return 1; + } +} \ No newline at end of file diff --git a/src/jtop/terminal/InputHandler.java b/src/jtop/terminal/InputHandler.java new file mode 100644 index 0000000..3309011 --- /dev/null +++ b/src/jtop/terminal/InputHandler.java @@ -0,0 +1,134 @@ +package jtop.terminal; +import java.util.concurrent.atomic.AtomicBoolean; + +import jtop.core.ShowProcesses; +import jtop.core.ProcessTableRenderer; + +/** + * Handles keyboard and mouse input from the user for the process monitor. + *

+ * Interprets key presses for: + *

+ *
    + *
  • Scrolling (Arrow keys, 'j'/'k', mouse wheel)
  • + *
  • Sorting by column (mouse click on header)
  • + *
  • Paging (Enter key)
  • + *
  • Exiting the application ('q' or Ctrl+C)
  • + *
+ */ +public class InputHandler { + + /** The main process display manager. */ + private final ShowProcesses showProcesses; + + /** Atomic flag indicating whether the process table should be refreshed. */ + private final AtomicBoolean refresh; + + /** Provides the current terminal size. */ + private final TerminalSize terminalSize; + + /** + * Creates a new input handler for a given process table and terminal. + * + * @param showProcesses the {@link ShowProcesses} instance to control + * @param refresh atomic boolean controlling background refresh + * @param terminalSize the {@link TerminalSize} instance + */ + public InputHandler(ShowProcesses showProcesses, AtomicBoolean refresh, TerminalSize terminalSize) { + this.showProcesses = showProcesses; + this.refresh = refresh; + this.terminalSize = terminalSize; + } + + /** + * Starts reading and handling user input. + *

+ * This method blocks and continuously interprets keyboard and mouse events + * until the user exits the application. + *

+ * + * @throws Exception if an I/O error occurs while reading input + */ + public void start() throws Exception { + int pageSize = terminalSize.getRows() - ProcessTableRenderer.getHeaderAndFooterLength(); + int c; + + while ((c = System.in.read()) != -1) { + switch (c) { + case 27: // ESC sequence + if (System.in.read() == 91) { // '[' + int next = System.in.read(); + switch (next) { + case 65 -> showProcesses.scrollUp(); // Arrow Up + case 66 -> showProcesses.scrollDown(); // Arrow Down + case 77 -> handleMouseEvent(); // Mouse event + } + showProcesses.draw(); + refresh.set(true); + } + break; + + case 106: // 'j' key + showProcesses.scrollDown(); + showProcesses.draw(); + refresh.set(true); + break; + + case 107: // 'k' key + showProcesses.scrollUp(); + showProcesses.draw(); + refresh.set(true); + break; + + case 13: // Enter key + for (int i = 0; i < pageSize; i++) { + showProcesses.scrollDown(); + } + showProcesses.draw(); + refresh.set(true); + break; + + default: + if (c >= 48 && c <= 57) { // 0-9 + if (c == 48) { + c = 58;// 0 acts as 10 and 1 is the first index + } + showProcesses.changeSort(c - 49); + } + if (c == 113 || c == 3) { // 'q' or Ctrl+C + return; // exit loop + } + break; + } + } + } + + /** + * Handles a mouse event received from the terminal. + *

+ * Interprets: + *

+ *
    + *
  • Left click on header row → changes sorting column
  • + *
  • Scroll wheel up → scrolls up
  • + *
  • Scroll wheel down → scrolls down
  • + *
+ * + * @throws Exception if an I/O error occurs while reading mouse input + */ + private void handleMouseEvent() throws Exception { + int cb = System.in.read() - 32; // button code + int cx = System.in.read() - 32; // column (X) + int cy = System.in.read() - 32; // row (Y) + + switch (cb) { + case 0 -> { // Left click + if (cy == 1 + Header.getRowsCount()) { + showProcesses.changeSortByClick(cx - 1); + } + } + case 64 -> showProcesses.scrollUp(); // wheel up + case 65 -> showProcesses.scrollDown(); // wheel down + } + } +} \ No newline at end of file diff --git a/src/jtop/terminal/TerminalSize.java b/src/jtop/terminal/TerminalSize.java new file mode 100644 index 0000000..2e76bb3 --- /dev/null +++ b/src/jtop/terminal/TerminalSize.java @@ -0,0 +1,57 @@ +package jtop.terminal; +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * Utility class to detect the current terminal window size. + *

+ * Provides methods to retrieve the number of rows and columns, + * allowing output to dynamically adjust to fit the screen. + */ +public class TerminalSize { + + /** + * Retrieves the terminal size by executing the "stty size" command. + * + * @return an array of two integers: {rows, columns}. + * Defaults to {24, 80} if the size cannot be determined. + */ + public static int[] getTerminalSize() { + try { + Process process = new ProcessBuilder("sh", "-c", "stty size < /dev/tty").start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line = reader.readLine(); + if (line != null) { + String[] parts = line.trim().split("\\s+"); + if (parts.length == 2) { + return new int[]{ + Integer.parseInt(parts[0]), + Integer.parseInt(parts[1]) + }; + } + } + } + } catch (Exception e) { + // Ignore and fallback + } + return new int[]{24, 80}; // default fallback + } + + /** + * Retrieves the number of terminal rows. + * + * @return the number of rows in the current terminal, or 24 if unknown + */ + public static int getRows() { + return getTerminalSize()[0]; + } + + /** + * Retrieves the number of terminal columns. + * + * @return the number of columns in the current terminal, or 80 if unknown + */ + public static int getColumns() { + return getTerminalSize()[1]; + } +} \ No newline at end of file