Skip to content
This repository was archived by the owner on Aug 15, 2026. It is now read-only.
Merged
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
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.1.0] - 2026-02-19

### Added

- Bash-to-YAML reverse import: paste `firewall-cmd` commands and convert them to YAML configuration
- Reverse parser supporting all zone commands, direct rules, rich rules, forward ports, and ICMP blocks
- "Paste Commands" button in editor toolbar with modal overlay for pasting shell scripts
- Handles both `--add-*` and `--remove-*` command variants
- Skips comments, shebang lines, blank lines, and reload commands
- Strips `sudo` prefix automatically

## [1.0.0] - 2026-02-16

### Added
Expand All @@ -27,5 +38,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Responsive layout for desktop and mobile
- Docker deployment with nginx

[Unreleased]: https://github.com/studiowebux/firegen/compare/v1.0.0...HEAD
[Unreleased]: https://github.com/studiowebux/firegen/compare/v1.1.0...HEAD
[1.1.0]: https://github.com/studiowebux/firegen/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/studiowebux/firegen/releases/tag/v1.0.0
125 changes: 125 additions & 0 deletions css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,46 @@ body {
background: var(--surface);
}

/* Error gutter and line markers */

.error-gutter {
width: 0.5rem;
}

.gutter-error,
.gutter-warning {
width: 6px;
height: 6px;
border-radius: 50%;
margin: 0.55rem auto 0;
}

.gutter-error {
background: var(--error);
}

.gutter-warning {
background: #d97706;
}

[data-theme="dark"] .gutter-warning {
background: #f59e0b;
}

.cm-error-line {
background: var(--error-bg) !important;
}

.cm-warning-line {
background: #fffbeb !important;
}

[data-theme="dark"] .cm-warning-line {
background: #1a1500 !important;
}

/* Error bar */

.error-bar {
background: var(--error-bg);
color: var(--error);
Expand Down Expand Up @@ -364,6 +404,91 @@ li.CodeMirror-hint-active {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

/* Modal */

.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
}

.modal-overlay[hidden] {
display: none;
}

.modal-dialog {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
width: min(40rem, 90vw);
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
}

[data-theme="dark"] .modal-dialog {
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
}

.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-block-end: 1px solid var(--border);
flex-shrink: 0;
}

.modal-title {
font-size: 0.8125rem;
font-weight: 500;
color: var(--text);
}

.modal-close {
border-color: transparent;
}

.modal-textarea {
font-family: var(--font-mono);
font-size: 0.8125rem;
line-height: 1.6;
background: var(--bg-editor);
color: var(--text);
border: none;
border-block-end: 1px solid var(--border);
padding: 0.75rem 1rem;
resize: vertical;
outline: none;
flex: 1;
min-height: 12rem;
}

.modal-textarea::placeholder {
color: var(--text-tertiary);
}

.modal-status {
padding: 0.375rem 1rem;
font-family: var(--font-mono);
font-size: 0.6875rem;
color: var(--text-secondary);
border-block-end: 1px solid var(--border);
}

.modal-actions {
display: flex;
justify-content: flex-end;
padding: 0.625rem 1rem;
gap: 0.375rem;
}

/* Responsive */

