From f785812da8fa1ccaa95d4dbbc802a2f9b62b8840 Mon Sep 17 00:00:00 2001 From: sfa Date: Fri, 21 Nov 2025 09:19:37 +0100 Subject: [PATCH] First version --- README.md | 249 ++++ extension.js | 1035 +++++++++++++++++ install.sh | 58 + metadata.json | 10 + ping-monitor.conf.example | 50 + ping.sh | 28 + prefs.js | 158 +++ ....shell.extensions.ping-monitor.gschema.xml | 72 ++ stylesheet.css | 45 + 9 files changed, 1705 insertions(+) create mode 100644 README.md create mode 100644 extension.js create mode 100755 install.sh create mode 100644 metadata.json create mode 100644 ping-monitor.conf.example create mode 100755 ping.sh create mode 100644 prefs.js create mode 100644 schemas/org.gnome.shell.extensions.ping-monitor.gschema.xml create mode 100644 stylesheet.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..eabe149 --- /dev/null +++ b/README.md @@ -0,0 +1,249 @@ +# Ping Monitor for GNOME Shell 48+ + +A GNOME Shell extension that displays network ping information in the status bar. This is a modernized version compatible with GNOME Shell 45, 46, 47, and 48. + +![Ping Monitor](https://user-images.githubusercontent.com/placeholder/ping-monitor.png) + +## Features + +- Monitor multiple network endpoints simultaneously +- Visual status indicators with customizable colors +- Real-time ping statistics in the panel +- Configurable refresh intervals and ping parameters +- Tooltip with detailed ping information +- Dropdown menu with comprehensive statistics + +## Prerequisites + +This extension requires GNOME Shell 45 or later (tested on GNOME Shell 48.4). + +Before installing this extension, ensure you have the necessary system packages installed: + +### On Ubuntu/Debian: +```bash +sudo apt-get install gir1.2-gtop-2.0 gir1.2-clutter-1.0 +``` + +### On Fedora: +```bash +sudo dnf install libgtop2-devel +``` + +### On Arch Linux: +```bash +sudo pacman -S libgtop +``` + +### On openSUSE: +```bash +sudo zypper install gnome-shell-devel libgtop-devel libgtop-2_0-10 typelib-1_0-GTop-2_0 +``` + +## Installation + +### Method 1: Manual Installation + +1. Clone or download this repository: +```bash +cd ~/Downloads +# If you already have the ping-monitor-gnome48 directory, you're good to go +``` + +2. Copy the extension to your local extensions directory: +```bash +mkdir -p ~/.local/share/gnome-shell/extensions +cp -r ~/Downloads/ping-monitor-gnome48 ~/.local/share/gnome-shell/extensions/ping-monitor@sfont.teldat.com +``` + +3. Compile the schema (required): +```bash +cd ~/.local/share/gnome-shell/extensions/ping-monitor@sfont.teldat.com +glib-compile-schemas schemas/ +``` + +4. Restart GNOME Shell: + - On X11: Press `Alt + F2`, type `r`, and press Enter + - On Wayland: Log out and log back in + +5. Enable the extension: +```bash +gnome-extensions enable ping-monitor@sfont.teldat.com +``` + +### Method 2: Development/Testing Installation (with symlink) + +If you want to easily test changes: + +```bash +mkdir -p ~/.local/share/gnome-shell/extensions +cd ~/.local/share/gnome-shell/extensions +ln -s ~/Downloads/ping-monitor-gnome48 ping-monitor@sfont.teldat.com +cd ping-monitor@sfont.teldat.com +glib-compile-schemas schemas/ +gnome-extensions enable ping-monitor@sfont.teldat.com +``` + +## Configuration + +### Create Configuration File + +The extension reads its configuration from `~/.config/ping-monitor.conf`. Create this file with the following structure: + +```json +{ + "debug_output": false, + "ping_config": [ + { + "tag": "google", + "name": "Google", + "address": "8.8.8.8", + "ping_count": 2, + "ping_interval": 0.3, + "ping_deadline": 3, + "refresh_interval": 2000, + "active": true, + "visible": true, + "show_name": true, + "show_address": true, + "show_tooltip": true, + "warning_threshold": 20 + }, + { + "tag": "router", + "name": "Router", + "address": "192.168.0.1", + "ping_count": 2, + "ping_interval": 0.3, + "ping_deadline": 3, + "refresh_interval": 2000, + "active": true, + "visible": true, + "show_name": true, + "show_address": true, + "show_tooltip": true, + "warning_threshold": 20 + } + ] +} +``` + +### Configuration Parameters + +- **debug_output**: Enable debug logging (true/false) +- **tag**: Unique identifier for the ping target +- **name**: Display name shown in the panel +- **address**: IP address or hostname to ping +- **ping_count**: Number of ping packets to send +- **ping_interval**: Interval between ping packets (seconds) +- **ping_deadline**: Maximum time to wait for all pings (seconds) +- **refresh_interval**: Time between ping operations (milliseconds) +- **active**: Enable this ping target (true/false) +- **visible**: Show in the status bar (true/false) +- **show_name**: Display the name in the panel (true/false) +- **show_address**: Display the address below the name (true/false) +- **show_tooltip**: Show tooltip on hover (true/false) +- **warning_threshold**: Ping time in ms to trigger warning color + +### Color Preferences + +You can customize the status indicator colors through the extension preferences: + +1. Open Extension Preferences: +```bash +gnome-extensions prefs ping-monitor@sfont.teldat.com +``` + +2. Adjust the colors for: + - **Good**: Successful ping with normal latency + - **Warning**: Successful ping with high latency + - **Bad**: Failed ping or timeout + - **Loss**: Partial packet loss + +### Reloading Configuration + +After modifying `~/.config/ping-monitor.conf`: + +1. Click on the Ping Monitor icon in the status bar +2. Select "Reload config" from the dropdown menu + +Or restart GNOME Shell (Alt + F2, type 'r', press Enter on X11). + +## Troubleshooting + +### Extension doesn't appear after installation + +1. Make sure the schema is compiled: +```bash +cd ~/.local/share/gnome-shell/extensions/ping-monitor@sfont.teldat.com +glib-compile-schemas schemas/ +``` + +2. Check if the extension is enabled: +```bash +gnome-extensions list +gnome-extensions enable ping-monitor@sfont.teldat.com +``` + +3. Restart GNOME Shell + +### No ping data showing + +1. Verify your configuration file exists and is valid JSON: +```bash +cat ~/.config/ping-monitor.conf +``` + +2. Check the logs for errors: +```bash +journalctl -f -o cat /usr/bin/gnome-shell +``` + +3. Ensure you have network connectivity and can ping the configured addresses: +```bash +ping -c 2 8.8.8.8 +``` + +### Permission errors + +The ping.sh script needs to be executable: +```bash +chmod +x ~/.local/share/gnome-shell/extensions/ping-monitor@sfont.teldat.com +``` + +## What's New in This Version + +This is a complete modernization for GNOME Shell 45-48: + +- ✅ Converted to ES6 modules +- ✅ Uses modern Extension API +- ✅ Updated to GTK4 for preferences +- ✅ Removed deprecated APIs (Lang.bind, old imports system) +- ✅ Modern async/await patterns +- ✅ Compatible with Wayland and X11 +- ✅ Updated GObject registration +- ✅ Modern file dialog APIs + +## Credits + +Original extension by [samuelba](https://github.com/samuelba) + +Based on [gnome-shell-system-monitor-applet](https://github.com/paradoxxxzero/gnome-shell-system-monitor-applet) + +Modernized for GNOME Shell 48+ by Claude + +## License + +Copyright (C) 2019 Samuel Bachmann aka samuelba + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/extension.js b/extension.js new file mode 100644 index 0000000..d00ff8e --- /dev/null +++ b/extension.js @@ -0,0 +1,1035 @@ +import Clutter from 'gi://Clutter'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; +import St from 'gi://St'; +import Shell from 'gi://Shell'; +import GTop from 'gi://GTop'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; +import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js'; +import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; + +let debugOutput = false; +let pingDepsGtop = true; + +function print_info(str) { + log('[Ping monitor INFO] ' + str); +} + +function print_debug(str) { + if (debugOutput) { + log('[Ping monitor DEBUG] ' + str); + } +} + +function color_from_string(color) { + let clutterColor = new Clutter.Color(); + clutterColor = Clutter.Color.from_string(color)[1]; + return clutterColor; +} + +const PingStyleManager = class PingMonitor_PingStyleManager { + constructor() { + print_debug('pingStyleManager constructor()'); + + this._extension = ''; + this._iconsize = 1; + this._text_scaling = 1; + + let interfaceSettings = new Gio.Settings({ + schema: 'org.gnome.desktop.interface' + }); + this._text_scaling = interfaceSettings.get_double('text-scaling-factor'); + if (!this._text_scaling) { + this._text_scaling = 1; + } + } + + get(style) { + return style + this._extension; + } + + text_scaling() { + return this._text_scaling; + } +}; + +const PingDialog = GObject.registerClass( +class PingDialog extends ModalDialog.ModalDialog { + _init() { + super._init({styleClass: 'prompt-dialog'}); + + print_debug('PingDialog construct()'); + + let mainContentBox = new St.BoxLayout({ + style_class: 'prompt-dialog-main-layout', + vertical: false + }); + this.contentLayout.add_child(mainContentBox); + + let messageBox = new St.BoxLayout({ + style_class: 'prompt-dialog-message-layout', + vertical: true + }); + mainContentBox.add_child(messageBox); + + this._subjectLabel = new St.Label({ + style_class: 'prompt-dialog-headline', + text: _('Ping Monitor Extension') + }); + + messageBox.add_child(this._subjectLabel); + + const MESSAGE = _('Dependencies Missing\n\ +Please install: \n\ +libgtop and gir bindings\n\ +\t on Ubuntu: gir1.2-gtop-2.0\n\ +\t on Fedora: libgtop2-devel\n\ +\t on Arch: libgtop\n\ +\t on openSUSE: typelib-1_0-GTop-2_0\n'); + + this._descriptionLabel = new St.Label({ + style_class: 'prompt-dialog-description', + text: MESSAGE + }); + + messageBox.add_child(this._descriptionLabel); + + this.setButtons([{ + label: _('Cancel'), + action: () => { + this.close(); + }, + key: Clutter.KEY_Escape + }]); + } +}); + +const StatusSquare = class PingMonitor_StatusSquare { + constructor(height, parent) { + print_debug('StatusSquare constructor()'); + + this._width = 12; + this._height = 12; + this._color = '#ff0000'; + this._activityState = 0; + this._isPingUpdate = false; + + this.actor = new St.Widget({ + style_class: 'ping-chart', + reactive: false, + width: this._width, + height: this._height, + y_align: Clutter.ActorAlign.CENTER, + y_expand: false + }); + + this.parentC = parent; + this.data = []; + this._updateStyle(); + } + + update(color, isPingUpdate) { + print_debug('StatusSquare update()'); + + this._color = color; + this._isPingUpdate = isPingUpdate; + + if (!this.actor.visible) { + return; + } + + if (this._isPingUpdate) { + this._activityState = (this._activityState + 1) % 4; + } + + this._updateStyle(); + } + + _updateStyle() { + print_debug('StatusSquare _updateStyle()'); + + if (!this.actor.visible) { + return; + } + + this.actor.set_style(` + background-color: ${this._color}; + border-radius: 2px; + width: ${this._width}px; + height: ${this._height}px; + min-width: ${this._width}px; + min-height: ${this._height}px; + `); + } + + resize(schema, key) { + print_debug('StatusSquare resize()'); + } +}; + +const PingTipItem = GObject.registerClass( +class PingTipItem extends PopupMenu.PopupBaseMenuItem { + _init() { + super._init(); + this.remove_style_class_name('popup-menu-item'); + this.add_style_class_name('ping-tooltip-item'); + } +}); + +const TipMenu = class PingMonitor_TipMenu extends PopupMenu.PopupMenuBase { + constructor(sourceActor) { + print_debug('TipMenu constructor()'); + super(sourceActor, 'ping-tooltip-box'); + this.actor = new Clutter.Actor(); + this.actor.add_child(this.box); + } + + _shift() { + print_debug('TipMenu _shift()'); + + let node = this.sourceActor.get_theme_node(); + let contentbox = node.get_content_box(this.sourceActor.get_allocation_box()); + let allocation = Shell.util_get_transformed_allocation(this.sourceActor); + let monitor = Main.layoutManager.findMonitorForActor(this.sourceActor); + let [x, y] = [allocation.x1 + contentbox.x1, + allocation.y1 + contentbox.y1]; + let [cx, cy] = [allocation.x1 + (contentbox.x1 + contentbox.x2) / 2, + allocation.y1 + (contentbox.y1 + contentbox.y2) / 2]; + let [xm, ym] = [allocation.x1 + contentbox.x2, + allocation.y1 + contentbox.y2]; + let [width, height] = this.actor.get_size(); + let tipx = cx - width / 2; + tipx = Math.max(tipx, monitor.x); + tipx = Math.min(tipx, monitor.x + monitor.width - width); + let tipy = Math.floor(ym); + + if (allocation.y1 / monitor.height > 0.3) { + tipy = allocation.y1 - height; + } + this.actor.set_position(tipx, tipy); + } + + open(animate) { + print_debug('TipMenu open()'); + + if (this.isOpen) { + return; + } + + this.isOpen = true; + this.actor.show(); + this._shift(); + this.actor.get_parent()?.set_child_above_sibling(this.actor, null); + this.emit('open-state-changed', true); + } + + close(animate) { + print_debug('TipMenu close()'); + + this.isOpen = false; + this.actor.hide(); + this.emit('open-state-changed', false); + } +}; + +const TipBox = class PingMonitor_TipBox { + constructor() { + print_debug('TipBox constructor()'); + + this.show_tooltip = true; + this.actor = new St.BoxLayout({ + reactive: true, + y_align: Clutter.ActorAlign.CENTER + }); + this.actor._delegate = this; + this.set_tip(new TipMenu(this.actor)); + this.in_to = this.out_to = 0; + this.actor.connect('enter-event', this.on_enter.bind(this)); + this.actor.connect('leave-event', this.on_leave.bind(this)); + } + + set_tip(tipmenu) { + print_debug('TipBox set_tip()'); + + if (this.tipmenu) { + this.tipmenu.destroy(); + } + this.tipmenu = tipmenu; + if (this.tipmenu) { + Main.uiGroup.add_child(this.tipmenu.actor); + this.hide_tip(); + } + } + + show_tip() { + print_debug('TipBox show_tip()'); + + if (!this.tipmenu) { + return; + } + this.tipmenu.open(); + if (this.in_to) { + GLib.Source.remove(this.in_to); + this.in_to = 0; + } + } + + hide_tip() { + print_debug('TipBox hide_tip()'); + + if (!this.tipmenu) { + return; + } + this.tipmenu.close(); + if (this.out_to) { + GLib.Source.remove(this.out_to); + this.out_to = 0; + } + if (this.in_to) { + GLib.Source.remove(this.in_to); + this.in_to = 0; + } + } + + on_enter() { + print_debug('TipBox on_enter()'); + + let show_tooltip = this.show_tooltip; + + if (!show_tooltip) { + return; + } + + if (this.out_to) { + GLib.Source.remove(this.out_to); + this.out_to = 0; + } + if (!this.in_to) { + this.in_to = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { + this.show_tip(); + this.in_to = 0; + return GLib.SOURCE_REMOVE; + }); + } + } + + on_leave() { + print_debug('TipBox on_leave()'); + + if (this.in_to) { + GLib.Source.remove(this.in_to); + this.in_to = 0; + } + if (!this.out_to) { + this.out_to = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { + this.hide_tip(); + this.out_to = 0; + return GLib.SOURCE_REMOVE; + }); + } + } + + destroy() { + print_debug('TipBox destroy()'); + + if (this.in_to) { + GLib.Source.remove(this.in_to); + this.in_to = 0; + } + + if (this.out_to) { + GLib.Source.remove(this.out_to); + this.out_to = 0; + } + + this.actor.destroy(); + } +}; + +const ElementBase = class PingMonitor_ElementBase extends TipBox { + constructor(properties, settings, extensionPath) { + super(); + print_debug('ElementBase constructor()'); + + this._settings = settings; + this._extensionPath = extensionPath; + this.elt = ''; + this.tag = ''; + this.name = ''; + this.show_name = false; + this.color_name = []; + this.text_items = []; + this.menu_items = []; + this.menu_visible = true; + this.color = '#ff0000'; + this.refresh_interval = 5000; + this.visible = true; + this.timeout = undefined; + + this._pingStdout = null; + this._pingDataStdout = null; + this._pingStderr = null; + this._pingDataStderr = null; + + this._prepareToDestroy = false; + + Object.assign(this, properties); + + this.vals = []; + this.tip_labels = []; + this.tip_vals = []; + this.tip_unit_labels = []; + + this.chart = new StatusSquare(Math.round(20 * 4 / 5), this); + this._settings.connect('changed::background', () => { + this.chart._updateStyle(); + }); + + this.actor.visible = this.visible; + + this.interval = this.refresh_interval; + this.add_timeout(); + + this.label = new St.Label({ + text: this.name, + style_class: 'ping-status-label', + y_align: Clutter.ActorAlign.CENTER, + x_expand: false, + y_expand: false, + natural_height_set: false + }); + this.label.visible = this.show_name; + + this.menu_visible = true; + + this.actor.add_child(this.label); + this.text_box = new St.BoxLayout(); + + this.text_items = this.create_text_items(); + this.actor.add_child(this.chart.actor); + this.text_box.visible = true; + this.chart.actor.visible = this.visible; + this.menu_items = this.create_menu_items(); + } + + add_timeout() { + this.remove_timeout(); + print_debug('Add timeout: ' + this.tag); + if (!this._prepareToDestroy) { + this.timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this.interval, () => { + return this.update(); + }); + } + } + + remove_timeout() { + print_debug('Remove (try) timeout: ' + this.tag); + if (this.timeout !== undefined) { + print_debug('Remove timeout: ' + this.tag); + GLib.Source.remove(this.timeout); + this.timeout = undefined; + } + } + + tip_format() { + print_debug('ElementBase tip_format()'); + + for (let i = 0; i < this.color_name.length; i++) { + let tipline = new PingTipItem(); + this.tipmenu.addMenuItem(tipline); + this.tip_labels[i] = new St.Label({text: ''}); + tipline.add_child(this.tip_labels[i]); + this.tip_vals[i] = 0; + } + } + + update() { + print_debug('ElementBase update()'); + + this.remove_timeout(); + + if (!this.menu_visible && !this.actor.visible) { + return false; + } + this.refresh(); + + return GLib.SOURCE_CONTINUE; + } + + updateDrawing() { + this._apply(); + this.chart.update(this.color, true); + for (let i = 0; i < this.tip_vals.length; i++) { + if (this.tip_labels[i]) { + this.tip_labels[i].text = this.tip_vals[i].toString(); + } + } + } + + destroy() { + print_debug('ElementBase destroy()'); + + this.remove_timeout(); + TipBox.prototype.destroy.call(this); + } + + stop() { + this._prepareToDestroy = true; + this.remove_timeout(); + } + + isRunning() { + return ( + this._pingStdout != null || + this._pingDataStdout != null || + this._pingStderr != null || + this._pingDataStderr != null + ); + } +}; + +const Ping = class PingMonitor_Ping extends ElementBase { + constructor(id, tag, name, address, ping_count, ping_interval, + ping_deadline, refresh_interval, active, visible, + show_name, show_address, show_tooltip, warning_threshold, + settings, extensionPath) { + if (show_address) { + name = name + '\n' + address; + } + super({ + elt: 'ping', + tag: tag, + name: name, + show_name: show_name, + visible: visible, + refresh_interval: refresh_interval, + color_name: ['used'], + }, settings, extensionPath); + + print_debug('Ping constructor()'); + + this.ping_message = ''; + this.id = id; + this.address = address; + this.ping_count = ping_count; + this.ping_interval = ping_interval; + this.ping_deadline = ping_deadline; + this.active = active; + this.show_address = show_address; + this.show_tooltip = show_tooltip; + this.warning_threshold = warning_threshold; + this.tip_format(); + this.update(); + } + + _pingReadStdout() { + this._pingDataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, (stream, result) => { + if (stream.fill_finish(result) == 0) { + try { + let buffer = stream.peek_buffer(); + if (buffer instanceof Uint8Array) { + this._pingOutput = new TextDecoder().decode(buffer); + } else { + this._pingOutput = buffer.toString(); + } + if (this._pingOutput) { + print_debug('Ping info: ' + this._pingOutput); + + let firstLine = this._pingOutput.match(/[\w .:()]+\n/m); + print_debug('First line: ' + firstLine[0]); + + let lastLines = this._pingOutput.match(/---[\w\W]+/m); + lastLines[0] = lastLines[0].replace(/^\s+|\s+$/g, ''); + print_debug('Last lines: ' + lastLines[0]); + + this.ping_message = firstLine[0] + lastLines[0]; + print_debug('Ping info: ' + this.ping_message); + + let loss = this._pingOutput.match(/received, (\d*)/m); + let times = this._pingOutput.match(/mdev = (\d*.\d*)\/(\d*.\d*)\/(\d*.\d*)\/(\d*.\d*)/m); + + if (times != null && times.length == 5 && + loss != null && loss.length == 2) { + print_debug('loss: ' + loss[1]); + print_debug('min: ' + times[1]); + print_debug('avg: ' + times[2]); + print_debug('max: ' + times[3]); + print_debug('mdev: ' + times[4]); + + if (loss[1] != 0 && loss[1] != 100) { + if (!this._prepareToDestroy) { + this.color = this._settings.get_string('ping-loss-color'); + } + } else if (loss[1] == 100) { + if (!this._prepareToDestroy) { + this.color = this._settings.get_string('ping-bad-color'); + } + } else if (times[3] > this.warning_threshold) { + if (!this._prepareToDestroy) { + this.color = this._settings.get_string('ping-warning-color'); + } + } else { + if (!this._prepareToDestroy) { + this.color = this._settings.get_string('ping-good-color'); + } + } + } else { + if (!this._prepareToDestroy) { + this.color = this._settings.get_string('ping-bad-color'); + } + } + if (!this._prepareToDestroy) { + this.updateDrawing(); + } + } + } catch (e) { + print_info(e.toString()); + if (!this._prepareToDestroy) { + this.color = this._settings.get_string('ping-bad-color'); + this.updateDrawing(); + } + } + this._pingStdout.close(null); + this._pingStdout = null; + this._pingDataStdout.close(null); + this._pingDataStdout = null; + + this.add_timeout(); + return; + } + + stream.set_buffer_size(2 * stream.get_buffer_size()); + this._pingReadStdout(); + }); + } + + _pingReadStderr() { + this._pingDataStderr.fill_async(-1, GLib.PRIORITY_DEFAULT, null, (stream, result) => { + if (stream.fill_finish(result) == 0) { + try { + let buffer = stream.peek_buffer(); + if (buffer instanceof Uint8Array) { + this._pingOutputErr = new TextDecoder().decode(buffer); + } else { + this._pingOutputErr = buffer.toString(); + } + if (this._pingOutputErr) { + this.ping_message = this._pingOutputErr; + print_debug('Ping error: ' + this.ping_message); + + if (!this._prepareToDestroy) { + this.color = this._settings.get_string('ping-bad-color'); + this.updateDrawing(); + } + } + } catch (e) { + print_info(e.toString()); + if (!this._prepareToDestroy) { + this.color = this._settings.get_string('ping-bad-color'); + this.updateDrawing(); + } + } + this._pingStderr.close(null); + this._pingStderr = null; + this._pingDataStderr.close(null); + this._pingDataStderr = null; + + return; + } + + stream.set_buffer_size(2 * stream.get_buffer_size()); + this._pingReadStderr(); + }); + } + + refresh() { + print_debug('Ping refresh()'); + + try { + let script = [ + '/bin/bash', + this._extensionPath + '/ping.sh', + this.address, + '' + this.ping_count, + '' + this.ping_deadline, + '' + this.ping_interval + ]; + + let success; + [success, this.child_pid, this.in_fd, this.out_fd, this.err_fd] = GLib.spawn_async_with_pipes( + null, + script, + null, + GLib.SpawnFlags.DO_NOT_REAP_CHILD, + null + ); + + this._pingStdout = new Gio.UnixInputStream({fd: this.out_fd, close_fd: true}); + this._pingDataStdout = new Gio.DataInputStream({base_stream: this._pingStdout}); + this._pingStderr = new Gio.UnixInputStream({fd: this.err_fd, close_fd: true}); + this._pingDataStderr = new Gio.DataInputStream({base_stream: this._pingStderr}); + new Gio.UnixOutputStream({fd: this.in_fd, close_fd: true}).close(null); + + this._tagWatchChild = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, this.child_pid, + (pid, status, data) => { + GLib.Source.remove(this._tagWatchChild); + GLib.spawn_close_pid(pid); + this.child_pid = undefined; + } + ); + + this._pingReadStdout(); + this._pingReadStderr(); + } catch (e) { + print_info(e.toString()); + } + } + + _apply() { + print_debug('Ping _apply()'); + + this.menu_items[0].text = this.ping_message; + this.tip_vals[0] = this.ping_message; + } + + create_text_items() { + print_debug('Ping create_text_items()'); + + return [ + new St.Label({ + text: '', + style_class: 'ping-status-value', + y_align: Clutter.ActorAlign.CENTER + }), + new St.Label({ + text: '%', + style_class: 'ping-perc-label', + y_align: Clutter.ActorAlign.CENTER + }) + ]; + } + + create_menu_items() { + print_debug('Ping create_menu_items()'); + + return [ + new St.Label({ + text: '', + style_class: 'ping-value-left' + }) + ]; + } +}; + +const Icon = class PingMonitor_Icon { + constructor(settings) { + print_debug('Icon constructor()'); + this._settings = settings; + this.actor = new St.Icon({ + icon_name: 'system-run-symbolic', + style_class: 'system-status-icon' + }); + this.actor.visible = this._settings.get_boolean('icon-display'); + this._settings.connect('changed::icon-display', () => { + print_debug('changed icon-display'); + this.actor.visible = this._settings.get_boolean('icon-display'); + }); + } +}; + +export default class PingMonitorExtension extends Extension { + enable() { + print_info('applet enabling'); + + this._settings = this.getSettings(); + this._style = new PingStyleManager(); + this._pingBox = null; + this._elts = []; + this._tray = null; + + if (!pingDepsGtop) { + this._pingDialog = new PingDialog(); + this._dialogTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { + this._pingDialog.open(); + this._dialogTimeout = null; + return GLib.SOURCE_REMOVE; + }); + } else { + this._tray = new PanelMenu.Button(0.5, 'PingMonitor', false); + this._icon = new Icon(this._settings); + this._elts = []; + + let isFileOk = false; + let path = this._settings.get_string('ping-config-path'); + if (path == '') { + path = GLib.getenv('HOME') + '/.config/ping-monitor.conf'; + this._settings.set_string('ping-config-path', path); + } + isFileOk = this._read_from_file(path); + this._settings.set_boolean('icon-display', !isFileOk); + + let spacing = '4'; + this._pingBox = new St.BoxLayout({style: 'spacing: ' + spacing + 'px;'}); + this._tray.add_child(this._pingBox); + this._pingBox.add_child(this._icon.actor); + + for (let elt of this._elts) { + this._pingBox.add_child(elt.actor); + } + + let menu_info = new PopupMenu.PopupBaseMenuItem({reactive: false}); + let menu_info_box = new St.BoxLayout(); + menu_info.add_child(menu_info_box); + this._tray.menu.addMenuItem(menu_info, 0); + + this._build_menu_info(); + + this._tray.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + this._tray.menu.connect('open-state-changed', (menu, isOpen) => { + if (isOpen) { + this._ping_menu_timeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 5, () => { + return GLib.SOURCE_CONTINUE; + }); + } else { + if (this._ping_menu_timeout) { + GLib.Source.remove(this._ping_menu_timeout); + this._ping_menu_timeout = null; + } + } + }); + + let item = new PopupMenu.PopupMenuItem(_('Reload config')); + item.connect('activate', () => { + this._reload_async(); + }); + this._tray.menu.addMenuItem(item); + + item = new PopupMenu.PopupMenuItem(_('Preferences...')); + item.connect('activate', () => { + this.openPreferences(); + }); + this._tray.menu.addMenuItem(item); + + Main.panel.addToStatusArea('ping-monitor', this._tray); + } + + print_info('applet enabling done'); + } + + disable() { + print_info('disable applet'); + + if (this._dialogTimeout) { + GLib.Source.remove(this._dialogTimeout); + this._dialogTimeout = null; + } + + if (this._pingDialog) { + this._pingDialog.destroy(); + this._pingDialog = null; + } + + if (this._ping_menu_timeout) { + GLib.Source.remove(this._ping_menu_timeout); + this._ping_menu_timeout = null; + } + + for (let elt of this._elts) { + elt.stop(); + } + + if (this._pingBox) { + for (let elt of this._elts) { + this._pingBox.remove_child(elt.actor); + } + } + + for (let elt of this._elts) { + elt.destroy(); + } + + if (this._tray) { + this._tray.destroy(); + this._tray = null; + } + + this._elts = []; + this._pingBox = null; + this._icon = null; + this._settings = null; + this._style = null; + + print_info('applet disabled'); + } + + _read_from_file(path) { + print_info('read_from_file()'); + + try { + let [ok, contents] = GLib.file_get_contents(path); + if (ok) { + if (contents instanceof Uint8Array) { + contents = new TextDecoder().decode(contents); + } + let map = JSON.parse(contents); + + try { + debugOutput = map['debug_output']; + } catch (e) { + debugOutput = false; + } + + try { + for (let i = 0; i < map['ping_config'].length; i++) { + let tag = map['ping_config'][i]['tag']; + let name = map['ping_config'][i]['name']; + let address = map['ping_config'][i]['address']; + let ping_count = map['ping_config'][i]['ping_count']; + let ping_interval = map['ping_config'][i]['ping_interval']; + let ping_deadline = map['ping_config'][i]['ping_deadline']; + let refresh_interval = map['ping_config'][i]['refresh_interval']; + let active = map['ping_config'][i]['active']; + let visible = map['ping_config'][i]['visible']; + let show_name = map['ping_config'][i]['show_name']; + let show_address = map['ping_config'][i]['show_address']; + let show_tooltip = map['ping_config'][i]['show_tooltip']; + let warning_threshold = map['ping_config'][i]['warning_threshold']; + + print_debug('tag: ' + tag); + print_debug('name: ' + name); + print_debug('address: ' + address); + + this._elts.push(new Ping( + i, + tag, + name, + address, + ping_count, + ping_interval, + ping_deadline, + refresh_interval, + active, + visible, + show_name, + show_address, + show_tooltip, + warning_threshold, + this._settings, + this.path)); + } + } catch (e) { + print_info('could not load config'); + print_info('error: ' + e); + return false; + } + } + } catch (e) { + print_info('Error: ' + e); + return false; + } + + return true; + } + + _build_menu_info() { + print_debug('build_menu_info()'); + + if (this._tray.menu._getMenuItems().length && + this._tray.menu._getMenuItems()[0].get_last_child) { + let lastChild = this._tray.menu._getMenuItems()[0].get_last_child(); + if (lastChild) { + lastChild.destroy_all_children(); + for (let elt of this._elts) { + elt.menu_items = elt.create_menu_items(); + } + } else { + return; + } + } else { + return; + } + + let menu_info_box_table = new St.Widget({ + style: 'padding: 10px 0px 10px 0px; spacing-rows: 10px; spacing-columns: 15px;', + layout_manager: new Clutter.GridLayout({orientation: Clutter.Orientation.VERTICAL}) + }); + let menu_info_box_table_layout = menu_info_box_table.layout_manager; + + let row_index = 0; + for (let elt of this._elts) { + if (!elt.menu_visible) { + continue; + } + + menu_info_box_table_layout.attach( + new St.Label({ + text: elt.name, + style_class: 'ping-title', + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER + }), 0, row_index, 1, 1); + + let col_index = 1; + for (let item of elt.menu_items) { + menu_info_box_table_layout.attach(item, col_index, row_index, 1, 1); + col_index++; + } + + row_index++; + } + + let lastChild = this._tray.menu._getMenuItems()[0].get_last_child(); + if (lastChild) { + lastChild.add_child(menu_info_box_table); + } + } + + async _reload_async() { + print_info('Reload ping applet async...'); + + for (let elt of this._elts) { + elt.stop(); + } + + for (let elt of this._elts) { + while (elt.isRunning()) { + print_info('still running'); + await new Promise(resolve => GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10, () => { + resolve(); + return GLib.SOURCE_REMOVE; + })); + } + } + + for (let elt of this._elts) { + this._pingBox.remove_child(elt.actor); + } + + for (let elt of this._elts) { + elt.destroy(); + } + this._elts = []; + + let isFileOk = false; + let path = this._settings.get_string('ping-config-path'); + if (path == '') { + path = GLib.getenv('HOME') + '/.config/ping-monitor.conf'; + this._settings.set_string('ping-config-path', path); + } + isFileOk = this._read_from_file(path); + this._settings.set_boolean('icon-display', !isFileOk); + + for (let elt of this._elts) { + this._pingBox.add_child(elt.actor); + } + + this._build_menu_info(); + + print_info('Reloaded.'); + } +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..1497dc7 --- /dev/null +++ b/install.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Ping Monitor Extension Installer for GNOME Shell 48+ +# This script installs the Ping Monitor extension to your local GNOME Shell extensions directory + +set -e + +EXTENSION_UUID="ping-monitor@sfont.teldat.com" +EXTENSION_DIR="$HOME/.local/share/gnome-shell/extensions/$EXTENSION_UUID" + +echo "Installing Ping Monitor Extension for GNOME Shell 48+" +echo "======================================================" +echo + +# Create extensions directory if it doesn't exist +echo "Creating extensions directory..." +mkdir -p "$HOME/.local/share/gnome-shell/extensions" + +# Copy extension files +echo "Copying extension files..." +if [ -d "$EXTENSION_DIR" ]; then + echo "Extension directory already exists. Removing old version..." + rm -rf "$EXTENSION_DIR" +fi + +cp -r "$(dirname "$0")" "$EXTENSION_DIR" + +# Compile schema +echo "Compiling GSettings schema..." +cd "$EXTENSION_DIR" +glib-compile-schemas schemas/ + +# Make ping.sh executable +echo "Setting permissions..." +chmod +x "$EXTENSION_DIR/ping.sh" + +# Create example config if it doesn't exist +if [ ! -f "$HOME/.config/ping-monitor.conf" ]; then + echo "Creating example configuration file at ~/.config/ping-monitor.conf..." + cp "$EXTENSION_DIR/ping-monitor.conf.example" "$HOME/.config/ping-monitor.conf" + echo "You can edit this file to configure your ping targets." +else + echo "Configuration file already exists at ~/.config/ping-monitor.conf" +fi + +echo +echo "Installation complete!" +echo +echo "To enable the extension:" +echo " 1. On X11: Press Alt+F2, type 'r', and press Enter to restart GNOME Shell" +echo " 2. On Wayland: Log out and log back in" +echo " 3. Run: gnome-extensions enable $EXTENSION_UUID" +echo +echo "Or use the Extensions app to enable it." +echo +echo "Configuration file: ~/.config/ping-monitor.conf" +echo "Edit this file and reload the extension to change ping targets." +echo diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..c292096 --- /dev/null +++ b/metadata.json @@ -0,0 +1,10 @@ +{ + "shell-version": ["45", "46", "47", "48"], + "uuid": "ping-monitor@sfont.teldat.com", + "name": "Ping Monitor", + "url": "https://github.com/anybotics/gnome-shell-ping-monitor-applet", + "description": "Display ping information. Read the readme to see how it is configured.", + "settings-schema": "org.gnome.shell.extensions.ping-monitor", + "gettext-domain": "ping-monitor", + "version": 2 +} diff --git a/ping-monitor.conf.example b/ping-monitor.conf.example new file mode 100644 index 0000000..78d9ad1 --- /dev/null +++ b/ping-monitor.conf.example @@ -0,0 +1,50 @@ +{ + "debug_output": false, + "ping_config": [ + { + "tag": "google", + "name": "Google", + "address": "8.8.8.8", + "ping_count": 2, + "ping_interval": 0.3, + "ping_deadline": 3, + "refresh_interval": 2000, + "active": true, + "visible": true, + "show_name": true, + "show_address": true, + "show_tooltip": true, + "warning_threshold": 20 + }, + { + "tag": "router", + "name": "Router", + "address": "192.168.0.1", + "ping_count": 2, + "ping_interval": 0.3, + "ping_deadline": 3, + "refresh_interval": 2000, + "active": true, + "visible": true, + "show_name": true, + "show_address": true, + "show_tooltip": true, + "warning_threshold": 20 + }, + { + "tag": "cloudflare", + "name": "Cloudflare", + "address": "1.1.1.1", + "ping_count": 2, + "ping_interval": 0.3, + "ping_deadline": 3, + "refresh_interval": 2000, + "active": true, + "visible": true, + "show_name": true, + "show_address": false, + "show_tooltip": true, + "warning_threshold": 20 + } + ] +} diff --git a/ping.sh b/ping.sh new file mode 100755 index 0000000..c51d11c --- /dev/null +++ b/ping.sh @@ -0,0 +1,28 @@ +#!/bin/bash +################################################################################## +# This file is part of Ping Monitor Gnome extension. +# Apt Update Indicator is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# Apt Update Indicator is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with System Monitor. If not, see . +# Copyright 2019 Samuel Bachmann, sbachmann@anybotics.com. +################################################################################## + +############ +# # +# Ping # +# # +############ + +ADDRESS=$1 +COUNT=$2 +DEADLINE=$3 +INTERVAL=$4 + +ping -c ${COUNT} -w ${DEADLINE} -i ${INTERVAL} ${ADDRESS} diff --git a/prefs.js b/prefs.js new file mode 100644 index 0000000..64921cd --- /dev/null +++ b/prefs.js @@ -0,0 +1,158 @@ +import Gtk from 'gi://Gtk'; +import Adw from 'gi://Adw'; +import Gio from 'gi://Gio'; +import Gdk from 'gi://Gdk'; +import GLib from 'gi://GLib'; + +import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + +function color_to_hex(rgba) { + const red = Math.round(rgba.red * 255); + const green = Math.round(rgba.green * 255); + const blue = Math.round(rgba.blue * 255); + const alpha = Math.round(rgba.alpha * 255); + return `#${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}${alpha.toString(16).padStart(2, '0')}`; +} + +function hex_to_rgba(hex) { + const rgba = new Gdk.RGBA(); + rgba.parse(hex); + return rgba; +} + +export default class PingMonitorPreferences extends ExtensionPreferences { + fillPreferencesWindow(window) { + const settings = this.getSettings(); + + // Create preferences page + const page = new Adw.PreferencesPage({ + title: _('Preferences'), + icon_name: 'dialog-information-symbolic' + }); + + // Create color preferences group + const colorGroup = new Adw.PreferencesGroup({ + title: _('Color Settings'), + description: _('Configure colors for different ping states') + }); + page.add(colorGroup); + + // Good color + const goodRow = this._createColorRow( + settings, + _('Good'), + _('Color for good ping response'), + 'ping-good-color' + ); + colorGroup.add(goodRow); + + // Warning color + const warningRow = this._createColorRow( + settings, + _('Warning'), + _('Color for warning ping response'), + 'ping-warning-color' + ); + colorGroup.add(warningRow); + + // Bad color + const badRow = this._createColorRow( + settings, + _('Bad'), + _('Color for bad ping response'), + 'ping-bad-color' + ); + colorGroup.add(badRow); + + // Loss color + const lossRow = this._createColorRow( + settings, + _('Loss'), + _('Color for ping packet loss'), + 'ping-loss-color' + ); + colorGroup.add(lossRow); + + // Create configuration file group + const configGroup = new Adw.PreferencesGroup({ + title: _('Configuration'), + description: _('Configure the path to the configuration file') + }); + page.add(configGroup); + + // Config path row + const configRow = new Adw.ActionRow({ + title: _('Configuration File'), + subtitle: settings.get_string('ping-config-path') || _('No file selected') + }); + + const fileButton = new Gtk.Button({ + label: _('Select File'), + valign: Gtk.Align.CENTER + }); + + fileButton.connect('clicked', () => { + const dialog = new Gtk.FileDialog(); + dialog.set_title(_('Open configuration file')); + + const initialFolder = Gio.File.new_for_path(GLib.getenv('HOME') + '/.config'); + dialog.set_initial_folder(initialFolder); + + const currentPath = settings.get_string('ping-config-path'); + if (currentPath) { + const currentFile = Gio.File.new_for_path(currentPath); + dialog.set_initial_file(currentFile); + } + + dialog.open(window, null, (source, result) => { + try { + const file = dialog.open_finish(result); + if (file) { + const path = file.get_path(); + settings.set_string('ping-config-path', path); + configRow.subtitle = path; + } + } catch (e) { + if (e.matches(Gtk.DialogError, Gtk.DialogError.DISMISSED)) { + // User cancelled, do nothing + } else { + log('Error selecting file: ' + e); + } + } + }); + }); + + configRow.add_suffix(fileButton); + configRow.activatable_widget = fileButton; + configGroup.add(configRow); + + window.add(page); + } + + _createColorRow(settings, title, subtitle, key) { + const row = new Adw.ActionRow({ + title: title, + subtitle: subtitle + }); + + const colorButton = new Gtk.ColorButton({ + valign: Gtk.Align.CENTER + }); + colorButton.set_use_alpha(true); + + // Set initial color + const currentColor = settings.get_string(key); + const rgba = hex_to_rgba(currentColor); + colorButton.set_rgba(rgba); + + // Connect color change signal + colorButton.connect('color-set', (button) => { + settings.set_string(key, color_to_hex(button.get_rgba())); + }); + + row.add_suffix(colorButton); + row.activatable_widget = colorButton; + + return row; + } +} diff --git a/schemas/org.gnome.shell.extensions.ping-monitor.gschema.xml b/schemas/org.gnome.shell.extensions.ping-monitor.gschema.xml new file mode 100644 index 0000000..ce78273 --- /dev/null +++ b/schemas/org.gnome.shell.extensions.ping-monitor.gschema.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + '#00ff00' + Color of good ping status. + + + '#ffaa00' + Color of warning ping status. + + + '#ff0000' + Color of bad ping status. + + + '#ff00d3' + Color of loss ping status. + + + '' + Configuration path. + + + + true + Display ping monitor icon + Set to true to display ping monitor icon in status bar.(NOTICE: The icon will be shown when none of the others is shown.) + + + + '#ffffffff' + Color of background + + + + ['0', '1', '2'] + Ping ids. + + + + ['lpc', 'npc', 'apc'] + Ping tags. + + + + ['LPC', 'NPC', 'APC'] + Ping names. + + + + ['8.8.8.8', '192.168.0.1', '192.168.0.100'] + Ping ip addresses. + + + + ['true', 'true', 'true'] + Ping show text. + + + + + diff --git a/stylesheet.css b/stylesheet.css new file mode 100644 index 0000000..b8efce2 --- /dev/null +++ b/stylesheet.css @@ -0,0 +1,45 @@ +/* Ping Monitor Extension Styles */ + +.ping-chart { + padding: 0px; + margin: 0px 4px; + min-width: 12px; + min-height: 12px; + width: 12px; + height: 12px; + border-radius: 2px; +} + +.ping-status-label { + padding: 1px 5px; + font-size: 0.9em; + line-height: 1.2; +} + +.ping-status-value { + padding: 0px 2px; +} + +.ping-perc-label { + padding: 0px 2px; +} + +.ping-title { + font-weight: bold; + padding: 5px; +} + +.ping-value-left { + padding: 5px; + min-width: 150px; +} + +.ping-tooltip-box { + background-color: rgba(0, 0, 0, 0.8); + border-radius: 5px; + padding: 10px; +} + +.ping-tooltip-item { + padding: 5px; +}