Console-based JVM monitoring tool

Tests Homebrew Release

jvm-mon

Console based JVM monitoring - when you just want to SSH into a server and see what's going on.

jvm-top lets you monitor your JVM server applications from the terminal.

Screenshot

New Version

Release: 1.0-ea1

  • Rewritten in Go
  • Single executable file
  • Can monitor applications on Java 8 and above
  • Does not require an existing JDK

How it works:

  • jvm-mon executable comes bundled with a Java agent jar
  • On startup it extracts the agent to a temp directory
  • It attaches to the JVM you want to monitor
  • Loads agent into running JVM to collect metrics
  • Agent and app establish a socket connection to send metrics

Install

Requirement: a JDK8 on the server and JAVA_HOME environment variable pointing to it. It won't work with just a JRE.

MacOS

brew install jvm-mon

Linux/MacOS

  1. Download the release and extract
  2. Set JAVA_HOME environment variable: export JAVA_HOME=/path/to/your/jdk8
  3. Execute ./bin/jvm-mon from extracted directory

Usage

  • Select a JVM process and press Enter to monitor it
  • Press q or Ctrl+C to exit
  • Press Del or Backspace to kill a process

What is available

Currently it shows:

  • List of running JVM processes
  • Cpu and GC load
  • Heap size and usage
  • Top threads with cpu usage

Building from source

To build locally run ./gradlew installDist. Then go to ./build/install/jvm-mon/ and run ./bin/jvm-mon.

To develop you will need npm on your machine and then run ./gradlew npmDeps once to get dependencies.

How does it work?

jvm-mon is a Kotlin application based on these awesome libraries:

  • blessed-contrib terminal dashboard library in JavaScript
  • J2V8 Java Bindings for V8 JavaScript engine and Node.js
  • jvmtop Java monitoring for the command-line

The way it works is:

  1. The Kotlin app starts a Node.js engine in-process
  2. Node.js loads a script with all the widgets
  3. The script calls back into Kotlin to get metrics
