This commit is contained in:
Jürg Hallenbarter
2026-01-22 14:04:12 +00:00
commit 5068895bd6
49 changed files with 3272 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
bin/
.vscode/
*.class
*TestFile.java
*.jar
doc/

21
LICENSE Normal file
View File

@@ -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.

134
README.md Normal file
View File

@@ -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!

99
build.sh Executable file
View File

@@ -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}"

120
config/default.conf Normal file
View File

@@ -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): 3037
# Background: 4047
# Bright Foreground: 9097
# Bright Background: 100107
# --- 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 (0255)
# 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"

96
generate_javadoc.sh Executable file
View File

@@ -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

16
install.sh Executable file
View File

@@ -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."

54
jtop.sh Executable file
View File

@@ -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" "$@"

88
report.md Normal file
View File

@@ -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

142
src/jtop/App.java Normal file
View File

@@ -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.
* <p>
* This class is responsible for:
* </p>
* <ul>
* <li>Configuring the terminal (raw input mode and mouse reporting)</li>
* <li>Initializing configuration and display components</li>
* <li>Starting background refresh and input handling threads</li>
* <li>Ensuring proper cleanup and terminal restoration on exit</li>
* </ul>
*/
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.
* <p>
* By default, it prepares a {@link ShowProcesses} object displaying
* PID, process name, user, CPU usage, and memory usage.
* </p>
*/
public App() {
showProcesses = new ShowProcesses(
InfoType.PID,
InfoType.NAME,
InfoType.USER,
InfoType.CPU,
InfoType.MEMORY
);
}
/**
* Starts the main application loop.
* <p>
* This method:
* </p>
* <ul>
* <li>Enables raw input mode and mouse tracking</li>
* <li>Draws the initial process table</li>
* <li>Launches the background refresh thread</li>
* <li>Delegates user interaction to the {@link InputHandler}</li>
* </ul>
* 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.
* <p>
* This disables canonical input processing and echoing,
* allowing single keypress handling without pressing Enter.
* </p>
*
* @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 </dev/tty").inheritIO().start().waitFor();
}
/**
* Restores terminal settings to normal mode.
* <p>
* This re-enables standard input behavior and character echoing.
* </p>
*
* @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 </dev/tty").inheritIO().start().waitFor();
}
/**
* Enables mouse reporting mode.
* <p>
* Allows the application to receive and interpret mouse events
* such as clicks and scrolls in the terminal.
* </p>
*/
private void enableMouseReporting() {
System.out.print("\u001B[?1000h");
System.out.flush();
}
/**
* Disables mouse reporting mode.
* <p>
* Ensures that the terminal stops sending mouse event sequences
* once the program exits or cleans up.
* </p>
*/
private void disableMouseReporting() {
System.out.print("\u001B[?1000l");
System.out.flush();
}
}

View File

@@ -0,0 +1,52 @@
package jtop.Isystem;
/**
* Interface for retrieving battery information.
* <p>
* Allows platform-specific implementations to provide battery status, charge,
* voltage, energy, and power readings.
*/
public interface IBatteryInfo {
/**
* Gets the current battery charge percentage (0100).
*
* @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();
}

View File

@@ -0,0 +1,32 @@
package jtop.Isystem;
/**
* Interface for retrieving CPU usage information in a cross-platform way.
* <p>
* 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);
}

View File

@@ -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.
* <p>
* Implementations should retrieve disk statistics and provide a map of device names to their
* read/write counts.
* </p>
*/
public interface IDiskInfo {
/**
* Retrieves disk I/O statistics for all block devices.
* <p>
* Each entry in the returned map contains the device name as the key,
* and an array of two long values as the value:
* </p>
* <ul>
* <li>index 0 - number of reads completed</li>
* <li>index 1 - number of writes completed</li>
* </ul>
*
* @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<String, long[]> getDiskStats() throws IOException;
}

View File

@@ -0,0 +1,50 @@
package jtop.Isystem;
import java.io.IOException;
/**
* Provides methods to gather memory usage statistics.
* <p>
* Implementations typically read Linux <code>/proc</code> files to determine:
* </p>
* <ul>
* <li>Total and available system memory</li>
* <li>Memory usage percentage by the system</li>
* <li>Memory usage percentage of a specific process (by PID)</li>
* </ul>
*/
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;
}

View File

@@ -0,0 +1,27 @@
package jtop.Isystem;
import java.io.IOException;
import java.util.Map;
/**
* Provides methods to collect network usage statistics.
* <p>
* Reads from the Linux file <code>/proc/net/dev</code> to retrieve
* the number of bytes received (RX) and transmitted (TX) per network interface.
* </p>
*/
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:
* <ul>
* <li>index 0: bytes received (RX)</li>
* <li>index 1: bytes transmitted (TX)</li>
* </ul>
* @throws IOException if /proc/net/dev cannot be read
*/
Map<String, long[]> getNetworkUsage() throws IOException;
}

