IRC support
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
<link rel="stylesheet" href="/src/css/modals.css" />
|
||||
<link rel="stylesheet" href="/src/css/github.css" />
|
||||
<link rel="stylesheet" href="/src/css/vnc.css" />
|
||||
<link rel="stylesheet" href="/src/css/irc.css" />
|
||||
<link rel="stylesheet" href="/src/css/leviathan.css" />
|
||||
</head>
|
||||
<body>
|
||||
@@ -334,6 +335,7 @@
|
||||
<script src="/src/js/github.js"></script>
|
||||
<script src="/src/js/lib/novnc.min.js"></script>
|
||||
<script src="/src/js/vnc.js"></script>
|
||||
<script src="/src/js/irc.js"></script>
|
||||
<script src="/src/js/settings.js"></script>
|
||||
<script src="/src/js/commands.js"></script>
|
||||
<script src="/src/js/secrets.js"></script>
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
/* ==========================================================================
|
||||
Nexus Terminal - IRC viewer
|
||||
========================================================================== */
|
||||
|
||||
.irc-viewer-panel {
|
||||
background: var(--bg-base);
|
||||
}
|
||||
|
||||
.irc-viewer {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 260px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.irc-main {
|
||||
display: grid;
|
||||
grid-template-rows: 34px minmax(0, 1fr) 42px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.irc-room-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.irc-room-tab {
|
||||
height: 24px;
|
||||
max-width: 180px;
|
||||
min-width: 44px;
|
||||
padding: 0 var(--space-2);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 22px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.irc-room-tab:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.irc-room-tab.active {
|
||||
background: var(--bg-active);
|
||||
border-color: var(--border-default);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.irc-room-tab.has-unread::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin-left: var(--space-1);
|
||||
border-radius: 50%;
|
||||
background: var(--protocol-irc);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.irc-terminal {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: var(--space-2);
|
||||
}
|
||||
|
||||
.irc-terminal .xterm {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.irc-composer {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2);
|
||||
background: var(--bg-surface);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.irc-input {
|
||||
min-width: 0;
|
||||
height: 26px;
|
||||
padding: 0 var(--space-2);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.irc-input:focus,
|
||||
.irc-join-input:focus,
|
||||
.irc-filter-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
.irc-send {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.irc-sidebar {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(88px, auto) minmax(120px, 1fr) minmax(120px, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--bg-surface);
|
||||
border-left: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.irc-sidebar-section {
|
||||
min-height: 0;
|
||||
padding: var(--space-2);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.irc-users-section {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.irc-sidebar-title,
|
||||
.irc-sidebar-title-row {
|
||||
min-height: 24px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--weight-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.irc-sidebar-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.irc-join-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--space-1);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.irc-join-input,
|
||||
.irc-filter-input {
|
||||
min-width: 0;
|
||||
height: 24px;
|
||||
padding: 0 var(--space-2);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.irc-filter-input {
|
||||
width: 100%;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.irc-sidebar-list,
|
||||
.irc-room-list,
|
||||
.irc-user-list {
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.irc-room-row,
|
||||
.irc-user-row,
|
||||
.irc-listed-room-main {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.irc-room-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
height: 26px;
|
||||
padding: 0 var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.irc-room-row:hover,
|
||||
.irc-user-row:hover,
|
||||
.irc-listed-room:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.irc-room-row.active {
|
||||
background: var(--bg-active);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.irc-room-name,
|
||||
.irc-listed-room-name,
|
||||
.irc-listed-room-topic {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.irc-room-count,
|
||||
.irc-listed-room-count {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 10px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.irc-listed-room {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-height: 34px;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.irc-listed-room-main {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.irc-listed-room-name {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.irc-listed-room-topic {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.irc-user-row {
|
||||
display: block;
|
||||
height: 24px;
|
||||
padding: 0 var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.irc-empty {
|
||||
padding: var(--space-2);
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.irc-viewer {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.irc-sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -304,6 +304,9 @@
|
||||
.sidebar-item .protocol-indicator.proto-vnc::before {
|
||||
content: "⊞";
|
||||
}
|
||||
.sidebar-item .protocol-indicator.proto-irc::before {
|
||||
content: "#";
|
||||
}
|
||||
.sidebar-item .protocol-indicator.proto-local::before {
|
||||
content: "›_";
|
||||
}
|
||||
@@ -328,6 +331,10 @@
|
||||
background: rgba(232, 93, 74, 0.18);
|
||||
color: var(--protocol-vnc);
|
||||
}
|
||||
.sidebar-item .protocol-indicator.proto-irc {
|
||||
background: rgba(216, 178, 76, 0.18);
|
||||
color: var(--protocol-irc);
|
||||
}
|
||||
.sidebar-item .protocol-indicator.proto-local {
|
||||
background: rgba(139, 195, 74, 0.18);
|
||||
color: var(--protocol-local);
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
background: rgba(232, 93, 74, 0.15);
|
||||
color: var(--protocol-vnc);
|
||||
}
|
||||
.tab-badge.badge-irc {
|
||||
background: rgba(216, 178, 76, 0.15);
|
||||
color: var(--protocol-irc);
|
||||
}
|
||||
.tab-badge.badge-local {
|
||||
background: rgba(139, 195, 74, 0.15);
|
||||
color: var(--protocol-local);
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
--protocol-raw: #ab47bc;
|
||||
--protocol-github: #7c8aff;
|
||||
--protocol-vnc: #e85d4a;
|
||||
--protocol-irc: #d8b24c;
|
||||
--protocol-local: #8bc34a;
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
|
||||
+50
-3
@@ -35,6 +35,7 @@
|
||||
this.terminals = new TerminalManager(this);
|
||||
this.profileEditor = new ProfileEditor(this);
|
||||
this.rawViewer = new RawViewer(this);
|
||||
window.NexusTerminal.IRCViewer?.setApp(this);
|
||||
this.settings = new Settings(this);
|
||||
this.commandPalette = new CommandPalette(this);
|
||||
this.secretPrompt = new SecretPrompt(this);
|
||||
@@ -239,6 +240,7 @@
|
||||
setTimeout(fit, 50);
|
||||
setTimeout(fit, 300);
|
||||
setTimeout(fit, 800);
|
||||
setTimeout(() => window.NexusTerminal.IRCViewer?.fit(payload.sessionID), 50);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -288,6 +290,12 @@
|
||||
);
|
||||
});
|
||||
|
||||
// IRC events
|
||||
window.runtime.EventsOn("session:irc:event", (payload) => {
|
||||
window.NexusTerminal.IRCViewer.handleEvent(payload);
|
||||
this.tabs.markActivity(payload.sessionID);
|
||||
});
|
||||
|
||||
// App-wide commands from the native menu
|
||||
window.runtime.EventsOn("app:open-about", () => {
|
||||
this.settings.openTab("about");
|
||||
@@ -801,7 +809,12 @@
|
||||
const decision = await this._confirmReuseSession(profile, preferred);
|
||||
if (decision === "existing") {
|
||||
this.tabs.setActive(preferred.id);
|
||||
if (preferred.protocol !== "raw" && preferred.protocol !== "github") {
|
||||
if (
|
||||
preferred.protocol !== "raw" &&
|
||||
preferred.protocol !== "github" &&
|
||||
preferred.protocol !== "vnc" &&
|
||||
preferred.protocol !== "irc"
|
||||
) {
|
||||
this.terminals.focusTerminal(preferred.id);
|
||||
}
|
||||
return;
|
||||
@@ -845,6 +858,15 @@
|
||||
profile,
|
||||
);
|
||||
}
|
||||
} else if (profile.protocol === "irc") {
|
||||
const container = document.getElementById(panelId);
|
||||
if (container) {
|
||||
window.NexusTerminal.IRCViewer.createViewer(
|
||||
sessionInfo.id,
|
||||
container,
|
||||
profile,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.terminals.createTerminal(sessionInfo.id, panelId, profile);
|
||||
}
|
||||
@@ -901,6 +923,7 @@
|
||||
<option value="ssh">SSH</option>
|
||||
<option value="telnet">Telnet</option>
|
||||
<option value="raw">RAW TCP</option>
|
||||
<option value="irc">IRC</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -915,6 +938,10 @@
|
||||
<label class="form-label">Username</label>
|
||||
<input class="form-input qc-username" type="text" placeholder="root" />
|
||||
</div>
|
||||
<div class="form-group qc-nick-group" style="display:none">
|
||||
<label class="form-label">Nick</label>
|
||||
<input class="form-input qc-nick" type="text" value="nexus" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-sm qc-cancel">Cancel</button>
|
||||
@@ -931,14 +958,17 @@
|
||||
const protocolSelect = modal.querySelector(".qc-protocol");
|
||||
const portInput = modal.querySelector(".qc-port");
|
||||
const usernameGroup = modal.querySelector(".qc-username-group");
|
||||
const nickGroup = modal.querySelector(".qc-nick-group");
|
||||
const hostInput = modal.querySelector(".qc-host");
|
||||
|
||||
// Update port default and username visibility when protocol changes
|
||||
protocolSelect.addEventListener("change", () => {
|
||||
const proto = protocolSelect.value;
|
||||
const defaultPorts = { ssh: 22, telnet: 23, raw: 23 };
|
||||
const defaultPorts = { ssh: 22, telnet: 23, raw: 23, irc: 6667 };
|
||||
portInput.value = defaultPorts[proto] || 22;
|
||||
usernameGroup.style.display = proto === "ssh" ? "" : "none";
|
||||
usernameGroup.style.display =
|
||||
proto === "ssh" || proto === "irc" ? "" : "none";
|
||||
nickGroup.style.display = proto === "irc" ? "" : "none";
|
||||
});
|
||||
|
||||
const close = (value) => {
|
||||
@@ -965,6 +995,7 @@
|
||||
host,
|
||||
port: parseInt(portInput.value, 10) || 22,
|
||||
username: modal.querySelector(".qc-username").value.trim(),
|
||||
nick: modal.querySelector(".qc-nick").value.trim(),
|
||||
});
|
||||
});
|
||||
backdrop.addEventListener("click", (e) => {
|
||||
@@ -989,6 +1020,13 @@
|
||||
displayName: result.host,
|
||||
tags: [],
|
||||
};
|
||||
if (result.protocol === "irc") {
|
||||
profile.irc = {
|
||||
nick: result.nick || "nexus",
|
||||
realName: "Nexus Terminal",
|
||||
autoJoinRooms: [],
|
||||
};
|
||||
}
|
||||
const sessionInfo = await window.NexusTerminal.wailsService.ConnectQuick(profile);
|
||||
this.state.addSession(sessionInfo);
|
||||
this.tabs.addTab(sessionInfo);
|
||||
@@ -999,6 +1037,15 @@
|
||||
protocol: "raw",
|
||||
raw: {},
|
||||
});
|
||||
} else if (result.protocol === "irc") {
|
||||
const container = document.getElementById(panelId);
|
||||
if (container) {
|
||||
window.NexusTerminal.IRCViewer.createViewer(
|
||||
sessionInfo.id,
|
||||
container,
|
||||
profile,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.terminals.createTerminal(sessionInfo.id, panelId, {
|
||||
protocol: result.protocol,
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
(function () {
|
||||
/**
|
||||
* Nexus Terminal - IRC viewer.
|
||||
* Uses xterm.js as the chat stream, with room subtabs, a composer, and
|
||||
* room/user sidebars.
|
||||
*/
|
||||
|
||||
/* global Terminal, FitAddon, SearchAddon, WebLinksAddon */
|
||||
|
||||
const { createElement } = window.NexusTerminal;
|
||||
const SERVER_TARGET = "server";
|
||||
const MAX_LINES_PER_TARGET = 2000;
|
||||
const MAX_LISTED_ROOMS = 200;
|
||||
|
||||
class IRCViewer {
|
||||
constructor() {
|
||||
this._viewers = new Map();
|
||||
this.app = null;
|
||||
}
|
||||
|
||||
setApp(app) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
createViewer(sessionID, container, profile) {
|
||||
if (this._viewers.has(sessionID) || !container) return;
|
||||
const viewer = new IRCViewerInstance(sessionID, container, profile, this.app);
|
||||
this._viewers.set(sessionID, viewer);
|
||||
viewer.render();
|
||||
}
|
||||
|
||||
destroyViewer(sessionID) {
|
||||
const viewer = this._viewers.get(sessionID);
|
||||
if (!viewer) return;
|
||||
viewer.destroy();
|
||||
this._viewers.delete(sessionID);
|
||||
}
|
||||
|
||||
handleEvent(payload) {
|
||||
const sessionID = payload?.sessionID;
|
||||
const event = payload?.event || payload;
|
||||
const viewer = this._viewers.get(sessionID);
|
||||
if (viewer) viewer.handleEvent(event);
|
||||
}
|
||||
|
||||
fit(sessionID) {
|
||||
const viewer = this._viewers.get(sessionID);
|
||||
if (viewer) viewer.fit();
|
||||
}
|
||||
}
|
||||
|
||||
class IRCViewerInstance {
|
||||
constructor(sessionID, container, profile, app) {
|
||||
this.sessionID = sessionID;
|
||||
this.container = container;
|
||||
this.profile = profile || {};
|
||||
this.app = app || null;
|
||||
|
||||
this.term = null;
|
||||
this.fitAddon = null;
|
||||
this.searchAddon = null;
|
||||
this.observer = null;
|
||||
|
||||
this.activeTarget = SERVER_TARGET;
|
||||
this.targets = new Map();
|
||||
this.users = new Map();
|
||||
this.joinedRooms = [];
|
||||
this.roomList = [];
|
||||
this.roomFilter = "";
|
||||
this.unreadTargets = new Set();
|
||||
|
||||
this.tabsEl = null;
|
||||
this.termHost = null;
|
||||
this.inputEl = null;
|
||||
this.roomsEl = null;
|
||||
this.usersEl = null;
|
||||
this.roomListEl = null;
|
||||
this.roomFilterEl = null;
|
||||
|
||||
this._ensureTarget(SERVER_TARGET, "Server");
|
||||
}
|
||||
|
||||
render() {
|
||||
this.container.innerHTML = "";
|
||||
this.container.classList.add("irc-viewer-panel");
|
||||
|
||||
const wrapper = createElement("div", "irc-viewer");
|
||||
const main = createElement("div", "irc-main");
|
||||
|
||||
this.tabsEl = createElement("div", "irc-room-tabs");
|
||||
main.appendChild(this.tabsEl);
|
||||
|
||||
this.termHost = createElement("div", "irc-terminal");
|
||||
main.appendChild(this.termHost);
|
||||
|
||||
const composer = createElement("form", "irc-composer");
|
||||
this.inputEl = createElement("input", "irc-input", {
|
||||
type: "text",
|
||||
placeholder: "Message or /command",
|
||||
autocomplete: "off",
|
||||
});
|
||||
const sendBtn = createElement("button", "btn btn-sm btn-primary irc-send", {
|
||||
type: "submit",
|
||||
textContent: "Send",
|
||||
});
|
||||
composer.appendChild(this.inputEl);
|
||||
composer.appendChild(sendBtn);
|
||||
composer.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
this._sendComposer();
|
||||
});
|
||||
this.inputEl.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Tab" && this._completeMention()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
main.appendChild(composer);
|
||||
|
||||
const sidebar = this._buildSidebar();
|
||||
wrapper.appendChild(main);
|
||||
wrapper.appendChild(sidebar);
|
||||
this.container.appendChild(wrapper);
|
||||
|
||||
this._openTerminal();
|
||||
this._renderTabs();
|
||||
this._renderRooms();
|
||||
this._renderUsers();
|
||||
this._renderRoomList();
|
||||
this._appendLine(SERVER_TARGET, {
|
||||
type: "system",
|
||||
text: "Connecting...",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
if (!event || !event.type) return;
|
||||
|
||||
if (event.type === "rooms") {
|
||||
this.joinedRooms = Array.isArray(event.rooms) ? event.rooms : [];
|
||||
for (const room of this.joinedRooms) {
|
||||
this._ensureTarget(room.name, room.name);
|
||||
}
|
||||
this._renderTabs();
|
||||
this._renderRooms();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "users") {
|
||||
const target = event.target;
|
||||
if (target) {
|
||||
this.users.set(target, Array.isArray(event.users) ? event.users : []);
|
||||
if (target === this.activeTarget) this._renderUsers();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "room-list") {
|
||||
this.roomList = Array.isArray(event.rooms) ? event.rooms : [];
|
||||
this._renderRoomList();
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this._eventTarget(event);
|
||||
this._ensureTarget(target, this._targetLabel(target));
|
||||
this._appendLine(target, event);
|
||||
|
||||
if (target !== this.activeTarget) {
|
||||
this.unreadTargets.add(target);
|
||||
this._renderTabs();
|
||||
}
|
||||
|
||||
if (["join", "part", "topic"].includes(event.type)) {
|
||||
this._renderRooms();
|
||||
}
|
||||
}
|
||||
|
||||
fit() {
|
||||
if (!this.fitAddon) return;
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
this.fitAddon.fit();
|
||||
} catch (_) {
|
||||
/* noop */
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
if (this.term) {
|
||||
this.term.dispose();
|
||||
this.term = null;
|
||||
}
|
||||
this.container.classList.remove("irc-viewer-panel");
|
||||
this.container.innerHTML = "";
|
||||
}
|
||||
|
||||
_buildSidebar() {
|
||||
const sidebar = createElement("aside", "irc-sidebar");
|
||||
|
||||
const joinSection = createElement("div", "irc-sidebar-section");
|
||||
joinSection.appendChild(
|
||||
createElement("div", "irc-sidebar-title", { textContent: "Rooms" }),
|
||||
);
|
||||
const joinRow = createElement("form", "irc-join-row");
|
||||
const joinInput = createElement("input", "irc-join-input", {
|
||||
type: "text",
|
||||
placeholder: "#room",
|
||||
autocomplete: "off",
|
||||
});
|
||||
const joinBtn = createElement("button", "btn btn-xs btn-secondary", {
|
||||
type: "submit",
|
||||
textContent: "Join",
|
||||
});
|
||||
joinRow.appendChild(joinInput);
|
||||
joinRow.appendChild(joinBtn);
|
||||
joinRow.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const room = joinInput.value.trim();
|
||||
if (!room) return;
|
||||
try {
|
||||
await window.NexusTerminal.wailsService.IRCJoin(this.sessionID, room);
|
||||
joinInput.value = "";
|
||||
} catch (err) {
|
||||
this._notifyError("Join failed: " + err);
|
||||
}
|
||||
});
|
||||
joinSection.appendChild(joinRow);
|
||||
this.roomsEl = createElement("div", "irc-sidebar-list");
|
||||
joinSection.appendChild(this.roomsEl);
|
||||
sidebar.appendChild(joinSection);
|
||||
|
||||
const listSection = createElement("div", "irc-sidebar-section");
|
||||
const listTitleRow = createElement("div", "irc-sidebar-title-row");
|
||||
listTitleRow.appendChild(
|
||||
createElement("div", "irc-sidebar-title", { textContent: "Room List" }),
|
||||
);
|
||||
const listBtn = createElement("button", "btn btn-xs btn-outline", {
|
||||
type: "button",
|
||||
textContent: "List",
|
||||
});
|
||||
listBtn.addEventListener("click", async () => {
|
||||
this.roomList = [];
|
||||
this._renderRoomList("Loading rooms...");
|
||||
try {
|
||||
await window.NexusTerminal.wailsService.IRCListRooms(this.sessionID);
|
||||
} catch (err) {
|
||||
this._renderRoomList("Room list failed");
|
||||
this._notifyError("Room list failed: " + err);
|
||||
}
|
||||
});
|
||||
listTitleRow.appendChild(listBtn);
|
||||
listSection.appendChild(listTitleRow);
|
||||
this.roomFilterEl = createElement("input", "irc-filter-input", {
|
||||
type: "text",
|
||||
placeholder: "Filter rooms",
|
||||
autocomplete: "off",
|
||||
});
|
||||
this.roomFilterEl.addEventListener("input", () => {
|
||||
this.roomFilter = this.roomFilterEl.value.toLowerCase();
|
||||
this._renderRoomList();
|
||||
});
|
||||
listSection.appendChild(this.roomFilterEl);
|
||||
this.roomListEl = createElement("div", "irc-room-list");
|
||||
listSection.appendChild(this.roomListEl);
|
||||
sidebar.appendChild(listSection);
|
||||
|
||||
const usersSection = createElement("div", "irc-sidebar-section irc-users-section");
|
||||
usersSection.appendChild(
|
||||
createElement("div", "irc-sidebar-title", { textContent: "Users" }),
|
||||
);
|
||||
this.usersEl = createElement("div", "irc-user-list");
|
||||
usersSection.appendChild(this.usersEl);
|
||||
sidebar.appendChild(usersSection);
|
||||
|
||||
return sidebar;
|
||||
}
|
||||
|
||||
_openTerminal() {
|
||||
const themeName =
|
||||
(this.app?.state?.preferences || {}).terminalTheme || "charcoal";
|
||||
this.term = new Terminal({
|
||||
theme: window.NexusTerminal.getTerminalTheme(themeName),
|
||||
fontFamily:
|
||||
this.profile.terminalFontFamily ||
|
||||
'JetBrains Mono, Menlo, Monaco, Consolas, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", monospace',
|
||||
fontSize: this.profile.terminalFontSize || 14,
|
||||
scrollback: this.profile.terminalScrollback || 10000,
|
||||
cursorBlink: false,
|
||||
convertEol: true,
|
||||
disableStdin: true,
|
||||
});
|
||||
this.fitAddon = new FitAddon.FitAddon();
|
||||
this.searchAddon = new SearchAddon.SearchAddon();
|
||||
this.term.loadAddon(this.fitAddon);
|
||||
this.term.loadAddon(this.searchAddon);
|
||||
try {
|
||||
this.term.loadAddon(new WebLinksAddon.WebLinksAddon((_event, uri) => {
|
||||
window.runtime.BrowserOpenURL(uri);
|
||||
}));
|
||||
} catch (_) {
|
||||
/* optional */
|
||||
}
|
||||
this.term.open(this.termHost);
|
||||
this.fit();
|
||||
this.observer = new ResizeObserver(() => this.fit());
|
||||
this.observer.observe(this.termHost);
|
||||
}
|
||||
|
||||
_renderTabs() {
|
||||
if (!this.tabsEl) return;
|
||||
this.tabsEl.innerHTML = "";
|
||||
for (const [target, state] of this.targets) {
|
||||
const btn = createElement(
|
||||
"button",
|
||||
`irc-room-tab ${target === this.activeTarget ? "active" : ""} ${
|
||||
this.unreadTargets.has(target) ? "has-unread" : ""
|
||||
}`,
|
||||
{
|
||||
type: "button",
|
||||
textContent: state.label,
|
||||
title: state.label,
|
||||
},
|
||||
);
|
||||
btn.addEventListener("click", () => this._setActiveTarget(target));
|
||||
this.tabsEl.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
_renderRooms() {
|
||||
if (!this.roomsEl) return;
|
||||
this.roomsEl.innerHTML = "";
|
||||
if (this.joinedRooms.length === 0) {
|
||||
this.roomsEl.appendChild(
|
||||
createElement("div", "irc-empty", { textContent: "No joined rooms" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const room of this.joinedRooms) {
|
||||
const row = createElement("button", "irc-room-row", {
|
||||
type: "button",
|
||||
title: room.topic || room.name,
|
||||
});
|
||||
row.classList.toggle("active", room.name === this.activeTarget);
|
||||
row.appendChild(createElement("span", "irc-room-name", { textContent: room.name }));
|
||||
row.appendChild(
|
||||
createElement("span", "irc-room-count", {
|
||||
textContent: String(room.users || 0),
|
||||
}),
|
||||
);
|
||||
row.addEventListener("click", () => this._setActiveTarget(room.name));
|
||||
this.roomsEl.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
_renderUsers() {
|
||||
if (!this.usersEl) return;
|
||||
this.usersEl.innerHTML = "";
|
||||
const users = this.users.get(this.activeTarget) || [];
|
||||
if (!this._isRoom(this.activeTarget)) {
|
||||
this.usersEl.appendChild(
|
||||
createElement("div", "irc-empty", { textContent: "No room selected" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (users.length === 0) {
|
||||
this.usersEl.appendChild(
|
||||
createElement("div", "irc-empty", { textContent: "No users yet" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const user of users) {
|
||||
const row = createElement("button", "irc-user-row", {
|
||||
type: "button",
|
||||
textContent: this._displayNick(user),
|
||||
title: user,
|
||||
});
|
||||
row.addEventListener("click", () => {
|
||||
this._insertMention(this._displayNick(user));
|
||||
});
|
||||
this.usersEl.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
_renderRoomList(message = "") {
|
||||
if (!this.roomListEl) return;
|
||||
this.roomListEl.innerHTML = "";
|
||||
if (message) {
|
||||
this.roomListEl.appendChild(
|
||||
createElement("div", "irc-empty", { textContent: message }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let rooms = this.roomList;
|
||||
if (this.roomFilter) {
|
||||
rooms = rooms.filter((room) => {
|
||||
const haystack = `${room.name || ""} ${room.topic || ""}`.toLowerCase();
|
||||
return haystack.includes(this.roomFilter);
|
||||
});
|
||||
}
|
||||
rooms = rooms.slice(0, MAX_LISTED_ROOMS);
|
||||
if (rooms.length === 0) {
|
||||
this.roomListEl.appendChild(
|
||||
createElement("div", "irc-empty", { textContent: "No rooms loaded" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const room of rooms) {
|
||||
const row = createElement("div", "irc-listed-room");
|
||||
const meta = createElement("button", "irc-listed-room-main", {
|
||||
type: "button",
|
||||
title: room.topic || room.name,
|
||||
});
|
||||
meta.appendChild(
|
||||
createElement("span", "irc-listed-room-name", {
|
||||
textContent: room.name || "",
|
||||
}),
|
||||
);
|
||||
meta.appendChild(
|
||||
createElement("span", "irc-listed-room-topic", {
|
||||
textContent: room.topic || "",
|
||||
}),
|
||||
);
|
||||
meta.addEventListener("click", () => this._joinRoom(room.name));
|
||||
const count = createElement("span", "irc-listed-room-count", {
|
||||
textContent: String(room.users || 0),
|
||||
});
|
||||
row.appendChild(meta);
|
||||
row.appendChild(count);
|
||||
this.roomListEl.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
_setActiveTarget(target) {
|
||||
this._ensureTarget(target, this._targetLabel(target));
|
||||
this.activeTarget = target;
|
||||
this.unreadTargets.delete(target);
|
||||
this._renderTabs();
|
||||
this._renderRooms();
|
||||
this._renderUsers();
|
||||
this._replayActiveTarget();
|
||||
if (this.inputEl) this.inputEl.focus();
|
||||
}
|
||||
|
||||
_appendLine(target, event) {
|
||||
const state = this._ensureTarget(target, this._targetLabel(target));
|
||||
const line = this._formatEvent(event);
|
||||
if (!line) return;
|
||||
state.lines.push(line);
|
||||
if (state.lines.length > MAX_LINES_PER_TARGET) {
|
||||
state.lines.splice(0, state.lines.length - MAX_LINES_PER_TARGET);
|
||||
}
|
||||
if (target === this.activeTarget && this.term) {
|
||||
this.term.writeln(line);
|
||||
this.term.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
_replayActiveTarget() {
|
||||
if (!this.term) return;
|
||||
this.term.clear();
|
||||
const state = this.targets.get(this.activeTarget);
|
||||
for (const line of state?.lines || []) {
|
||||
this.term.writeln(line);
|
||||
}
|
||||
this.term.scrollToBottom();
|
||||
this.fit();
|
||||
}
|
||||
|
||||
async _sendComposer() {
|
||||
const text = this.inputEl.value;
|
||||
if (!text.trim()) return;
|
||||
this.inputEl.value = "";
|
||||
try {
|
||||
await window.NexusTerminal.wailsService.IRCHandleInput(
|
||||
this.sessionID,
|
||||
this.activeTarget,
|
||||
text,
|
||||
);
|
||||
} catch (err) {
|
||||
this._notifyError(String(err));
|
||||
this._appendLine(this.activeTarget, {
|
||||
type: "error",
|
||||
text: String(err),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _joinRoom(room) {
|
||||
if (!room) return;
|
||||
try {
|
||||
await window.NexusTerminal.wailsService.IRCJoin(this.sessionID, room);
|
||||
} catch (err) {
|
||||
this._notifyError("Join failed: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
_completeMention() {
|
||||
if (!this.inputEl || !this._isRoom(this.activeTarget)) return false;
|
||||
const start = this.inputEl.selectionStart || 0;
|
||||
const before = this.inputEl.value.slice(0, start);
|
||||
const match = before.match(/(^|\s)@([^\s@]*)$/);
|
||||
if (!match) return false;
|
||||
const prefix = match[2].toLowerCase();
|
||||
const users = this.users.get(this.activeTarget) || [];
|
||||
const nick = users
|
||||
.map((user) => this._displayNick(user))
|
||||
.find((user) => user.toLowerCase().startsWith(prefix));
|
||||
if (!nick) return false;
|
||||
const replacement = `${match[1]}@${nick} `;
|
||||
const nextValue =
|
||||
this.inputEl.value.slice(0, start - match[0].length) +
|
||||
replacement +
|
||||
this.inputEl.value.slice(start);
|
||||
const caret = start - match[0].length + replacement.length;
|
||||
this.inputEl.value = nextValue;
|
||||
this.inputEl.setSelectionRange(caret, caret);
|
||||
return true;
|
||||
}
|
||||
|
||||
_insertMention(nick) {
|
||||
if (!this.inputEl) return;
|
||||
const current = this.inputEl.value;
|
||||
const spacer = current && !/\s$/.test(current) ? " " : "";
|
||||
this.inputEl.value = `${current}${spacer}@${nick} `;
|
||||
this.inputEl.focus();
|
||||
}
|
||||
|
||||
_ensureTarget(target, label) {
|
||||
const safeTarget = target || SERVER_TARGET;
|
||||
if (!this.targets.has(safeTarget)) {
|
||||
this.targets.set(safeTarget, {
|
||||
label: label || this._targetLabel(safeTarget),
|
||||
lines: [],
|
||||
});
|
||||
}
|
||||
return this.targets.get(safeTarget);
|
||||
}
|
||||
|
||||
_eventTarget(event) {
|
||||
if (!event) return SERVER_TARGET;
|
||||
if (event.target) return event.target;
|
||||
return SERVER_TARGET;
|
||||
}
|
||||
|
||||
_targetLabel(target) {
|
||||
return target === SERVER_TARGET ? "Server" : target;
|
||||
}
|
||||
|
||||
_formatEvent(event) {
|
||||
const time = this._time(event.timestamp);
|
||||
const from = this._sanitize(event.from || "");
|
||||
const text = this._sanitize(event.text || "");
|
||||
switch (event.type) {
|
||||
case "message":
|
||||
return `[${time}] <${from}> ${text}`;
|
||||
case "action":
|
||||
return `[${time}] * ${from} ${text}`;
|
||||
case "notice":
|
||||
return `[${time}] -${from || "notice"}- ${text}`;
|
||||
case "join":
|
||||
return `[${time}] --> ${from || "user"} joined`;
|
||||
case "part":
|
||||
return `[${time}] <-- ${from || "user"} left${text ? ` (${text})` : ""}`;
|
||||
case "quit":
|
||||
return `[${time}] <-- ${from || "user"} quit${text ? ` (${text})` : ""}`;
|
||||
case "nick":
|
||||
return `[${time}] ${from || "user"} is now known as ${text}`;
|
||||
case "topic":
|
||||
return `[${time}] topic: ${text}`;
|
||||
case "error":
|
||||
return `[${time}] error: ${text}`;
|
||||
case "system":
|
||||
default:
|
||||
return `[${time}] ${text}`;
|
||||
}
|
||||
}
|
||||
|
||||
_time(value) {
|
||||
const date = value ? new Date(value) : new Date();
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const mins = String(date.getMinutes()).padStart(2, "0");
|
||||
const secs = String(date.getSeconds()).padStart(2, "0");
|
||||
return `${hours}:${mins}:${secs}`;
|
||||
}
|
||||
|
||||
_sanitize(value) {
|
||||
return String(value || "")
|
||||
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
||||
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
||||
.replace(/\x03(\d{1,2}(,\d{1,2})?)?/g, "")
|
||||
.replace(/[\x02\x0f\x16\x1d\x1f]/g, "");
|
||||
}
|
||||
|
||||
_displayNick(user) {
|
||||
return String(user || "").replace(/^[~&@%+]+/, "");
|
||||
}
|
||||
|
||||
_isRoom(target) {
|
||||
return /^[#&+!]/.test(target || "");
|
||||
}
|
||||
|
||||
_notifyError(message) {
|
||||
if (this.app?.notifications) {
|
||||
this.app.notifications.error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.NexusTerminal = window.NexusTerminal || {};
|
||||
window.NexusTerminal.IRCViewer = new IRCViewer();
|
||||
})();
|
||||
+270
-2
@@ -86,6 +86,31 @@
|
||||
sshAgentAuth: false,
|
||||
},
|
||||
},
|
||||
irc: {
|
||||
port: 6667,
|
||||
protocol: "irc",
|
||||
irc: {
|
||||
nick: "nexus",
|
||||
realName: "Nexus Terminal",
|
||||
useTLS: false,
|
||||
serverPasswordRequired: false,
|
||||
serverPasswordMode: "ask-every-time",
|
||||
nickServIdentify: false,
|
||||
nickServAccount: "",
|
||||
nickServMode: "saved",
|
||||
autoJoinRooms: [],
|
||||
sshTunnel: false,
|
||||
sshHost: "",
|
||||
sshPort: 22,
|
||||
sshUsername: "",
|
||||
sshAuthMethod: "publickey",
|
||||
sshSecretMode: "saved",
|
||||
sshPrivateKey: "",
|
||||
sshAgentAuth: false,
|
||||
autoReconnect: true,
|
||||
maxReconnectTries: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class ProfileEditor {
|
||||
@@ -162,10 +187,11 @@
|
||||
drawer.appendChild(header);
|
||||
|
||||
// ── Protocol selector tabs ──────────────────────────────
|
||||
const protocols = ["ssh", "telnet", "raw", "github", "vnc"];
|
||||
const protocols = ["ssh", "telnet", "raw", "github", "vnc", "irc"];
|
||||
const tabBar = createElement("div", "drawer-protocol-tabs");
|
||||
for (const proto of protocols) {
|
||||
const label = proto === "github" ? "GitHub" : proto.toUpperCase();
|
||||
const label =
|
||||
proto === "github" ? "GitHub" : proto === "irc" ? "IRC" : proto.toUpperCase();
|
||||
const btn = createElement(
|
||||
"button",
|
||||
`drawer-proto-tab ${profile.protocol === proto ? "active" : ""}`,
|
||||
@@ -208,6 +234,10 @@
|
||||
case "vnc":
|
||||
this._renderVNCSections(profile).forEach((s) => body.appendChild(s));
|
||||
break;
|
||||
case "irc":
|
||||
this._renderIRCSections(profile).forEach((s) => body.appendChild(s));
|
||||
body.appendChild(sshKeyDatalist);
|
||||
break;
|
||||
}
|
||||
|
||||
// Terminal section (SSH and Telnet only)
|
||||
@@ -1075,6 +1105,185 @@
|
||||
return sections;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_renderIRCSections(profile) {
|
||||
const sections = [];
|
||||
const irc = profile.irc || DEFAULTS.irc.irc;
|
||||
|
||||
sections.push(
|
||||
this._renderSection("IRC", [
|
||||
this._field("text", "irc.nick", "Nick", irc.nick, {
|
||||
placeholder: "yourNick",
|
||||
}),
|
||||
this._field("text", "irc.realName", "Real Name", irc.realName, {
|
||||
placeholder: "Shown in WHOIS",
|
||||
}),
|
||||
this._field("toggle", "irc.useTLS", "Use TLS", irc.useTLS, {
|
||||
help: "Use port 6697 for most TLS IRC networks.",
|
||||
}),
|
||||
this._field(
|
||||
"toggle",
|
||||
"irc.serverPasswordRequired",
|
||||
"Server Password",
|
||||
irc.serverPasswordRequired,
|
||||
{
|
||||
help: "Prompt for an IRC PASS secret before registration.",
|
||||
},
|
||||
),
|
||||
this._field(
|
||||
"select",
|
||||
"irc.serverPasswordMode",
|
||||
"Server Password Storage",
|
||||
irc.serverPasswordMode,
|
||||
{
|
||||
options: [
|
||||
{ value: "saved", label: "Save to keychain" },
|
||||
{ value: "session-only", label: "Remember for session" },
|
||||
{ value: "ask-every-time", label: "Ask every time" },
|
||||
],
|
||||
},
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
sections.push(
|
||||
this._renderSection("NickServ", [
|
||||
this._field(
|
||||
"toggle",
|
||||
"irc.nickServIdentify",
|
||||
"Identify on Connect",
|
||||
irc.nickServIdentify,
|
||||
),
|
||||
this._field(
|
||||
"text",
|
||||
"irc.nickServAccount",
|
||||
"Account",
|
||||
irc.nickServAccount,
|
||||
{
|
||||
placeholder: "Optional if it matches your nick",
|
||||
},
|
||||
),
|
||||
this._field(
|
||||
"select",
|
||||
"irc.nickServMode",
|
||||
"Password Storage",
|
||||
irc.nickServMode,
|
||||
{
|
||||
options: [
|
||||
{ value: "saved", label: "Save to keychain" },
|
||||
{ value: "session-only", label: "Remember for session" },
|
||||
{ value: "ask-every-time", label: "Ask every time" },
|
||||
],
|
||||
},
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
sections.push(
|
||||
this._renderSection("Rooms", [
|
||||
this._field(
|
||||
"textarea",
|
||||
"irc.autoJoinRoomsText",
|
||||
"Auto-Join Rooms",
|
||||
this._roomsToText(irc.autoJoinRooms),
|
||||
{
|
||||
placeholder: "#general\n#ops",
|
||||
rows: 4,
|
||||
help: "One room per line or comma-separated.",
|
||||
},
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
sections.push(
|
||||
this._renderSection("SSH Tunnel", [
|
||||
this._field(
|
||||
"toggle",
|
||||
"irc.sshTunnel",
|
||||
"Enable SSH Tunnel",
|
||||
irc.sshTunnel,
|
||||
),
|
||||
this._field("text", "irc.sshHost", "SSH Host", irc.sshHost, {
|
||||
placeholder: "jump.example.com",
|
||||
}),
|
||||
this._field("number", "irc.sshPort", "SSH Port", irc.sshPort, {
|
||||
min: 1,
|
||||
max: 65535,
|
||||
}),
|
||||
this._field(
|
||||
"text",
|
||||
"irc.sshUsername",
|
||||
"SSH Username",
|
||||
irc.sshUsername,
|
||||
{
|
||||
placeholder: "admin",
|
||||
},
|
||||
),
|
||||
this._field(
|
||||
"select",
|
||||
"irc.sshAuthMethod",
|
||||
"SSH Auth Method",
|
||||
irc.sshAuthMethod,
|
||||
{
|
||||
options: [
|
||||
{ value: "publickey", label: "Public Key" },
|
||||
{ value: "password", label: "Password" },
|
||||
],
|
||||
},
|
||||
),
|
||||
this._field(
|
||||
"text",
|
||||
"irc.sshPrivateKey",
|
||||
"SSH Private Key Path",
|
||||
irc.sshPrivateKey,
|
||||
{
|
||||
placeholder: "~/.ssh/id_ed25519",
|
||||
datalistId: "ssh-key-path-options",
|
||||
},
|
||||
),
|
||||
this._field(
|
||||
"toggle",
|
||||
"irc.sshAgentAuth",
|
||||
"Use SSH Agent",
|
||||
irc.sshAgentAuth,
|
||||
),
|
||||
this._field(
|
||||
"select",
|
||||
"irc.sshSecretMode",
|
||||
"SSH Secret Storage",
|
||||
irc.sshSecretMode,
|
||||
{
|
||||
options: [
|
||||
{ value: "saved", label: "Save to keychain" },
|
||||
{ value: "session-only", label: "Remember for session" },
|
||||
{ value: "ask-every-time", label: "Ask every time" },
|
||||
],
|
||||
},
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
sections.push(
|
||||
this._renderSection("Reconnection", [
|
||||
this._field(
|
||||
"toggle",
|
||||
"irc.autoReconnect",
|
||||
"Auto-Reconnect",
|
||||
irc.autoReconnect,
|
||||
),
|
||||
this._field(
|
||||
"number",
|
||||
"irc.maxReconnectTries",
|
||||
"Max Reconnect Attempts",
|
||||
irc.maxReconnectTries || 5,
|
||||
{ min: 1, max: 20 },
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_renderTerminalSection(profile) {
|
||||
return this._renderSection("Terminal", [
|
||||
@@ -2275,6 +2484,34 @@
|
||||
p.vnc.sshAgentAuth = val("vnc.sshAgentAuth") || false;
|
||||
p.vnc.sshSecretMode = val("vnc.sshSecretMode") || "saved";
|
||||
}
|
||||
|
||||
// IRC-specific
|
||||
if (p.protocol === "irc") {
|
||||
if (!p.irc) p.irc = {};
|
||||
p.irc.nick = val("irc.nick") || "nexus";
|
||||
p.irc.realName = val("irc.realName") || "Nexus Terminal";
|
||||
p.irc.useTLS = val("irc.useTLS") || false;
|
||||
p.irc.serverPasswordRequired =
|
||||
val("irc.serverPasswordRequired") || false;
|
||||
p.irc.serverPasswordMode =
|
||||
val("irc.serverPasswordMode") || "ask-every-time";
|
||||
p.irc.nickServIdentify = val("irc.nickServIdentify") || false;
|
||||
p.irc.nickServAccount = val("irc.nickServAccount") || "";
|
||||
p.irc.nickServMode = val("irc.nickServMode") || "saved";
|
||||
p.irc.autoJoinRooms = this._parseRoomList(
|
||||
val("irc.autoJoinRoomsText") || "",
|
||||
);
|
||||
p.irc.sshTunnel = val("irc.sshTunnel") || false;
|
||||
p.irc.sshHost = val("irc.sshHost") || "";
|
||||
p.irc.sshPort = val("irc.sshPort") || 22;
|
||||
p.irc.sshUsername = val("irc.sshUsername") || "";
|
||||
p.irc.sshAuthMethod = val("irc.sshAuthMethod") || "publickey";
|
||||
p.irc.sshPrivateKey = val("irc.sshPrivateKey") || "";
|
||||
p.irc.sshAgentAuth = val("irc.sshAgentAuth") || false;
|
||||
p.irc.sshSecretMode = val("irc.sshSecretMode") || "saved";
|
||||
p.irc.autoReconnect = val("irc.autoReconnect") !== false;
|
||||
p.irc.maxReconnectTries = val("irc.maxReconnectTries") || 5;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Validation ───────────────────────────────────────────────────
|
||||
@@ -2300,6 +2537,25 @@
|
||||
if (!profile.port || profile.port < 1 || profile.port > 65535) {
|
||||
errors.push("Port must be between 1 and 65535.");
|
||||
}
|
||||
if (profile.protocol === "irc") {
|
||||
const nick = profile.irc?.nick || "";
|
||||
if (!nick.trim()) {
|
||||
errors.push("IRC nick is required.");
|
||||
}
|
||||
for (const room of profile.irc?.autoJoinRooms || []) {
|
||||
if (!/^[#&+!][^\s,]+$/.test(room)) {
|
||||
errors.push(`Invalid IRC room "${room}".`);
|
||||
}
|
||||
}
|
||||
if (profile.irc?.sshTunnel) {
|
||||
if (!profile.irc.sshHost?.trim()) {
|
||||
errors.push("SSH Host is required when tunnelling IRC.");
|
||||
}
|
||||
if (!profile.irc.sshUsername?.trim()) {
|
||||
errors.push("SSH Username is required when tunnelling IRC.");
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -2476,6 +2732,7 @@
|
||||
raw: null,
|
||||
github: null,
|
||||
vnc: null,
|
||||
irc: null,
|
||||
};
|
||||
|
||||
const defaults = DEFAULTS[protocol] || {};
|
||||
@@ -2494,6 +2751,17 @@
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
_roomsToText(rooms) {
|
||||
return Array.isArray(rooms) ? rooms.join("\n") : "";
|
||||
}
|
||||
|
||||
_parseRoomList(value) {
|
||||
return String(value || "")
|
||||
.split(/[\n,]/)
|
||||
.map((room) => room.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
window.NexusTerminal = window.NexusTerminal || {};
|
||||
|
||||
@@ -926,6 +926,8 @@
|
||||
return "TELNET";
|
||||
case "raw":
|
||||
return "RAW";
|
||||
case "irc":
|
||||
return "IRC";
|
||||
default:
|
||||
return (protocol || "").toUpperCase() || "UNKNOWN";
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@
|
||||
this.app.terminals.destroyTerminal(sessionId);
|
||||
this.app.rawViewer.destroyViewer(sessionId);
|
||||
window.NexusTerminal.GitHubViewer.destroyViewer(sessionId);
|
||||
window.NexusTerminal.IRCViewer.destroyViewer(sessionId);
|
||||
this.app.leviathan?.destroyPanel(sessionId);
|
||||
this.removeTab(sessionId);
|
||||
this.app.state.removeSession(sessionId);
|
||||
|
||||
@@ -290,6 +290,8 @@
|
||||
return "proto-github";
|
||||
case "vnc":
|
||||
return "proto-vnc";
|
||||
case "irc":
|
||||
return "proto-irc";
|
||||
case "local":
|
||||
return "proto-local";
|
||||
default:
|
||||
|
||||
Vendored
+12
@@ -80,6 +80,18 @@ export function GetVNCWebSocketURL(arg1:string):Promise<string>;
|
||||
|
||||
export function HasTOTPSecret(arg1:string):Promise<boolean>;
|
||||
|
||||
export function IRCHandleInput(arg1:string,arg2:string,arg3:string):Promise<void>;
|
||||
|
||||
export function IRCJoin(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function IRCListRooms(arg1:string):Promise<void>;
|
||||
|
||||
export function IRCPart(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function IRCRooms(arg1:string):Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function IRCUsers(arg1:string,arg2:string):Promise<Array<string>>;
|
||||
|
||||
export function ImportProfiles(arg1:string,arg2:string):Promise<number>;
|
||||
|
||||
export function ImportProfilesEncrypted(arg1:string,arg2:string):Promise<number>;
|
||||
|
||||
@@ -154,6 +154,30 @@ export function HasTOTPSecret(arg1) {
|
||||
return window['go']['api']['Service']['HasTOTPSecret'](arg1);
|
||||
}
|
||||
|
||||
export function IRCHandleInput(arg1, arg2, arg3) {
|
||||
return window['go']['api']['Service']['IRCHandleInput'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function IRCJoin(arg1, arg2) {
|
||||
return window['go']['api']['Service']['IRCJoin'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function IRCListRooms(arg1) {
|
||||
return window['go']['api']['Service']['IRCListRooms'](arg1);
|
||||
}
|
||||
|
||||
export function IRCPart(arg1, arg2) {
|
||||
return window['go']['api']['Service']['IRCPart'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function IRCRooms(arg1) {
|
||||
return window['go']['api']['Service']['IRCRooms'](arg1);
|
||||
}
|
||||
|
||||
export function IRCUsers(arg1, arg2) {
|
||||
return window['go']['api']['Service']['IRCUsers'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ImportProfiles(arg1, arg2) {
|
||||
return window['go']['api']['Service']['ImportProfiles'](arg1, arg2);
|
||||
}
|
||||
|
||||
@@ -181,6 +181,54 @@ export namespace models {
|
||||
this.updatedAt = source["updatedAt"];
|
||||
}
|
||||
}
|
||||
export class IRCConfig {
|
||||
nick: string;
|
||||
realName: string;
|
||||
useTLS: boolean;
|
||||
serverPasswordRequired: boolean;
|
||||
serverPasswordMode: string;
|
||||
nickServIdentify: boolean;
|
||||
nickServAccount: string;
|
||||
nickServMode: string;
|
||||
autoJoinRooms: string[];
|
||||
sshTunnel: boolean;
|
||||
sshHost: string;
|
||||
sshPort: number;
|
||||
sshUsername: string;
|
||||
sshAuthMethod: string;
|
||||
sshSecretMode: string;
|
||||
sshPrivateKeyPath: string;
|
||||
sshAgentAuth: boolean;
|
||||
autoReconnect: boolean;
|
||||
maxReconnectTries: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new IRCConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.nick = source["nick"];
|
||||
this.realName = source["realName"];
|
||||
this.useTLS = source["useTLS"];
|
||||
this.serverPasswordRequired = source["serverPasswordRequired"];
|
||||
this.serverPasswordMode = source["serverPasswordMode"];
|
||||
this.nickServIdentify = source["nickServIdentify"];
|
||||
this.nickServAccount = source["nickServAccount"];
|
||||
this.nickServMode = source["nickServMode"];
|
||||
this.autoJoinRooms = source["autoJoinRooms"];
|
||||
this.sshTunnel = source["sshTunnel"];
|
||||
this.sshHost = source["sshHost"];
|
||||
this.sshPort = source["sshPort"];
|
||||
this.sshUsername = source["sshUsername"];
|
||||
this.sshAuthMethod = source["sshAuthMethod"];
|
||||
this.sshSecretMode = source["sshSecretMode"];
|
||||
this.sshPrivateKeyPath = source["sshPrivateKeyPath"];
|
||||
this.sshAgentAuth = source["sshAgentAuth"];
|
||||
this.autoReconnect = source["autoReconnect"];
|
||||
this.maxReconnectTries = source["maxReconnectTries"];
|
||||
}
|
||||
}
|
||||
export class JumpHost {
|
||||
host: string;
|
||||
port: number;
|
||||
@@ -520,6 +568,7 @@ export namespace models {
|
||||
raw?: RAWConfig;
|
||||
github?: GitHubConfig;
|
||||
vnc?: VNCConfig;
|
||||
irc?: IRCConfig;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Profile(source);
|
||||
@@ -553,6 +602,7 @@ export namespace models {
|
||||
this.raw = this.convertValues(source["raw"], RAWConfig);
|
||||
this.github = this.convertValues(source["github"], GitHubConfig);
|
||||
this.vnc = this.convertValues(source["vnc"], VNCConfig);
|
||||
this.irc = this.convertValues(source["irc"], IRCConfig);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
|
||||
Reference in New Issue
Block a user