Comments
  • UnsatisfiedLinkError: Could not load J2V8 library. Reasons: no j2v8_linux_x86_64 in java.library.path

    UnsatisfiedLinkError: Could not load J2V8 library. Reasons: no j2v8_linux_x86_64 in java.library.path

    Executing from a VM, unzipped package, CentOS 6

    Java HotSpot(TM) 64-Bit Server VM warning: You have loaded library /home/rss/libj2v8_linux_x86_64.so which might have disabled stack guard. The VM will try to fix the stack guard now.
    It's highly recommended that you fix the library with 'execstack -c <libfile>', or link it with '-z noexecstack'.
    Exception in thread "main" java.lang.ExceptionInInitializerError
    Caused by: java.lang.IllegalStateException: J2V8 native library not loaded
    	at com.eclipsesource.v8.V8.checkNativeLibraryLoaded(V8.java:195)
    	at com.eclipsesource.v8.V8.createV8Runtime(V8.java:149)
    	at com.eclipsesource.v8.V8.createV8Runtime(V8.java:125)
    	at com.eclipsesource.v8.NodeJS.createNodeJS(NodeJS.java:58)
    	at com.eclipsesource.v8.NodeJS.createNodeJS(NodeJS.java:45)
    	at JvmMon.<clinit>(JvmMon.java:12)
    Caused by: java.lang.UnsatisfiedLinkError: Could not load J2V8 library. Reasons:
    	no j2v8_linux_x86_64 in java.library.path
    	/home/rss/libj2v8_linux_x86_64.so: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.15' not found (required by /home/rss/libj2v8_linux_x86_64.so)
    
    	at com.eclipsesource.v8.LibraryLoader.loadLibrary(LibraryLoader.java:75)
    	at com.eclipsesource.v8.V8.load(V8.java:71)
    	at com.eclipsesource.v8.V8.createV8Runtime(V8.java:145)
    	... 4 more
    
  • ClassNotFoundException on MacOS/Java 8

    ClassNotFoundException on MacOS/Java 8

    When I try to run a build of HEAD, I get the following:

    $ bin/jvm-mon
    Exception in thread "main" java.lang.NoClassDefFoundError: sun/jvmstat/monitor/MonitorException
    	at com.jvmtop.view.VMOverviewView.scanForNewVMs(VMOverviewView.java:157)
    	at JvmMon.update(JvmMon.java:38)
    	at JvmMon.main(JvmMon.java:22)
    Caused by: java.lang.ClassNotFoundException: sun.jvmstat.monitor.MonitorException
    	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
    	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    	... 3 more
    
    $ java -version
    java version "1.8.0_91"
    Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
    Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)
    
    $ uname -a
    Darwin somehost.local 15.6.0 Darwin Kernel Version 15.6.0: Mon Jan  9 23:07:29 PST 2017; root:xnu-3248.60.11.2.1~1/RELEASE_X86_64 x86_64
    
  • Display Java processes being run by other users on a box

    Display Java processes being run by other users on a box

    I like the idea of jvm-mon, but it would be really useful if it displayed/monitored all the java processes running on a box (irrespective of user). If I run jvm-mon as root on my machine (Fedora 23) I would expect to be able to see all java processes running (they are available via jps), but this isn't the case.

  • Doesn't work when not run from within the exploded zip/tar

    Doesn't work when not run from within the exploded zip/tar

    $ opt/jvm-mon-0.1/bin/jvm-mon
    objc[39393]: Class JavaLaunchHelper is implemented in both /Users/dnw/.jenv/versions/1.8/bin/java and /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be used. Which one is undefined.
    
    node.js:376
            callback();
            ^
    module.js:341: Error: Cannot find module '/Users/dnw/opt/jvm-mon-0.1/bin/src/dist/jvm-mon.js'
    
    $ cd opt/jvm-mon-0.1
    $ bin/jvm-mon
    objc[39418]: Class JavaLaunchHelper is implemented in both /Users/dnw/.jenv/versions/1.8/bin/java and /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be used. Which one is undefined.~
    $ # that worked
    
  • Find and load tools.jar from java code

    Find and load tools.jar from java code

    To avoid setting JAVA_HOME env var before launching

    Perhaps using javax.tools.ToolProvider.getSystemToolClassLoader() http://docs.oracle.com/javase/7/docs/api/javax/tools/ToolProvider.html#getSystemToolClassLoader()

  • when start got error

    when start got error

    objc[56636]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_73.jdk/Contents/Home/bin/java (0x10b6864c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_73.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x11333f4e0). One of the two will be used. Which one is undefined.

  • IndexOutOfBoundsException: Index: 0, Size: 0

    IndexOutOfBoundsException: Index: 0, Size: 0

    Dear ajermakovics,

    I get a "IndexOutOfBoundsException: Index: 0, Size: 0" on running jvm-mon-0.3/bin/jvm-mon.

    ./jvm-mon-0.3/bin/jvm-mon OpenJDK 64-Bit Server VM warning: You have loaded library /tmp/java-monitoring/jvm-mon-0.3/libj2v8_linux_x86_64.so which might have disabled stack guard. The VM will try to fix the stack guard now. It's highly recommended that you fix the library with 'execstack -c ', or link it with '-z noexecstack'. Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:653) at java.util.ArrayList.get(ArrayList.java:429) at JvmMon.(JvmMon.java:46) at JvmMon.main(JvmMon.java:27)

    My Env

    cat /etc/*-release PRETTY_NAME="Debian GNU/Linux 8 (jessie)" NAME="Debian GNU/Linux" VERSION_ID="8" VERSION="8 (jessie)" ID=debian

    java -version openjdk version "1.8.0_121" OpenJDK Runtime Environment (build 1.8.0_121-8u121-b13-1~bpo8+1-b13) OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)

    echo $JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64

    Could you help?

    best regards

  • Cannot find module jvm-mon.js on Ubuntu 16.04

    Cannot find module jvm-mon.js on Ubuntu 16.04

    Hey,

    I downloaded your tar, extracted it, did the configuration for Java, and when I executed jvm-mon, here is the error I got :

    OpenJDK 64-Bit Server VM warning: You have loaded library /home/tristan/libj2v8_linux_x86_64.so which might have disabled stack guard. The VM will try to fix the stack guard now.
    It's highly recommended that you fix the library with 'execstack -c <libfile>', or link it with '-z noexecstack'.
    
    node.js:376
            callback();
            ^
    module.js:341: Error: Cannot find module '/home/tristan/Téléchargements/jvm-mon-0.1/bin/src/dist/jvm-mon.js'
    
  • Stack guard message, then empty screen and broken console

    Stack guard message, then empty screen and broken console

    Hi, during the startup I get the "You have loaded library...." message, then my console is cleared and nothing is shown. image

    When I CTRL-C, my console is not usable, since there is only 1 line available. On the screenshot there is an OLD line written, then Enter, then NEW written: image

    Any ideas? I work on Windows, using Cygwin consoles managed by ConEmu 64

    Possible hint: I got I working the first time! But since then never again...

  • Native library extracted to user's home directory

    Native library extracted to user's home directory

    Currently j2v8 extracts it's native library (libj2v8_linux_x86_64.so) to users home directory. Should look into extracting it somewhere else, ideally project directory

  •  SyntaxError: Unexpected token =

    SyntaxError: Unexpected token =

    (base) ➜ jvm-mon git:(master) ✗ ./gradlew npmDeps

    Task :npmDeps npm WARN saveError ENOENT: no such file or directory, open '/Users/develop/jvm-mon/package.json' npm notice created a lockfile as package-lock.json. You should commit this file. npm WARN enoent ENOENT: no such file or directory, open '/Users/develop/jvm-mon/package.json' npm WARN jvm-mon No description npm WARN jvm-mon No repository field. npm WARN jvm-mon No README data npm WARN jvm-mon No license field.

    3 packages are looking for funding run npm fund for details

    BUILD SUCCESSFUL in 2s 1 actionable task: 1 executed (base) ➜ jvm-mon git:(master) ✗ ./build/install/jvm-mon/bin/jvm-mon java.io.IOException: Management agent not found at com.jvmtop.openjdk.tools.LocalVirtualMachine.loadManagementAgent(LocalVirtualMachine.java:365) at com.jvmtop.openjdk.tools.LocalVirtualMachine.startManagementAgent(LocalVirtualMachine.java:141) at com.jvmtop.openjdk.tools.ProxyClient.tryConnect(ProxyClient.java:372) at com.jvmtop.openjdk.tools.ProxyClient.connect(ProxyClient.java:354) at com.jvmtop.monitor.VMInfo.attachToVM(VMInfo.java:219) at com.jvmtop.monitor.VMInfo.processNewVM(VMInfo.java:186) at com.jvmtop.view.VMOverviewView.scanForNewVMs(VMOverviewView.java:176) at jvmmon.JvmStats.update(JvmStats.kt:32) at jvmmon.JvmStats.update$default(JvmStats.kt:31) at jvmmon.JvmStats.(JvmStats.kt:15) at JvmMon.(JvmMon.kt:12) at JvmMon$Companion.main(JvmMon.kt:28) at JvmMon.main(JvmMon.kt) java.io.IOException: Management agent not found at com.jvmtop.openjdk.tools.LocalVirtualMachine.loadManagementAgent(LocalVirtualMachine.java:365) at com.jvmtop.openjdk.tools.LocalVirtualMachine.startManagementAgent(LocalVirtualMachine.java:141) at com.jvmtop.openjdk.tools.ProxyClient.tryConnect(ProxyClient.java:372) at com.jvmtop.openjdk.tools.ProxyClient.connect(ProxyClient.java:354) at com.jvmtop.monitor.VMInfo.attachToVM(VMInfo.java:219) at com.jvmtop.monitor.VMInfo.processNewVM(VMInfo.java:186) at com.jvmtop.view.VMOverviewView.scanForNewVMs(VMOverviewView.java:176) at jvmmon.JvmStats.update(JvmStats.kt:32) at jvmmon.JvmStats.update$default(JvmStats.kt:31) at jvmmon.JvmStats.(JvmStats.kt:15) at JvmMon.(JvmMon.kt:12) at JvmMon$Companion.main(JvmMon.kt:28) at JvmMon.main(JvmMon.kt) java.io.IOException: Management agent not found at com.jvmtop.openjdk.tools.LocalVirtualMachine.loadManagementAgent(LocalVirtualMachine.java:365) at com.jvmtop.openjdk.tools.LocalVirtualMachine.startManagementAgent(LocalVirtualMachine.java:141) at com.jvmtop.openjdk.tools.ProxyClient.tryConnect(ProxyClient.java:372) at com.jvmtop.openjdk.tools.ProxyClient.connect(ProxyClient.java:354) at com.jvmtop.monitor.VMInfo.attachToVM(VMInfo.java:219) at com.jvmtop.monitor.VMInfo.processNewVM(VMInfo.java:186) at com.jvmtop.view.VMOverviewView.scanForNewVMs(VMOverviewView.java:176) at jvmmon.JvmStats.update(JvmStats.kt:32) at jvmmon.JvmStats.update$default(JvmStats.kt:31) at jvmmon.JvmStats.(JvmStats.kt:15) at JvmMon.(JvmMon.kt:12) at JvmMon$Companion.main(JvmMon.kt:28) at JvmMon.main(JvmMon.kt)

    node.js:376 callback(); ^ /Users/develop/jvm-mon/build/install/jvm-mon/node_modules/marked-terminal/index.cjs:23: SyntaxError: Unexpected token = (base) ➜ jvm-mon git:(master) ✗ SyntaxError: Unexpected token =

  • Calculating values for heap usage for plotting the graph

    Calculating values for heap usage for plotting the graph

    How can I get plotted values of heap usage graph in text file ( possibly comma separated ) ? Also, I couldn't understand how exactly are you getting/calculating those values for plotting ? Can you please tell this approach, that would be more beneficial for me. Thanks in advance, means a lot.

  • ConnectIOException at startup

    ConnectIOException at startup

    This maybe a little weird but it's really happening and I don't realize why.

    Just launching bin/jvm-mon with:

    • jvm-mon version 0.3
    • java version "1.8.0_171" Java(TM) SE Runtime Environment (build 1.8.0_171-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)
    • Linux localhost 3.13.0-24-generic #46-Ubuntu SMP Thu Apr 10 19:11:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
    • Ubuntu 14.04 LTS
    java.rmi.ConnectIOException: Exception creating connection to: 192.168.0.150; nested exception is:
    	java.net.NoRouteToHostException: No route to host (Host unreachable)
    	at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:631)
    	at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:216)
    	at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:202)
    	at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:129)
    	at java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod(RemoteObjectInvocationHandler.java:227)
    	at java.rmi.server.RemoteObjectInvocationHandler.invoke(RemoteObjectInvocationHandler.java:179)
    	at com.sun.proxy.$Proxy0.newClient(Unknown Source)
    	at javax.management.remote.rmi.RMIConnector.getConnection(RMIConnector.java:2430)
    	at javax.management.remote.rmi.RMIConnector.connect(RMIConnector.java:308)
    	at javax.management.remote.JMXConnectorFactory.connect(JMXConnectorFactory.java:270)
    	at javax.management.remote.JMXConnectorFactory.connect(JMXConnectorFactory.java:229)
    	at com.jvmtop.openjdk.tools.ProxyClient.tryConnect(ProxyClient.java:392)
    	at com.jvmtop.openjdk.tools.ProxyClient.connect(ProxyClient.java:354)
    	at com.jvmtop.monitor.VMInfo.attachToVM(VMInfo.java:219)
    	at com.jvmtop.monitor.VMInfo.processNewVM(VMInfo.java:186)
    	at com.jvmtop.view.VMOverviewView.scanForNewVMs(VMOverviewView.java:176)
    	at JvmMon.update(JvmMon.java:50)
    	at JvmMon.<init>(JvmMon.java:45)
    	at JvmMon.main(JvmMon.java:27)
    Caused by: java.net.NoRouteToHostException: No route to host (Host unreachable)
    	at java.net.PlainSocketImpl.socketConnect(Native Method)
    	at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
    	at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
    	at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
    	at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
    	at java.net.Socket.connect(Socket.java:589)
    	at java.net.Socket.connect(Socket.java:538)
    	at java.net.Socket.<init>(Socket.java:434)
    	at java.net.Socket.<init>(Socket.java:211)
    	at sun.rmi.transport.proxy.RMIDirectSocketFactory.createSocket(RMIDirectSocketFactory.java:40)
    	at sun.rmi.transport.proxy.RMIMasterSocketFactory.createSocket(RMIMasterSocketFactory.java:148)
    	at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:613)
    	... 18 more
    

    Interfaces Ip addr:

    ip a
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
        inet 127.0.0.1/8 scope host lo
           valid_lft forever preferred_lft forever
        inet6 ::1/128 scope host
           valid_lft forever preferred_lft forever
    2: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
        link/ether f4:03:43:57:41:6c brd ff:ff:ff:ff:ff:ff
        inet 192.168.0.200/24 brd 192.168.0.255 scope global em1
           valid_lft forever preferred_lft forever
        inet6 fe80::f603:43ff:fe57:416c/64 scope link
           valid_lft forever preferred_lft forever
    3: em2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
        link/ether f4:03:43:57:41:6d brd ff:ff:ff:ff:ff:ff
    4: em3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
        link/ether f4:03:43:57:41:6e brd ff:ff:ff:ff:ff:ff
    5: em4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
        link/ether f4:03:43:57:41:6f brd ff:ff:ff:ff:ff:ff
    

    Kernel IP routing table

    Kernel IP routing table
    Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
    0.0.0.0         192.168.0.1     0.0.0.0         UG        0 0          0 em1
    192.168.0.0     0.0.0.0         255.255.255.0   U         0 0          0 em1
    
📝 connote is a simple console-based note taking tool.

?? connote connote is a simple console-based note taking tool. Install Download the binary for your operating system from Releases Run go install gith

Jun 10, 2022
This is a Go Cli app that receives an string path to a log file, and based on it generates and prints in console an encoded polyline with the locations found in the log file.
This is a Go Cli app that receives an string path to a log file, and based on it generates  and prints in console an encoded polyline with the locations found in the log file.

GEOENCODE GO CLI APP DESCRIPTION This is a Go Cli app that receives an string path to a log file, and based on it generates and prints in console an e

Oct 1, 2021
Console progress bar for Golang

Terminal progress bar for Go Installation go get github.com/cheggaaa/pb/v3 Documentation for v1 bar available here Quick start package main import (

Jan 9, 2023
Utilities to prettify console output of tables, lists, progress-bars, text, etc.
Utilities to prettify console output of tables, lists, progress-bars, text, etc.

go-pretty Utilities to prettify console output of tables, lists, progress-bars, text, etc. Table Pretty-print tables into ASCII/Unicode strings.

Dec 29, 2022
Disk usage analyzer with console interface written in Go
Disk usage analyzer with console interface written in Go

Gdu is intended primarily for SSD disks where it can fully utilize parallel processing. However HDDs work as well, but the performance gain is not so huge.

Jan 7, 2023
Integrated console application library, using Go structs as commands, with menus, completions, hints, history, Vim mode, $EDITOR usage, and more ...
Integrated console application library, using Go structs as commands, with menus, completions, hints, history, Vim mode, $EDITOR usage, and more ...

Gonsole - Integrated Console Application library This package rests on a readline console library, (giving advanced completion, hint, input and histor

Nov 20, 2022
oc CLI plugin to interact with Helm features provided by the OpenShift Console

OpenShift provides support for managing the lifecycle of Helm charts. This capability is limited primarily to the Web Console. This plugin enables the management of Helm charts similar to using the standalone Helm CLI while offloading much of the work to OpenShift.

Aug 20, 2022
Simple console formatting library for Go
Simple console formatting library for Go

flair Simple console formatting library for Go Motivation There are definitely a bunch of other libraries like this, but I wanted one I knew would be

Aug 30, 2021
Use Golang to achieve better console backend services

Use Golang to achieve better console backend services

Dec 7, 2021
Basic timestamped console printing.

loglines Basic timestamped console printing. What This is a very simple package with three functions: NowString() returns a string with the current ti

Nov 20, 2021
This is a cli to watch anime using the console/terminal.
 This is a cli to watch anime using the console/terminal.

monas-chinas-cli Este un un cli para ver anime usando la consola/terminal. This is a cli to watch anime using the console/terminal. ⚠️ AVISO ⚠️ Los an

Dec 8, 2022
Windows API to hide console window by golang

Doge-Hide windows API to hide console window by golang ShowWindow ShowWindowAsy

Nov 7, 2022
Godbolt console wrapper for easily execute local file without any security risk and compiler.

Godbolt CLI Godbolt console wrapper for easily execute local file without any security risk and compiler. Install Compile the source code and add to y

May 22, 2022
Lux is a command-line interface for controlling and monitoring Govee lighting strips built in Go.

What is Lux? Lux is a command-line interface for controlling and monitoring Govee lighting strips built in Go. Lux provides it's users with the abilit

Dec 28, 2022
Command line monitoring for goroutines
Command line monitoring for goroutines

grmon Command line monitoring for goroutines Install go get -u github.com/bcicen/grmon Usage Simply import and call grmon.Start() somewhere in your co

Dec 30, 2022
Small CLI based programs for solving structural engineering design problems based on the book 'Structural Concrete'

Small CLI based programs for solving structural engineering design problems based on the book 'Structural Concrete' written by M. Nadim Hassoun and Akhtem Al-Manaseer (edition-6)

Nov 26, 2021
A CLI tool that generates OpenTelemetry Collector binaries based on a manifest.

OpenTelemetry Collector builder This program generates a custom OpenTelemetry Collector binary based on a given configuration. TL;DR $ go get github.c

Sep 14, 2022
A simple command line tool using which you can skip phone number based SMS verification by using a temporary phone number that acts like a proxy.
A simple command line tool using which you can skip phone number based SMS verification by using a temporary phone number that acts like a proxy.

Fake-SMS A simple command line tool using which you can skip phone number based SMS verification by using a temporary phone number that acts like a pr

Dec 31, 2022
Terminal based presentation tool
Terminal based presentation tool

Terminal based presentation tool

Jan 2, 2023