View File

@@ -0,0 +1,33 @@
package jtop.Isystem;
/**
* Provides utilities to retrieve process path information.
* <p>
* Implementations may use OS-specific mechanisms to fetch details about
* a running process, including its command (full path) and executable name.
* </p>
*/
public interface IPathInfo {
/**
* Returns the name of the executable for the given process ID.
* <p>
* For example, if the command is "/usr/bin/java", this should return "java".
* </p>
*
* @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.
* <p>
* For example, "/usr/bin/java".
* </p>
*
* @param pid the process ID
* @return the full command path, or "Unknown" if the process does not exist
*/
String getPath(long pid);
}

View File

@@ -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<String, Double> getTemperatures() throws IOException;
}

View File

@@ -0,0 +1,22 @@
package jtop.Isystem;
/**
* Interface for retrieving system uptime information.
* <p>
* 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;
}

23
src/jtop/Main.java Normal file
View File

@@ -0,0 +1,23 @@
package jtop;
/**
* Entry point for the jtop system monitoring application.
* <p>
* 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.
* <p>
* 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();
}
}

141
src/jtop/config/Config.java Normal file
View File

@@ -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.
* <p>
* 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.
* </p>
*/
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}.
* <p>
* If the file cannot be loaded, an error message is printed to {@code stderr}.
* </p>
*/
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.
* <p>
* Cleaning includes:
* </p>
* <ul>
* <li>Removing inline comments starting with '#'</li>
* <li>Removing quotes and '+' signs</li>
* <li>Replacing escape sequences (033 or \033) with ANSI escape character</li>
* <li>Trimming whitespace and spaces immediately after escape codes</li>
* </ul>
*
* @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<String> getList(String key, String separator, List<String> 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;
}
}

View File

@@ -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();
}

View File

@@ -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
}

View File

@@ -0,0 +1,47 @@
package jtop.core;
/**
* Represents a single process entry in the system.
* <p>
* Holds basic information about a process including its ID, name, executable path,
* owner, CPU usage, and memory usage.
* </p>
*/
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;
}
}

View File

@@ -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.
* <p>
* Generates comparators to sort {@link ProcessHandle} instances based on
* PID, name, path, user, CPU usage, or memory usage. Supports ascending
* and descending order.
* </p>
*/
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<ProcessHandle> getComparator(InfoType sortBy, boolean ascending) {
// create interface instances from factory
Optional<IPathInfo> pathOpt = SystemInfoFactory.getFeature(Feature.PROCESS);
Optional<ICpuInfo> cpuOpt = SystemInfoFactory.getFeature(Feature.CPU);
Optional<IMemoryInfo> 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;
}
}
}

View File

