Skip to content

Add GNOME Shell extension for panel integration#10

Merged
Batuhan4 merged 4 commits into
Batuhan4:mainfrom
adymany:feature/gnome-extension
Mar 22, 2026
Merged

Add GNOME Shell extension for panel integration#10
Batuhan4 merged 4 commits into
Batuhan4:mainfrom
adymany:feature/gnome-extension

Conversation

@adymany
Copy link
Copy Markdown
Contributor

@adymany adymany commented Jan 31, 2026

Summary

Adds a GNOME Shell extension that provides quick access to fan control and keyboard RGB settings directly from the GNOME top panel, without needing to open the GTK app.

Features

  • 🌀 Fan Mode Control: Switch between AUTO, Better Auto, MANUAL, and MAX modes
  • 📊 Manual Fan Speed: Per-fan sliders with 8 RPM steps (visible only in MANUAL mode)
  • ⌨️ Keyboard RGB: 10 color presets (White, Red, Green, Blue, Cyan, Magenta, Yellow, Orange, Purple, Off) and brightness slider
  • 🌡️ Live Status: Real-time CPU temperature and fan RPM display (updates every 3 seconds)
  • 🔌 Socket Communication: Direct connection to victus-backend.service using the same protocol as the GTK client

Hardware Validation

  • ✅ Tested on HP Victus 15-fa0666TX (Intel 12th Gen)
  • Ubuntu 25.10 with GNOME Shell 49

Future Integration Ideas

This extension could potentially be extended to support:

  • auto-cpufreq integration for CPU governor control (powersave/performance modes, turbo boost toggle)
  • nbfc-linux integration for alternative fan control on systems where victus-control isn't compatible
  • Platform Profile switching via /sys/firmware/acpi/platform_profile

Installation

cd gnome-extension
chmod +x install.sh
./install.sh
gnome-extensions enable victus-control@victus

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @adymany, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the victus-control project by integrating its core functionalities directly into the GNOME Shell environment. It introduces a new extension that allows users to manage fan modes, adjust manual fan speeds, control keyboard RGB lighting, and monitor system status (CPU temp, fan RPM) conveniently from the top panel. This provides a more seamless and accessible user experience for HP Victus laptop owners, reducing reliance on the standalone GTK application for common adjustments.

Highlights

  • GNOME Shell Extension Introduction: A new GNOME Shell extension is added, providing direct access to fan and keyboard RGB controls from the top panel, bypassing the need for the GTK app.
  • Fan Control Features: Users can now switch between AUTO, Better Auto, MANUAL, and MAX fan modes, and in MANUAL mode, adjust individual fan speeds using sliders with 8 RPM steps.
  • Keyboard RGB Management: The extension offers 10 color presets (White, Red, Green, Blue, Cyan, Magenta, Yellow, Orange, Purple, Off) and a brightness slider for keyboard RGB lighting.
  • Real-time Status Display: Live CPU temperature and fan RPM are displayed, updating every 3 seconds, providing immediate feedback on system performance.
  • Backend Communication: The extension communicates directly with the victus-backend.service via a Unix socket, using the same protocol as the GTK client.
  • Installation and Documentation: New installation script (install.sh) and detailed README files (README.md and gnome-extension/README.md) are included to guide users through setup and usage.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a GNOME Shell extension for victus-control, which is a great addition for usability. The overall structure of the extension is well-done, with good use of UI components and debouncing for sliders. However, there are critical performance issues in the current implementation. The extension uses synchronous I/O for both socket communication and file reading, which will block the main UI thread of GNOME Shell, leading to unresponsiveness and stuttering. I've provided detailed comments and suggestions on how to refactor this to use asynchronous I/O. There is also a minor issue with a hardcoded sensor path that affects portability. Addressing these points will make the extension robust and performant.