@media (max-width: 768px) {
Expand Down
20 changes: 19 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Firegen</title>

<script>document.documentElement.setAttribute("data-theme", localStorage.getItem("firegen-theme") || "light");</script>
<link rel="stylesheet" href="vendor/codemirror-5/codemirror.min.css">
<link rel="stylesheet" href="vendor/codemirror-5/theme/monokai.min.css">
<link rel="stylesheet" href="vendor/codemirror-5/addon/hint/show-hint.min.css">
Expand Down Expand Up @@ -34,6 +35,7 @@ <h1 class="app-title">Firegen</h1>
<button id="btn-example" class="btn btn-sm">Load Example</button>
<button id="btn-import" class="btn btn-sm">Import</button>
<button id="btn-export" class="btn btn-sm">Export</button>
<button id="btn-paste-commands" class="btn btn-sm">Paste Commands</button>
<input type="file" id="file-import" accept=".yaml,.yml" hidden>
</div>
</div>
Expand Down Expand Up @@ -70,6 +72,22 @@ <h1 class="app-title">Firegen</h1>
</section>
</main>

<div id="bash-import-modal" class="modal-overlay" hidden>
<div class="modal-dialog">
<div class="modal-header">
<span class="modal-title">Paste Commands</span>
<button id="bash-import-cancel" class="btn-icon modal-close" title="Close">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<textarea id="bash-import-textarea" class="modal-textarea" rows="15" placeholder="Paste firewall-cmd commands here...&#10;&#10;firewall-cmd --permanent --zone=public --add-service=http&#10;firewall-cmd --permanent --zone=public --add-port=8080/tcp"></textarea>
<div id="bash-import-status" class="modal-status" hidden></div>
<div class="modal-actions">
<button id="bash-import-submit" class="btn btn-sm">Import</button>
</div>
</div>
</div>

<script type="module" src="js/app.mjs"></script>
</body>
</html>
38 changes: 32 additions & 6 deletions js/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { initEditor } from "./editor.mjs";
import { parseConfig } from "./parser.mjs";
import { generateApply, generateRemove } from "./generator.mjs";
import { initTabs, setTabContent, getActiveTabContent, setOutputTheme } from "./tabs.mjs";
import { exportYaml, setupImport } from "./import-export.mjs";
import { exportYaml, setupImport, setupBashImport } from "./import-export.mjs";
import { SAMPLE_YAML } from "./sample.mjs";

function init() {
Expand All @@ -17,6 +17,7 @@ function init() {
const btnExample = document.getElementById("btn-example");
const btnImport = document.getElementById("btn-import");
const btnExport = document.getElementById("btn-export");
const btnPasteCommands = document.getElementById("btn-paste-commands");
const btnCopy = document.getElementById("btn-copy");
const fileImport = document.getElementById("file-import");

Expand All @@ -43,12 +44,13 @@ function init() {
// Process the initial sample
handleYamlChange(SAMPLE_YAML);

// Theme toggle
// Theme toggle with persistence
themeToggle.addEventListener("click", () => {
const html = document.documentElement;
const isDark = html.getAttribute("data-theme") === "dark";
const newTheme = isDark ? "light" : "dark";
html.setAttribute("data-theme", newTheme);
localStorage.setItem("firegen-theme", newTheme);
editor.setTheme(newTheme === "dark");
setOutputTheme(newTheme === "dark");
});
Expand All @@ -67,6 +69,21 @@ function init() {
editor.setValue(content);
});

// Bash import
setupBashImport(
{
btnOpen: btnPasteCommands,
modal: document.getElementById("bash-import-modal"),
textarea: document.getElementById("bash-import-textarea"),
btnImport: document.getElementById("bash-import-submit"),
btnCancel: document.getElementById("bash-import-cancel"),
status: document.getElementById("bash-import-status"),
},
(yaml) => {
editor.setValue(yaml);
}
);

// Export
btnExport.addEventListener("click", () => {
const yaml = editor.getValue();
Expand All @@ -87,11 +104,12 @@ function init() {

/**
* Show errors/warnings in the collapsible error bar.
* Accepts structured objects with .message property.
*/
function showErrorBar(messages, isError) {
const count = messages.length;
const label = isError ? "error" : "warning";
const first = messages[0];
const first = messages[0].message;

if (count === 1) {
errorText.textContent = first;
Expand All @@ -101,19 +119,27 @@ function init() {
errorToggle.hidden = false;
}

errorDetails.textContent = messages.join("\n");
errorDetails.textContent = messages.map((m) => m.message).join("\n");
errorDetails.hidden = true;
errorToggle.setAttribute("aria-expanded", "false");
errorBar.hidden = false;
}

/**
* Handle YAML editor content changes.
* Parse, expand templates, generate commands, update output tabs.
* Parse, expand templates, generate commands, update output tabs and markers.
*/
function handleYamlChange(yamlString) {
editor.clearMarkers();

const { config, errors, warnings } = parseConfig(yamlString);

// Collect markers from errors and warnings with known line numbers
const markers = [...errors, ...warnings].filter((m) => m.line !== null);
if (markers.length > 0) {
editor.setMarkers(markers);
}

// Display errors
if (errors.length > 0) {
showErrorBar(errors, true);
Expand All @@ -125,7 +151,7 @@ function init() {
// Display warnings
let warningHeader = "";
if (warnings.length > 0) {
warningHeader = warnings.map((w) => `# WARNING: ${w}`).join("\n") + "\n\n";
warningHeader = warnings.map((w) => `# WARNING: ${w.message}`).join("\n") + "\n\n";
showErrorBar(warnings, false);
} else {
errorBar.hidden = true;
Expand Down
27 changes: 26 additions & 1 deletion js/editor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export function initEditor(container, { value = "", onChange = null, debounceMs
tabSize: 2,
indentWithTabs: false,
indentUnit: 2,
placeholder: "# Enter your firewalld YAML configuration here...",
gutters: ["CodeMirror-linenumbers", "error-gutter"],
placeholder: "# Enter your firegen YAML configuration here...",
extraKeys: {
"Ctrl-Space": "autocomplete",
// Tab inserts spaces instead of tab character
Expand Down Expand Up @@ -79,6 +80,9 @@ export function initEditor(container, { value = "", onChange = null, debounceMs
}
});

// Track lines with classes so clearMarkers can remove them
let markedLines = [];

return {
getValue() {
return editorInstance.getValue();
Expand All @@ -92,5 +96,26 @@ export function initEditor(container, { value = "", onChange = null, debounceMs
refresh() {
editorInstance.refresh();
},
/** Set gutter markers and line highlights for errors/warnings. */
setMarkers(markers) {
for (const { line, severity } of markers) {
const dot = document.createElement("div");
dot.className = severity === "error" ? "gutter-error" : "gutter-warning";
dot.title = markers.filter((m) => m.line === line).map((m) => m.message).join("\n");
editorInstance.setGutterMarker(line, "error-gutter", dot);

const lineClass = severity === "error" ? "cm-error-line" : "cm-warning-line";
editorInstance.addLineClass(line, "wrap", lineClass);
markedLines.push({ line, lineClass });
}
},
/** Remove all gutter markers and line highlights. */
clearMarkers() {
editorInstance.clearGutter("error-gutter");
for (const { line, lineClass } of markedLines) {
editorInstance.removeLineClass(line, "wrap", lineClass);
}
markedLines = [];
},
};
}
Loading