@@ -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.
* <p>
* Reads the process status from <code>/proc/[pid]/stat</code> and maps
* the one-letter state code to a human-readable description.
* </p>
* <p>
* Typical Linux process states:
* </p>
* <ul>
* <li><b>R</b> - Running</li>
* <li><b>S</b> - Sleeping</li>
* <li><b>D</b> - Disk Sleep</li>
* <li><b>T</b> - Stopped</li>
* <li><b>Z</b> - Zombie</li>
* <li><b>X</b> - Dead</li>
* </ul>
*/
public class ProcessState {
/**
* Retrieves the current state of the specified process.
* <p>
* Reads <code>/proc/[pid]/stat</code> and extracts the third field,
* which represents the process state as a single-character code.
* </p>
*
* @param pid the process ID whose state should be retrieved
* @return a human-readable description of the process state, or <code>"?"</code> 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 <code>/proc/[pid]/stat</code>
* 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;
}
}
}

View File

@@ -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.
* <p>
* This class handles:
* </p>
* <ul>
* <li>Color formatting for header, footer, and table rows.</li>
* <li>Column alignment based on terminal width and cell size.</li>
* <li>Displaying keybindings and scrolling status.</li>
* </ul>
*/
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<ProcessRow> processes, List<InfoType> 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<InfoType> infoTypes, InfoType sortBy, boolean sortAsc) {
List<String> 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<InfoType> infoTypes) {
List<String> 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<String> 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;
}
}

View File

@@ -0,0 +1,52 @@
package jtop.core;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Background thread that periodically refreshes a {@link IRefreshable} component.
* <p>
* 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}.
* <p>
* 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.
* <p>
* Sleeps for 2 seconds between updates and refreshes the target object
* if the {@code refresh} flag is set to {@code true}.
* <p>
* 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
}
}
}
}

View File

@@ -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<InfoType> 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<ProcessRow> 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<Long, Double> memCache = memoryInfo != null ? new HashMap<>() : null;
// fetch all processes and sort
List<ProcessHandle> processes = new ArrayList<>(ProcessHandle.allProcesses().toList());
processes.sort(ProcessSorter.getComparator(sortBy, sortAsc));
List<ProcessRow> 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();
}
}
}

View File

@@ -0,0 +1,40 @@
package jtop.system;
/**
* Represents a system feature that can be dynamically implemented for different operating systems.
* <p>
* Each feature stores the name of its default implementation class, which is used
* by {@link SystemInfoFactory} to instantiate the appropriate OS-specific implementation.
* </p>
*/
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;
}
}

View File

@@ -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.
* <p>
* Provides a centralized way to query the supported {@link Feature}s for
* Linux, FreeBSD, and macOS without hardcoding OS-specific logic elsewhere.
* </p>
*/
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<Feature> supported(OperatingSystem os) {
return switch (os) {
case LINUX -> LinuxFeatures.SUPPORTED;
case FREEBSD -> FreeBsdFeatures.SUPPORTED;
case MAC -> MacFeatures.SUPPORTED;
};
}
}

View File

@@ -0,0 +1,36 @@
package jtop.system;
/**
* Enum representing supported operating systems.
* <p>
* Provides a method to detect the current operating system at runtime.
* </p>
*/
public enum OperatingSystem {
/** Linux operating system. */
LINUX,
/** FreeBSD operating system. */
FREEBSD,
/** macOS operating system. */
MAC;
/**
* Detects the current operating system.
* <p>
* Uses the system property {@code os.name} to determine the OS.
* Returns the corresponding {@link OperatingSystem} enum value.
* </p>
*
* @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);
}
}

View File

@@ -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.
* <p>
* 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<Feature> 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 <T> Optional<T> 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<Feature> 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<Feature, Object> 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));
}
}

View File

@@ -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.
* <p>
* Each operating system has its own feature class (e.g., {@link jtop.system.linux.LinuxFeatures})
* that lists which features are implemented and available.
* </p>
*/
public final class FreeBsdFeatures {
/**
* The set of features currently supported on FreeBSD.
* <p>
* This is used by {@link jtop.system.FeatureResolver} to determine at runtime
* which features can be instantiated via {@link jtop.system.SystemInfoFactory}.
* </p>
*/
public static final EnumSet<Feature> SUPPORTED = EnumSet.of(
Feature.PROCESS
);
/** Private constructor to prevent instantiation of this utility class. */
private FreeBsdFeatures() {}
}

View File

@@ -0,0 +1,53 @@
package jtop.system.freebsd;
import java.util.Optional;
import jtop.Isystem.IPathInfo;
/**
* Provides utilities to retrieve process path information.
* <p>
* Uses {@link ProcessHandle} to fetch details about a running process,
* including its command (full path) and executable name.
* </p>
*/
public class PathInfo implements IPathInfo {
/**
* Returns the name of the executable for the given process ID.
* <p>
* For example, if the command is "/usr/bin/java", this will return "java".
* </p>
*
* @param pid the process ID
* @return the executable name, or "Unknown" if the process does not exist
*/
@Override
public String getName(long pid) {
Optional<ProcessHandle> 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.
* <p>
* For example, "/usr/bin/java".
* </p>
*
* @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<ProcessHandle> ph = ProcessHandle.of(pid);
if (ph.isPresent()) {
ProcessHandle.Info info = ph.get().info();
return info.command().orElse("Unknown");
}
return "Unknown";
}
}

View File

@@ -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.
* <p>
* Automatically detects the battery directory under
* <code>/sys/class/power_supply/</code> (e.g. BAT0, BAT1)
* and exposes percentage, status, voltage, energy, and power readings.
* <p>
* 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<Path> 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 (0100).
*
* @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);
}
}