Comment thread gnome-extension/extension.js Outdated
Comment on lines +177 to +219
_sendCommand(command) {
return new Promise((resolve, reject) => {
try {
let connection = new Gio.SocketClient();
let socketAddress = new Gio.UnixSocketAddress({ path: SOCKET_PATH });

connection.connect_async(socketAddress, null, (client, result) => {
try {
let conn = client.connect_finish(result);
let outputStream = conn.get_output_stream();
let inputStream = conn.get_input_stream();

// Send length-prefixed message
let encoder = new TextEncoder();
let cmdBytes = encoder.encode(command);
let lenBytes = new Uint8Array(4);
let dv = new DataView(lenBytes.buffer);
dv.setUint32(0, cmdBytes.length, true); // little-endian

outputStream.write_all(lenBytes, null);
outputStream.write_all(cmdBytes, null);

// Read response length
let responseLenBytes = inputStream.read_bytes(4, null).get_data();
let responseLenDv = new DataView(responseLenBytes.buffer);
let responseLen = responseLenDv.getUint32(0, true);

// Read response
let responseBytes = inputStream.read_bytes(responseLen, null).get_data();
let decoder = new TextDecoder();
let response = decoder.decode(responseBytes);

conn.close(null);
resolve(response);
} catch (e) {
reject(e);
}
});
} catch (e) {
reject(e);
}
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The current implementation of _sendCommand uses synchronous socket I/O operations (outputStream.write_all and inputStream.read_bytes). In a GNOME Shell extension, any synchronous I/O on the main thread will block the entire user interface, leading to freezes and a poor user experience. This is a critical issue that needs to be addressed.

All I/O should be performed asynchronously. I've provided a refactored version of the function below that uses the asynchronous variants of the GIO methods (write_all_async, read_bytes_async) along with async/await to keep the code readable while being non-blocking.

_sendCommand(command) {
    return new Promise((resolve, reject) => {
        try {
            const client = new Gio.SocketClient();
            const socketAddress = new Gio.UnixSocketAddress({ path: SOCKET_PATH });

            client.connect_async(socketAddress, null, async (source, result) => {
                let conn = null;
                try {
                    conn = source.connect_finish(result);
                    const outputStream = conn.get_output_stream();
                    const inputStream = conn.get_input_stream();

                    // Send length-prefixed message
                    const encoder = new TextEncoder();
                    const cmdBytes = encoder.encode(command);
                    const lenBytes = new Uint8Array(4);
                    const dv = new DataView(lenBytes.buffer);
                    dv.setUint32(0, cmdBytes.length, true); // little-endian

                    await outputStream.write_all_async(lenBytes, GLib.PRIORITY_DEFAULT, null);
                    await outputStream.write_all_async(cmdBytes, GLib.PRIORITY_DEFAULT, null);

                    // Read response length
                    const responseLenBytes = await inputStream.read_bytes_async(4, GLib.PRIORITY_DEFAULT, null);
                    const responseLenDv = new DataView(responseLenBytes.get_data().buffer);
                    const responseLen = responseLenDv.getUint32(0, true);

                    // Read response
                    const responseBytes = await inputStream.read_bytes_async(responseLen, GLib.PRIORITY_DEFAULT, null);
                    const decoder = new TextDecoder();
                    const response = decoder.decode(responseBytes.get_data());

                    resolve(response);
                } catch (e) {
                    reject(e);
                } finally {
                    if (conn) {
                        conn.close(null);
                    }
                }
            });
        } catch (e) {
            reject(e);
        }
    });
}

Comment on lines +327 to +335
try {
let [ok, contents] = GLib.file_get_contents('/sys/class/thermal/thermal_zone0/temp');
if (ok) {
let temp = parseInt(new TextDecoder().decode(contents)) / 1000;
this._tempItem.label.text = `🌡️ CPU: ${temp.toFixed(0)} °C`;
}
} catch (e) {
// Ignore temp read errors
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The use of GLib.file_get_contents is a synchronous I/O operation. This will block the main thread while reading the file, which can cause the GNOME Shell UI to become unresponsive. While reading from /sys is usually fast, it's a bad practice that can lead to performance issues.

You should use the asynchronous Gio.File.load_contents_async method instead. Since _updateStatus is already an async function, you can use await to handle the result without blocking.

            try {
                const file = Gio.File.new_for_path('/sys/class/thermal/thermal_zone0/temp');
                const [ok, contents] = await file.load_contents_async(null);
                if (ok) {
                    const temp = parseInt(new TextDecoder().decode(contents), 10) / 1000;
                    this._tempItem.label.text = `🌡️ CPU: ${temp.toFixed(0)} °C`;
                }
            } catch (e) {
                // Ignore temp read errors
            }


// Get CPU temp (from sysfs)
try {
let [ok, contents] = GLib.file_get_contents('/sys/class/thermal/thermal_zone0/temp');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The path to the CPU temperature sensor, /sys/class/thermal/thermal_zone0/temp, is hardcoded. This path is not guaranteed to be correct on all systems, as the thermal zone for the CPU can vary. This reduces the portability of the extension.

The C++ backend already contains robust logic to discover the correct sensor path. For better reliability, consider one of the following:

  1. Add a new command to the backend (e.g., GET_CPU_TEMP) that the extension can call.
  2. Replicate the sensor discovery logic from the backend in the extension's JavaScript code.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a GNOME Shell extension that integrates with the existing victus-control backend socket protocol, exposing fan controls and keyboard RGB settings directly in the GNOME top panel.

Changes:

  • Added a new GNOME Shell extension (JS + metadata + CSS) that connects to /run/victus-control/victus_backend.sock to control fan modes/speeds and keyboard RGB/brightness.
  • Added an extension installer script to deploy into the user’s local GNOME extensions directory.
  • Updated project documentation to describe the new extension and installation steps.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
README.md Documents the new GNOME Shell extension and how to install/enable it.
gnome-extension/extension.js Implements the panel indicator UI, backend socket protocol client, and periodic status polling.
gnome-extension/install.sh Installs the extension into ~/.local/share/gnome-shell/extensions/victus-control@victus.
gnome-extension/metadata.json GNOME Shell extension metadata (uuid, shell versions, project URL).
gnome-extension/README.md Extension-specific docs (requirements, install, usage, development/logging).
gnome-extension/stylesheet.css Adds extension stylesheet (currently only partially used by the JS UI).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +283 to +304
_sendCommand(command) {
let run = async () => {
await this._ensureConnection();

try {
let encoder = new TextEncoder();
let cmdBytes = encoder.encode(command);
let lenBytes = new Uint8Array(4);
let dv = new DataView(lenBytes.buffer);
dv.setUint32(0, cmdBytes.length, true);

this._outputStream.write_all(lenBytes, null);
this._outputStream.write_all(cmdBytes, null);

let responseLenBytes = this._inputStream.read_bytes(4, null).get_data();
let responseLenDv = new DataView(responseLenBytes.buffer);
let responseLen = responseLenDv.getUint32(0, true);

let responseBytes = this._inputStream.read_bytes(responseLen, null).get_data();
let decoder = new TextDecoder();
return decoder.decode(responseBytes);
} catch (e) {
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_sendCommand() uses synchronous socket I/O (write_all/read_bytes) on the GNOME Shell main thread. If the backend is slow/unresponsive, this can freeze the entire shell UI (including the panel) until the call returns. Please switch to async stream APIs (write_all_async/read_bytes_async or Gio.DataInputStream with async reads) and consider a cancellable/timeout per request.

Copilot uses AI. Check for mistakes.
Comment on lines +297 to +303
let responseLenBytes = this._inputStream.read_bytes(4, null).get_data();
let responseLenDv = new DataView(responseLenBytes.buffer);
let responseLen = responseLenDv.getUint32(0, true);

let responseBytes = this._inputStream.read_bytes(responseLen, null).get_data();
let decoder = new TextDecoder();
return decoder.decode(responseBytes);
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gio.InputStream.read_bytes(n) is not guaranteed to return exactly n bytes; it may return fewer (especially over sockets). The current framing logic assumes 4 bytes for the length prefix and then exactly responseLen bytes for the payload, which can lead to truncated/garbled reads. Implement a small readExact/readAll helper that loops until the requested byte count is satisfied (or use a DataInputStream read_all/read_exact equivalent).

Copilot uses AI. Check for mistakes.
Comment on lines +291 to +299
let dv = new DataView(lenBytes.buffer);
dv.setUint32(0, cmdBytes.length, true);

this._outputStream.write_all(lenBytes, null);
this._outputStream.write_all(cmdBytes, null);

let responseLenBytes = this._inputStream.read_bytes(4, null).get_data();
let responseLenDv = new DataView(responseLenBytes.buffer);
let responseLen = responseLenDv.getUint32(0, true);
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backend sends/receives the 32-bit length prefix using native endianness (it writes the raw uint32_t). This extension encodes/decodes the length using DataView(..., true) (little-endian). That works on x86, but will break on big-endian architectures and is inconsistent with the protocol as currently implemented. Either update the protocol to explicitly use little-endian on all sides (backend + GTK client + extension), or in the extension use native-endian encoding/decoding (e.g., via Uint32Array) to match the backend.

Suggested change
let dv = new DataView(lenBytes.buffer);
dv.setUint32(0, cmdBytes.length, true);
this._outputStream.write_all(lenBytes, null);
this._outputStream.write_all(cmdBytes, null);
let responseLenBytes = this._inputStream.read_bytes(4, null).get_data();
let responseLenDv = new DataView(responseLenBytes.buffer);
let responseLen = responseLenDv.getUint32(0, true);
let lenArray = new Uint32Array(lenBytes.buffer);
lenArray[0] = cmdBytes.length;
this._outputStream.write_all(lenBytes, null);
this._outputStream.write_all(cmdBytes, null);
let responseLenBytes = this._inputStream.read_bytes(4, null).get_data();
let responseLenArray = new Uint32Array(responseLenBytes.buffer, responseLenBytes.byteOffset, 1);
let responseLen = responseLenArray[0];

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +26
const FAN1_MAX_RPM = 5800;
const FAN2_MAX_RPM = 6100;
const RPM_STEPS = 8;

Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FAN1_MAX_RPM/FAN2_MAX_RPM are hard-coded, but the backend already discovers per-fan max RPM via sysfs (fan*_max) with fallbacks. Hardcoding here duplicates logic and can produce incorrect slider→RPM mapping on other Victus models (or if firmware reports different maxima). Consider querying the backend (new command) or reading fan*_max from hwmon so the slider range matches the backend’s actual clamp behavior.

Suggested change
const FAN1_MAX_RPM = 5800;
const FAN2_MAX_RPM = 6100;
const RPM_STEPS = 8;
const RPM_STEPS = 8;
function _readIntFromFile(path) {
try {
if (!GLib.file_test(path, GLib.FileTest.EXISTS))
return null;
// GLib.file_get_contents returns [ok, contents]
let [ok, contents] = GLib.file_get_contents(path);
if (!ok)
return null;
// contents is a ByteArray; convert to string and parse
let text = imports.byteArray.toString(contents).trim();
let value = parseInt(text, 10);
if (!Number.isFinite(value) || value <= 0)
return null;
return value;
} catch (e) {
return null;
}
}
function getFanMaxRpm(fanIndex, fallback) {
// Try a small set of likely sysfs paths; fall back to the provided default
// if none yield a valid value. This avoids changing behavior on systems
// where the expected files are not present.
const candidatePaths = [
// Common hwmon-style locations
`/sys/class/hwmon/hwmon0/fan${fanIndex}_max`,
`/sys/class/hwmon/hwmon1/fan${fanIndex}_max`,
// Possible platform-specific location for victus-control, if exposed
`/sys/devices/platform/victus-control/hwmon/hwmon0/fan${fanIndex}_max`,
];
for (let path of candidatePaths) {
let value = _readIntFromFile(path);
if (value !== null)
return value;
}
return fallback;
}
const FAN1_MAX_RPM = getFanMaxRpm(1, 5800);
const FAN2_MAX_RPM = getFanMaxRpm(2, 6100);

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +22

.victus-status {
color: #888888;
font-size: 0.9em;
}

.victus-slider-container {
spacing: 8px;
}

.victus-color-preview {
width: 16px;
height: 16px;
border-radius: 4px;
margin-right: 8px;
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stylesheet.css defines several style classes (victus-status, victus-slider-container, victus-color-preview) that are never applied in extension.js (only victus-header is used). Consider either wiring these classes up in the UI code or removing the unused CSS to avoid dead/unclear styling surface.

Suggested change
.victus-status {
color: #888888;
font-size: 0.9em;
}
.victus-slider-container {
spacing: 8px;
}
.victus-color-preview {
width: 16px;
height: 16px;
border-radius: 4px;
margin-right: 8px;
}

Copilot uses AI. Check for mistakes.
Comment thread gnome-extension/README.md

View extension logs:
```bash
journalctl -f -o cat /usr/bin/gnome-shell | grep -i victus
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggested log command pipes journalctl into grep, but without grep --line-buffered the output may be delayed due to buffering when following (-f). Consider adding --line-buffered (or using SYSTEMD_LOG_LEVEL=debug gnome-shell workflows) so logs appear in real time.

Suggested change
journalctl -f -o cat /usr/bin/gnome-shell | grep -i victus
journalctl -f -o cat /usr/bin/gnome-shell | grep --line-buffered -i victus

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants