Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,58 @@
# OpenBuilds CONTROL
OpenBuilds CONTROL - Grbl Host / Interface for all CNC style machines running Grbl

---

## About this fork

**This is a modified fork of OpenBuilds CONTROL, not the original.** The upstream
project is at https://github.com/OpenBuilds/OpenBuilds-CONTROL.

Changes in this fork, made 2026-08-11, are confined to window management on Linux-
based hosts:

- **Native window decorations on Linux.** Upstream creates a frameless window and
draws its own titlebar. Under Wayland a frameless window gets no compositor
resize edges and does not reliably report its maximized state, so the window
manager now draws the titlebar on Linux. Windows and macOS are unchanged.
- **Window minimums lowered from 1000x850 to 800x600.** This was a problem for my
CNC computer as it uses a 1366x768 display - upstream's own comment reads
`1366 * 768 == minimum to cater for` directly above `minHeight: 850`. The window
therefore could not be shrunk to fit the screen, and maximizing was an
unsatisfiable constraint.
- **Initial size clamped to the available work area**, so the window opens at a
size that fits the screen.
- **Maximized state tracked from window events** rather than `isMaximized()`,
which is unreliable under Wayland.
- **Close-to-tray fixed** to use `event.preventDefault()`; returning `false` does
not cancel an Electron close.

Modified files carry notices at the top: `index.js`, `app/index.html`,
`app/js/websocket.js`.

Verified on Ubuntu 26.04 (GNOME 50, Wayland) against a 1280x768 display.

## License

This project is licensed under the **GNU General Public License v3.0**; the full
text is in [LICENSE](LICENSE) and is unmodified from upstream.

Note a discrepancy inherited from upstream: `LICENSE` contains the GPL-3.0 text,
while `package.json` declares `"license": "AGPL-3.0"`. Both have been present
since upstream's first commit, and the company behind OpenBuilds is no longer
operating, so there is no authority available to resolve which was intended.

Neither file has been altered in this fork. Because AGPL-3.0's obligations are a
superset of GPL-3.0's - AGPL adds section 13, covering source provision to users
who interact with the software over a network - **this fork is distributed in
compliance with the stricter AGPL-3.0 reading**, which satisfies the GPL-3.0
reading as well. Complete corresponding source is published in this repository.

Note that OpenBuilds CONTROL serves its user interface over HTTP on port 3000.
If you run a modified version and allow others to reach that interface over a
network, the AGPL-3.0 reading would require you to offer those users the source
of your modified version from within the application itself.

## Download

#### Latest Version
Expand Down
21 changes: 21 additions & 0 deletions app/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
<!DOCTYPE html>
<!--
MODIFIED FILE - this is not the original OpenBuilds CONTROL source.

Modified 2026-08-11: hide the in-page titlebar before first paint on Linux,
where native window decorations are used, to avoid a duplicate titlebar.

Upstream: https://github.com/OpenBuilds/OpenBuilds-CONTROL
Licensed under the GNU GPL v3 - see LICENSE. See README.md for a note on the
GPL/AGPL discrepancy inherited from upstream.
-->
<html lang="en">

<head>
Expand Down Expand Up @@ -43,6 +53,17 @@
<span title="Close to Tray" class="btn-custom" onclick="socket.emit('minimisetotray')"><i class="fas fa-window-close"></i></span>
</div>
</div>
<script>
// Linux runs with native window decorations, so drop the in-page
// titlebar before first paint. Waiting for the socket 'status' event to
// do it flashes a duplicate titlebar on every launch. In a plain
// browser window.process is undefined and websocket.js handles it.
try {
if (window.process && window.process.platform === 'linux') {
document.getElementById('windowtitlebar').style.display = 'none';
}
} catch (e) {}
</script>
<div class="window-content" style="height: 100%; overflow: hidden !important;">
<nav data-role="ribbonmenu" class="mt-1">
<ul class="tabs-holder">
Expand Down
17 changes: 16 additions & 1 deletion app/js/websocket.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
/*
* MODIFIED FILE - this is not the original OpenBuilds CONTROL source.
*
* Modified 2026-08-11: hide the in-page titlebar when the platform draws native
* window decorations, so Linux does not show two titlebars at once.
*
* Upstream: https://github.com/OpenBuilds/OpenBuilds-CONTROL
* Licensed under the GNU GPL v3 - see LICENSE. See README.md for a note on the
* GPL/AGPL discrepancy inherited from upstream.
*/

var socket, laststatus;;
var server = ''; //192.168.14.100';
var programBoard = {};
Expand Down Expand Up @@ -587,7 +598,11 @@ function initSocket() {
if (nostatusyet) {
// $('#windowtitle').html("OpenBuilds CONTROL v" + status.driver.version)
setWindowTitle(status)
if (status.driver.operatingsystem == "rpi") {
// Hide the in-page titlebar when something else is already drawing one:
// the Raspberry Pi kiosk browser, or a platform using native window
// decorations (Linux). Leaving both visible gives two titlebars, and the
// in-page window buttons would fight the real ones.
if (status.driver.operatingsystem == "rpi" || status.driver.nativeframe) {
$('#windowtitlebar').hide();
}
}
Expand Down
145 changes: 133 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*
* MODIFIED FILE - this is not the original OpenBuilds CONTROL source.
*
* Modified 2026-08-11: Linux window management.
* - Native window decorations on Linux, so the window manager owns maximize,
* resize, snapping and tiling. Windows and macOS keep the custom titlebar.
* - Window minimums lowered from 1000x850 to 800x600. The old minimum height
* exceeded the 768px height of the displays this app targets, so the window
* could be neither shrunk to fit nor maximized.
* - Initial window size clamped to the available work area.
* - Maximized state tracked from window events rather than isMaximized().
* - close handler now uses event.preventDefault() so close-to-tray works.
* - Platform reported correctly on Linux and on argument-less Windows launch.
*
* Upstream: https://github.com/OpenBuilds/OpenBuilds-CONTROL
* Licensed under the GNU GPL v3 - see LICENSE. See README.md for a note on the
* GPL/AGPL discrepancy inherited from upstream.
*/

process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = '1';

process.on('uncaughtException', function(err) {
Expand Down Expand Up @@ -208,6 +227,9 @@ var forceQuit
var appIcon = null,
jogWindow = null,
mainWindow = null
// Mirrors the window's maximized state from its own events, because
// BrowserWindow.isMaximized() is not dependable under Wayland.
var jogWindowMaximized = false
var autoUpdater


Expand Down Expand Up @@ -369,6 +391,12 @@ function checkPowerSettings() {

var oldiplist;
var oldpinslist;

// Linux (X11 and Wayland alike) uses native window decorations so the window
// manager owns maximize, resize, snapping and tiling. Windows and macOS keep
// the custom frameless titlebar drawn in app/index.html.
const useNativeFrame = (process.platform === 'linux');

const iconPath = path.join(__dirname, 'app/icon.png');
const iconNoComm = path.join(__dirname, 'app/icon-notconnected.png');
const iconPlay = path.join(__dirname, 'app/icon-play.png');
Expand Down Expand Up @@ -413,6 +441,9 @@ var status = {
version: require('./package').version,
ipaddress: ip.address(),
operatingsystem: false,
// true when the window manager draws the titlebar, so the UI knows to hide
// its own in-page titlebar and window buttons
nativeframe: useNativeFrame,
powersettings: {
usbselectiveAC: null,
usbselectiveDC: null
Expand Down Expand Up @@ -833,25 +864,33 @@ io.on("connection", function(socket) {
});

socket.on("minimisetotray", function(data) {
if (!jogWindow) return;
jogWindow.hide();
});

socket.on("minimize", function(data) {
if (!jogWindow) return;
jogWindow.minimize();
});

socket.on("maximize", function(data) {
if (!jogWindow) return;
if (jogWindow.isFullScreen()) {
jogWindow.setFullScreen(false);
}
if (jogWindow.isMaximized()) {
// Toggle off jogWindowMaximized rather than isMaximized(). Under Wayland
// the latter does not reliably reflect the compositor's current state, so
// the toggle desynchronises and the window stops maximizing. The flag is
// kept honest by the window's own maximize/unmaximize events.
if (jogWindowMaximized) {
jogWindow.unmaximize();
} else {
jogWindow.maximize();
}
});

socket.on("fullscreen", function(data) {
if (!jogWindow) return;
if (jogWindow.isFullScreen()) {
jogWindow.setFullScreen(false);
} else {
Expand Down Expand Up @@ -2929,12 +2968,23 @@ function addQRealtime(gcode) {

function showJogWindow() {
if (jogWindow === null) {
// createJogWindow() shows the window itself from 'ready-to-show'. Showing
// it here as well would put an unpainted window on screen first, which is
// what produced the blank or wrongly sized frame on the initial open.
createJogWindow();
return;
}
jogWindow.show()
jogWindow.setAlwaysOnTop(true);
// Briefly pinning always-on-top is a focus-stealing workaround for Windows.
// Wayland ignores both it and programmatic focus, and some compositors leave
// the window stuck above everything else, so it is skipped there.
if (!useNativeFrame) {
jogWindow.setAlwaysOnTop(true);
}
jogWindow.focus();
jogWindow.setAlwaysOnTop(false);
if (!useNativeFrame) {
jogWindow.setAlwaysOnTop(false);
}
}

// Electron
Expand Down Expand Up @@ -3001,18 +3051,28 @@ if (isElectron()) {

function createApp() {
createTrayIcon();

// Report the platform up front. This used to be set for Windows only
// inside the argv check below, so a plain launch with no file argument
// left it as false and the UI's platform checks never matched.
if (process.platform == 'darwin') {
status.driver.operatingsystem = 'macos';
} else if (process.platform == 'win32') {
status.driver.operatingsystem = 'windows';
} else if (process.platform == 'linux') {
status.driver.operatingsystem = 'linux';
}

if (process.platform == 'darwin') {
debug_log("Creating MacOS Menu");
createMenu();
status.driver.operatingsystem = 'macos';
}
if (process.platform == 'win32' && process.argv.length >= 2) {
var openFilePath = process.argv[1];
if (openFilePath !== "") {
debug_log("path" + openFilePath);
readFile(openFilePath);
}
status.driver.operatingsystem = 'windows';
}

if (process.platform == 'darwin' || uploadedgcode.length > 1) {
Expand Down Expand Up @@ -3150,19 +3210,39 @@ if (isElectron()) {
}

function createJogWindow() {
// Open at 1000x850, but never larger than the screen actually available.
// 850 exceeds the 768px height of the 1366x768 panels this app targets,
// so the preferred size is clamped to the work area rather than trusted.
var workArea = electron.screen.getPrimaryDisplay().workAreaSize;
var startWidth = Math.min(1000, workArea.width);
var startHeight = Math.min(850, workArea.height);
debug_log("Screen work area " + workArea.width + "x" + workArea.height +
" - opening window at " + startWidth + "x" + startHeight);

// Create the browser window.
jogWindow = new BrowserWindow({
// 1366 * 768 == minimum to cater for
width: 1000,
minWidth: 1000,
height: 850,
minHeight: 850,
// The minimums must stay below the smallest supported panel (1366x768,
// and 1280x768 in practice). The old minHeight of 850 was taller than
// the screen itself, so the window could neither be shrunk to fit nor
// successfully maximized - the constraint was unsatisfiable.
width: startWidth,
minWidth: 800,
height: startHeight,
minHeight: 600,
fullscreen: false,
center: true,
resizable: true,
maximizable: true,
// Stay hidden until the first paint. BrowserWindow shows immediately by
// default, which put an empty window on screen while the UI was still
// loading. 'ready-to-show' below reveals it.
show: false,
title: "OpenBuilds CONTROL ",
frame: false,
// Linux keeps its native decorations: under Wayland a frameless window
// gets no compositor resize edges and never reports its maximized state
// back correctly, which breaks both resizing and the maximize toggle.
// Windows and macOS keep the custom in-page titlebar.
frame: useNativeFrame,
autoHideMenuBar: true,
//icon: '/app/favicon.png',
icon: nativeImage.createFromPath(
Expand All @@ -3171,7 +3251,6 @@ if (isElectron()) {
webgl: true,
experimentalFeatures: true,
experimentalCanvasFeatures: true,
offscreen: true,
backgroundColor: "#fff",
webPreferences: {
nodeIntegration: true,
Expand All @@ -3187,19 +3266,61 @@ if (isElectron()) {

jogWindow.on('close', function(event) {
if (!forceQuit) {
// Returning false does NOT cancel an Electron close - it must be
// preventDefault(). This matters now that Linux has a real native
// close button; without it the window would hide and then be
// destroyed, and reopening from the tray would fail.
event.preventDefault();
jogWindow.hide();
return false;
}
});

// The in-page titlebar carries the only fullscreen control, and it is
// hidden when native decorations are used, so bind F11 to replace it.
jogWindow.webContents.on('before-input-event', (event, input) => {
if (input.type === 'keyDown' && input.key === 'F11') {
event.preventDefault();
jogWindow.setFullScreen(!jogWindow.isFullScreen());
}
});

// Track the real maximized state from the window manager, so the
// maximize toggle stays correct even when the user maximizes by
// double-clicking the titlebar or tiling with the keyboard.
jogWindow.on('maximize', function() {
jogWindowMaximized = true;
});
jogWindow.on('unmaximize', function() {
jogWindowMaximized = false;
});

// Emitted when the window is closed.
jogWindow.on('closed', function() {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
jogWindow = null;
jogWindowMaximized = false;
});
// Safety net for the show: false above. If the UI fails to load,
// 'ready-to-show' never fires and the window would stay invisible with no
// reliable way back - GNOME hides Electron tray icons unless an
// AppIndicator extension is installed. Show it anyway after a few
// seconds so a broken UI is at least visible and reportable.
var readyToShowFallback = setTimeout(function() {
readyToShowFallback = null;
if (jogWindow && !jogWindow.isVisible()) {
debug_log("Window never reported ready-to-show, showing it anyway");
showJogWindow();
}
}, 10000);

jogWindow.once('ready-to-show', () => {
if (readyToShowFallback) {
clearTimeout(readyToShowFallback);
readyToShowFallback = null;
}
showJogWindow()
})
}
Expand Down