View File

@@ -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.
* <p>
* Reads data from the <code>/proc</code> filesystem on Linux:
* </p>
* <ul>
* <li><code>/proc/[pid]/stat</code> for per-process CPU usage</li>
* <li><code>/proc/stat</code> for overall CPU usage</li>
* <li><code>/proc/loadavg</code> for system load average</li>
* </ul>
*/
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 <code>/proc/loadavg</code>.
*
* @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 <code>/proc/stat</code>.
*
* @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;
}
}
}

View File

@@ -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.
* <p>
* Reads data from <code>/proc/diskstats</code> 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.
* </p>
*/
public class DiskInfo implements IDiskInfo {
/**
* Retrieves disk I/O statistics for all block devices.
* <p>
* Each entry in the returned map contains the device name as the key,
* and an array of two long values as the value:
* </p>
* <ul>
* <li>index 0 - number of reads completed</li>
* <li>index 1 - number of writes completed</li>
* </ul>
*
* @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 <code>/proc/diskstats</code> fails
*/
@Override
public Map<String, long[]> getDiskStats() throws IOException {
Map<String, long[]> 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;
}
}

View File

@@ -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.
* <p>
* Each operating system has its own feature class (e.g., {@link jtop.system.freebsd.FreeBsdFeatures})
* that lists which features are implemented and available.
* </p>
*/
public final class LinuxFeatures {
/**
* The set of features currently supported on Linux.
* <p>
* This is used by {@link jtop.system.FeatureResolver} to determine at runtime
* which features can be instantiated via {@link jtop.system.SystemInfoFactory}.
* </p>
*/
public static final EnumSet<Feature> 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() {}
}

View File

@@ -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.
* <p>
* Reads Linux /proc files to determine:
* </p>
* <ul>
* <li>Total and available system memory</li>
* <li>Memory usage percentage by the system</li>
* <li>Memory usage percentage of a specific process (by PID)</li>
* </ul>
*
* <p>
* Performance notes:
* <ul>
* <li>/proc/meminfo is cached for a short time window</li>
* <li>No regex usage</li>
* <li>No temporary Maps or Lists</li>
* </ul>
* </p>
*/
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;
}
}

View File

@@ -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.
* <p>
* Reads from the Linux file <code>/proc/net/dev</code> to retrieve
* the number of bytes received (RX) and transmitted (TX) per network interface.
* </p>
*/
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:
* <ul>
* <li>index 0: bytes received (RX)</li>
* <li>index 1: bytes transmitted (TX)</li>
* </ul>
* @throws IOException if /proc/net/dev cannot be read
*/
@Override
public Map<String, long[]> getNetworkUsage() throws IOException {
Map<String, long[]> 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;
}
}

View File

@@ -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.
* <p>
* Uses {@link ProcessHandle} to fetch details about a running process,
* including its command (full path) and executable name.
* </p>
*
* <p>
* Performance notes:
* <ul>
* <li>Results are cached per PID</li>
* <li>ProcessHandle is queried only once per PID</li>
* </ul>
* </p>
*/
public class PathInfo implements IPathInfo {
private static final String UNKNOWN = "Unknown";
/** Cache full command path per PID */
private final Map<Long, String> pathCache = new HashMap<>();
/** Cache executable name per PID */
private final Map<Long, String> 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<ProcessHandle> 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();
}
}

View File

@@ -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<String, Double> 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<String, Double> getTemps() { return lastTemps; }
public double getTotalMemoryBytes() { return totalMemoryBytes; }
}

View File

@@ -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.
* <p>
* Temperature sources:
* <ul>
* <li>Primary: /sys/class/hwmon</li>
* <li>Fallback: /sys/class/thermal</li>
* </ul>
* 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<String, Double> getTemperatures() throws IOException {
Map<String, Double> temps = new LinkedHashMap<>();
// --- Primary source: /sys/class/hwmon ---
Path hwmonBase = Path.of("/sys/class/hwmon");
if (Files.isDirectory(hwmonBase)) {
try (DirectoryStream<Path> hwmons = Files.newDirectoryStream(hwmonBase)) {
for (Path hwmon : hwmons) {
String name = readTrimmed(hwmon.resolve("name"), "hwmon");
try (DirectoryStream<Path> 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<Path> 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;
}
}
}

View File

@@ -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.
* <p>
* Reads the uptime from <code>/proc/uptime</code> 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 <code>/proc/uptime</code> 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);
};
}
}

View File

@@ -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.
* <p>
* Each operating system has its own feature class (e.g., {@link jtop.system.linux.LinuxFeatures})
* that lists which features are implemented and available.
* </p>
*/
public final class MacFeatures {
/**
* The set of features currently supported on macOS.
* <p>
* This is used by {@link jtop.system.FeatureResolver} to determine at runtime
* which features can be instantiated via {@link jtop.system.SystemInfoFactory}.
* </p>
*/
public static final EnumSet<Feature> SUPPORTED = EnumSet.of(
Feature.PROCESS
);
/** Private constructor to prevent instantiation of this utility class. */
private MacFeatures() {}
}

View File

@@ -0,0 +1,53 @@
package jtop.system.mac;
import java.util.Optional;
import jtop.Isystem.IPathInfo;
/**
* Provides utilities to retrieve process path information.
* <p>
* Uses {@link ProcessHandle} to fetch details about a running process,
* including its command (full path) and executable name.
* </p>
*/
public class PathInfo implements IPathInfo {
/**
* Returns the name of the executable for the given process ID.
* <p>
* For example, if the command is "/usr/bin/java", this will return "java".
* </p>
*
* @param pid the process ID
* @return the executable name, or "Unknown" if the process does not exist
*/
@Override
public String getName(long pid) {
Optional<ProcessHandle> 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.
* <p>
* For example, "/usr/bin/java".
* </p>
*
* @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<ProcessHandle> ph = ProcessHandle.of(pid);
if (ph.isPresent()) {
ProcessHandle.Info info = ph.get().info();
return info.command().orElse("Unknown");
}
return "Unknown";
}
}

View File

@@ -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<String, Double> temps = sampler.getTemps();
if (temps != null) {
int count = 0;
for (Map.Entry<String, Double> 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;
}
}

View File

@@ -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.
* <p>
* Interprets key presses for:
* </p>
* <ul>
* <li>Scrolling (Arrow keys, 'j'/'k', mouse wheel)</li>
* <li>Sorting by column (mouse click on header)</li>
* <li>Paging (Enter key)</li>
* <li>Exiting the application ('q' or Ctrl+C)</li>
* </ul>
*/
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.
* <p>
* This method blocks and continuously interprets keyboard and mouse events
* until the user exits the application.
* </p>
*
* @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.
* <p>
* Interprets:
* </p>
* <ul>
* <li>Left click on header row → changes sorting column</li>
* <li>Scroll wheel up → scrolls up</li>
* <li>Scroll wheel down → scrolls down</li>
* </ul>
*
* @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
}
}
}

View File

@@ -0,0 +1,57 @@
package jtop.terminal;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* Utility class to detect the current terminal window size.
* <p>
* 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];
}
}