Improve WorkerWaiter and OutputWaiter.

- Will run and display outputs in the output area as they're baked
- Creates output tabs
- Can change output tabs (only the first 4 at the moment)
This commit is contained in:
j433866 2019-04-02 16:58:36 +01:00
parent 2cb33bfec4
commit e0c9aba25e
5 changed files with 814 additions and 727 deletions

View File

@ -90,7 +90,7 @@ class InputWaiter {
}
/**
* Removes a loaderworker using inputNum
* Removes a loaderworker
*
* @param {Object} workerObj
*/
@ -203,14 +203,6 @@ class InputWaiter {
this.inputs.splice(i, 1);
}
}
// if (this.inputs.length === 0) {
// this.inputs.push({
// inputNum: inputNum,
// data: "",
// status: "loaded",
// progress: 100
// });
// }
}
/**
@ -327,14 +319,11 @@ class InputWaiter {
if (index === -1) {
return null;
}
if (this.inputs[index].inputNum === inputNum) {
if (typeof this.inputs[index].data === "string") {
return this.inputs[index].data;
} else {
return this.inputs[index].data.fileBuffer;
}
if (typeof this.inputs[index].data === "string") {
return this.inputs[index].data;
} else {
return this.inputs[index].data.fileBuffer;
}
return null;
}
/**
@ -359,7 +348,8 @@ class InputWaiter {
const value = (textArea.value !== undefined) ? textArea.value : "";
const inputNum = this.getActiveTab();
if (this.getInput(inputNum) === null || typeof this.getInput(inputNum) === "string") {
const input = this.getInput(inputNum);
if (input === null || typeof input === "string") {
this.updateInputValue(inputNum, value);
}
@ -774,6 +764,8 @@ class InputWaiter {
progress: 100
});
this.manager.output.addOutput(inputNum, changeTab);
const tabsWrapper = document.getElementById("input-tabs");
const numTabs = tabsWrapper.children.length;
@ -831,6 +823,8 @@ class InputWaiter {
}
this.refreshTabs(activeTab);
}
this.manager.output.removeTab(inputNum);
}
/**
@ -883,6 +877,8 @@ class InputWaiter {
}
this.changeTab(activeTab);
// MAKE THE OUTPUT REFRESH TOO
}
/**
@ -966,14 +962,8 @@ class InputWaiter {
* @param {number} inputNum
*/
changeTab(inputNum) {
const inputIdx = this.getInputIndex(inputNum);
let currentIdx = -1;
try {
currentIdx = this.getActiveTab();
} catch (err) {}
if (inputIdx === -1) {
return;
}
const currentNum = this.getActiveTab();
if (this.getInputIndex(inputNum) === -1) return;
const tabsWrapper = document.getElementById("input-tabs");
const tabs = tabsWrapper.children;
@ -990,7 +980,7 @@ class InputWaiter {
if (!found) {
// Shift the tabs here
let direction = "right";
if (currentIdx > inputIdx) {
if (currentNum > inputNum) {
direction = "left";
}
@ -1122,9 +1112,8 @@ class InputWaiter {
const activeTab = activeTabs.item(0);
const tabNum = activeTab.getAttribute("inputNum");
return parseInt(tabNum, 10);
} else {
return -1;
}
return -1;
}
/**

View File

@ -84,7 +84,7 @@ class Manager {
setup() {
this.input.addTab();
this.input.setupLoaderWorker();
this.worker.setupChefWorkers();
this.worker.setupChefWorker();
this.recipe.initialiseOperationDragNDrop();
this.controls.initComponents();
this.controls.autoBakeChange();
@ -165,24 +165,24 @@ class Manager {
this.addDynamicListener("#input-tabs li .input-tab-content", "click", this.input.changeTabClick, this.input);
// Output
document.getElementById("save-to-file").addEventListener("click", this.output.saveClick.bind(this.output));
document.getElementById("copy-output").addEventListener("click", this.output.copyClick.bind(this.output));
document.getElementById("switch").addEventListener("click", this.output.switchClick.bind(this.output));
document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output));
document.getElementById("maximise-output").addEventListener("click", this.output.maximiseOutputClick.bind(this.output));
document.getElementById("magic").addEventListener("click", this.output.magicClick.bind(this.output));
document.getElementById("output-text").addEventListener("scroll", this.highlighter.outputScroll.bind(this.highlighter));
document.getElementById("output-text").addEventListener("mouseup", this.highlighter.outputMouseup.bind(this.highlighter));
document.getElementById("output-text").addEventListener("mousemove", this.highlighter.outputMousemove.bind(this.highlighter));
document.getElementById("output-html").addEventListener("mouseup", this.highlighter.outputHtmlMouseup.bind(this.highlighter));
document.getElementById("output-html").addEventListener("mousemove", this.highlighter.outputHtmlMousemove.bind(this.highlighter));
this.addMultiEventListener("#output-text", "mousedown dblclick select", this.highlighter.outputMousedown, this.highlighter);
this.addMultiEventListener("#output-html", "mousedown dblclick select", this.highlighter.outputHtmlMousedown, this.highlighter);
this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output);
this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output);
// this.addDynamicListener("#output-tabs ul li .output-tab-content", "click", this.output.changeTabClick, this.output);
// document.getElementById("save-to-file").addEventListener("click", this.output.saveClick.bind(this.output));
// document.getElementById("copy-output").addEventListener("click", this.output.copyClick.bind(this.output));
// document.getElementById("switch").addEventListener("click", this.output.switchClick.bind(this.output));
// document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output));
// document.getElementById("maximise-output").addEventListener("click", this.output.maximiseOutputClick.bind(this.output));
// document.getElementById("magic").addEventListener("click", this.output.magicClick.bind(this.output));
// document.getElementById("output-text").addEventListener("scroll", this.highlighter.outputScroll.bind(this.highlighter));
// document.getElementById("output-text").addEventListener("mouseup", this.highlighter.outputMouseup.bind(this.highlighter));
// document.getElementById("output-text").addEventListener("mousemove", this.highlighter.outputMousemove.bind(this.highlighter));
// document.getElementById("output-html").addEventListener("mouseup", this.highlighter.outputHtmlMouseup.bind(this.highlighter));
// document.getElementById("output-html").addEventListener("mousemove", this.highlighter.outputHtmlMousemove.bind(this.highlighter));
// this.addMultiEventListener("#output-text", "mousedown dblclick select", this.highlighter.outputMousedown, this.highlighter);
// this.addMultiEventListener("#output-html", "mousedown dblclick select", this.highlighter.outputHtmlMousedown, this.highlighter);
// this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output);
// this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
// document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
// this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output);
this.addDynamicListener("#output-tabs-wrapper #output-tabs li .output-tab-content", "click", this.output.changeTabClick, this.output);
// Options
document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options));

File diff suppressed because it is too large Load Diff

View File

@ -8,85 +8,170 @@
import ChefWorker from "worker-loader?inline&fallback=false!../core/ChefWorker";
/**
* Waiter to handle conversations with the ChefWorker.
* Waiter to handle conversations with the ChefWorker
*/
class WorkerWaiter {
/**
* WorkerWaiter constructor.
* WorkerWaiter constructor
*
* @param {App} app - The main view object for CyberChef
* @param {Manager} manager - The CyberChef event manager.
* @param {Manager} manager - The CyberChef event manager
*/
constructor(app, manager) {
this.app = app;
this.manager = manager;
this.callbacks = {};
this.callbackID = 0;
this.pendingInputs = [];
this.runningWorkers = 0;
this.chefWorkers = [];
this.outputs = [];
this.maxWorkers = navigator.hardwareConcurrency || 4;
this.inputs = [];
this.totalOutputs = 0;
}
/**
* Sets up a pool of ChefWorkers to be used for baking
* Terminates any existing ChefWorkers and sets up a new worker
*/
setupChefWorkers() {
const threads = navigator.hardwareConcurrency || 4; // Default to 4
setupChefWorker() {
for (let i = 0; i < this.chefWorkers.length; i++) {
const worker = this.chefWorkers.pop();
worker.terminate();
}
for (let i = 0; i < threads; i++) {
const newWorker = new ChefWorker();
newWorker.addEventListener("message", this.handleChefMessage.bind(this));
let docURL = document.location.href.split(/[#?]/)[0];
const index = docURL.lastIndexOf("/");
if (index > 0) {
docURL = docURL.substring(0, index);
this.addChefWorker();
}
/**
* Adds a new ChefWorker
*
* @returns {number} The index of the created worker
*/
addChefWorker() {
// First find if there are any inactive workers, as this will be
// more efficient than creating a new one
for (let i = 0; i < this.chefWorkers.length; i++) {
if (!this.chefWorkers[i].active) {
return i;
}
newWorker.postMessage({"action": "docURL", "data": docURL});
newWorker.postMessage({"action": "inputNum", "data": 0});
}
this.chefWorkers.push({
worker: newWorker,
inputNum: 0
});
if (this.chefWorkers.length === this.maxWorkers) {
// Can't create any more workers
return -1;
}
log.debug("Adding new ChefWorker");
// Create a new ChefWorker and send it the docURL
const newWorker = new ChefWorker();
newWorker.addEventListener("message", this.handleChefMessage.bind(this));
let docURL = document.location.href.split(/[#?]/)[0];
const index = docURL.lastIndexOf("/");
if (index > 0) {
docURL = docURL.substring(0, index);
}
newWorker.postMessage({"action": "docURL", "data": docURL});
// Store the worker, whether or not it's active, and the inputNum as an object
const newWorkerObj = {
worker: newWorker,
active: false,
inputNum: this.manager.input.getActiveTab()
};
this.chefWorkers.push(newWorkerObj);
return this.chefWorkers.indexOf(newWorkerObj);
}
/**
* Removes a ChefWorker
*
* @param {Object} workerObj
*/
removeChefWorker(workerObj) {
const index = this.chefWorkers.indexOf(workerObj);
if (index === -1) {
return;
}
this.chefWorkers[index].worker.terminate();
this.chefWorkers.splice(index, 1);
// There should always be a ChefWorker loaded
if (this.chefWorkers.length === 0) {
this.addChefWorker();
}
}
/**
* Finds and returns the object for the ChefWorker of a given inputNum
*
* @param {number} inputNum
*/
getChefWorker(inputNum) {
for (let i = 0; i < this.chefWorkers.length; i++) {
if (this.chefWorkers[i].inputNum === inputNum) {
return this.chefWorkers[i];
}
}
}
/**
* Handler for messages sent back by the ChefWorker.
* Handler for messages sent back by the ChefWorkers
*
* @param {MessageEvent} e
*/
handleChefMessage(e) {
const r = e.data;
log.debug("Receiving '" + r.action + "' from ChefWorker");
let inputNum = 0;
log.debug(`Receiving ${r.action} from ChefWorker.`);
if (r.data.hasOwnProperty("inputNum")) {
inputNum = r.data.inputNum;
}
const currentWorker = this.getChefWorker(inputNum);
switch (r.action) {
case "bakeComplete":
this.runningWorkers -= 1;
this.outputs.push({
data: r.data,
inputNum: r.data.inputNum
});
if (this.pendingInputs.length > 0) {
log.debug(`Bake ${r.data.inputNum} complete. Baking next input`);
this.bakeNextInput(r.data.inputNum);
} else if (this.runningWorkers <= 0) {
this.runningWorkers = 0;
this.recipeConfig = undefined;
this.options = undefined;
this.progress = undefined;
this.step = undefined;
this.bakingComplete();
log.debug(`Bake ${inputNum} complete.`);
this.updateOutput(r.data, r.data.inputNum);
if (this.inputs.length > 0) {
const nextInput = this.inputs.pop();
log.debug(`Baking input ${nextInput.inputNum}.`);
this.manager.output.updateOutputStatus("baking", nextInput.inputNum);
this.manager.output.updateOutputMessage("Baking...", nextInput.inputNum);
currentWorker.inputNum = nextInput.inputNum;
currentWorker.active = true;
currentWorker.worker.postMessage({
action: "bake",
data: {
input: nextInput.input,
recipeConfig: nextInput.recipeConfig,
options: nextInput.options,
progress: nextInput.progress,
step: nextInput.step,
inputNum: nextInput.inputNum
}
});
this.displayProgress();
} else {
// The ChefWorker is no longer needed
log.debug("No more inputs to bake. Closing ChefWorker.");
this.removeChefWorker(currentWorker);
this.displayProgress();
const progress = this.getBakeProgress();
if (progress.total === progress.baked) {
this.bakingComplete();
}
}
break;
case "bakeError":
this.runningWorkers -= 1;
this.app.handleError(r.data);
this.setBakingStatus(false);
case "BakeError":
this.manager.output.updateOutputError(r.data, inputNum);
// do more here
break;
case "dishReturned":
this.callbacks[r.data.id](r.data);
@ -95,17 +180,20 @@ class WorkerWaiter {
break;
case "workerLoaded":
this.app.workerLoaded = true;
log.debug("ChefWorker loaded");
log.debug("ChefWorker loaded.");
this.app.loaded();
break;
case "statusMessage":
this.manager.output.setStatusMsg(r.data);
// Status message should be done per output
// log.error(r);
this.manager.output.updateOutputMessage(r.data.message, r.data.inputNum);
break;
case "optionUpdate":
log.debug(`Setting ${r.data.option} to ${r.data.value}`);
this.app.options[r.data.option] = r.data.value;
break;
case "setRegisters":
// Should this update with the tabs?
this.manager.recipe.setRegisters(r.data.opIndex, r.data.numPrevRegisters, r.data.registers);
break;
case "highlightsCalculated":
@ -117,78 +205,96 @@ class WorkerWaiter {
}
}
/**
* Update the value of an output
*
* @param {Object} data
* @param {number} inputNum
*/
updateOutput(data, inputNum) {
this.manager.output.updateOutputValue(data, inputNum);
this.manager.output.updateOutputStatus("baked", inputNum);
this.manager.recipe.updateBreakpointIndicator(this.app.progress);
}
/**
* Updates the UI to show if baking is in process or not.
*
* @param {bakingStatus}
* @param {boolean} bakingStatus
*/
setBakingStatus(bakingStatus) {
this.app.baking = bakingStatus;
this.manager.output.toggleLoader(bakingStatus);
this.manager.controls.toggleBakeButtonFunction(bakingStatus);
}
/**
* Get the progress of the ChefWorkers
*/
getBakeProgress() {
const pendingInputs = this.inputs.length;
let bakingInputs = 0;
for (let i = 0; i < this.chefWorkers.length; i++) {
if (this.chefWorkers[i].active) {
bakingInputs++;
}
}
const total = this.totalOutputs;
const bakedInputs = total - pendingInputs - bakingInputs;
return {
total: total,
pending: pendingInputs,
baking: bakingInputs,
baked: bakedInputs
};
}
/**
* Calcels the current bake by terminating and removing all ChefWorkers,
* and creating a new one
* Cancels the current bake by terminating and removing all ChefWorkers
*/
cancelBake() {
for (let i = this.chefWorkers.length - 1; i >= 0; i--) {
this.chefWorkers[i].worker.terminate();
this.chefWorkers.pop();
this.removeChefWorker(this.chefWorkers[i]);
}
this.setupChefWorkers();
this.setBakingStatus(false);
this.inputs = [];
this.totalOutputs = 0;
this.manager.controls.showStaleIndicator();
this.displayProgress();
}
/**
* Handler for completed bakes
*/
bakingComplete() {
this.setBakingStatus(false);
if (this.pendingInputs.length !== 0) return;
// look into changing this to something better
// for (let i = 0; i < this.outputs.length; i++) {
// if (this.outputs[i].data.error) {
// this.app.handleError(this.outputs[i].error);
// }
// }
for (let i = 0; i < this.outputs.length; i++) {
if (this.outputs[i].error) {
this.app.handleError(this.outputs[i].error);
}
}
this.app.progress = this.outputs[0].data.progress;
this.app.dish = this.outputs[0].data.dish;
// What are these for?
// Should be a value for each input, not just one
// this.app.progress = this.outputs[0].data.progress;
// this.app.dish = this.outputs[0].data.dish;
this.manager.recipe.updateBreakpointIndicator(this.app.progress);
this.manager.output.multiSet(this.outputs);
// Don't need to update the output here as updateOutput() will take care of that
document.getElementById("bake").style.background = "";
this.totalOutputs = 0; // Reset for next time
log.debug("--- Bake complete ---");
}
/**
* Handler for completed bakes
*
* @param {Object} response
*/
bakingCompleteOld(response) {
this.setBakingStatus(false);
if (!response) return;
if (response.error) {
this.app.handleError(response.error);
}
this.app.progress = response.progress;
this.app.dish = response.dish;
this.manager.recipe.updateBreakpointIndicator(response.progress);
this.manager.output.set(response.result, response.type, response.duration);
log.debug("--- Bake complete ---");
}
/**
* Asks the ChefWorker to bake the current input using the current recipe.
* Bakes the current input using the current recipe.
* Either sends the input and recipe to a ChefWorker,
* or, if there's already the max running, adds it to inputs
*
* @param {string | Array} input
* @param {Object[]} recipeConfig
@ -199,67 +305,55 @@ class WorkerWaiter {
bake(input, recipeConfig, options, progress, step) {
this.setBakingStatus(true);
this.recipeConfig = recipeConfig;
this.options = options;
this.progress = progress;
this.step = step;
this.outputs = [];
if (typeof input === "string") {
input = [{
input: input,
inputNum: 0
inputNum: this.manager.input.getActiveTab()
}];
}
const initialInputs = input.slice(0, this.chefWorkers.length);
this.pendingInputs = input.slice(this.chefWorkers.length, input.length);
this.runningWorkers = 0;
for (let i = 0; i < initialInputs.length; i++) {
this.runningWorkers += 1;
this.chefWorkers[i].inputNum = initialInputs[i].inputNum;
this.chefWorkers[i].worker.postMessage({
action: "bake",
data: {
input: initialInputs[i].input,
for (let i = 0; i < input.length; i++) {
this.totalOutputs++;
this.manager.output.updateOutputStatus("pending", input[i].inputNum);
this.manager.output.updateOutputMessage(`Input ${input[i].inputNum} has not been baked yet.`, input[i].inputNum);
// If an input exists for the current inputNum, remove it
for (let x = 0; x < this.inputs.length; x++) {
if (this.inputs[x].inputNum === input[i].inputNum) {
this.inputs.splice(x, 1);
}
}
const workerId = this.addChefWorker();
if (workerId !== -1) {
// Send the input to the ChefWorker
this.manager.output.updateOutputStatus("baking", input[i].inputNum);
this.manager.output.updateOutputMessage("Baking...", input[i].inputNum);
this.chefWorkers[workerId].active = true;
this.chefWorkers[workerId].inputNum = input[i].inputNum;
this.chefWorkers[workerId].worker.postMessage({
action: "bake",
data: {
input: input[i].input,
recipeConfig: recipeConfig,
options: options,
progress: progress,
step: step,
inputNum: input[i].inputNum
}
});
} else {
// Add the input to inputs so it can be processed when ready
this.inputs.push({
input: input[i].input,
recipeConfig: recipeConfig,
options: options,
progress: progress,
step: step,
inputNum: initialInputs[i].inputNum
}
});
}
}
/**
*
* @param inputNum
*/
bakeNextInput(inputNum) {
this.runningWorkers += 1;
const nextInput = this.pendingInputs.pop();
for (let i = 0; i < this.chefWorkers.length; i++) {
if (this.chefWorkers[i].inputNum === inputNum) {
this.chefWorkers[i].inputNum = nextInput.inputNum;
this.chefWorkers[i].worker.postMessage({
action: "bake",
data: {
input: nextInput.input,
recipeConfig: this.recipeConfig,
options: this.options,
progress: this.progress,
step: this.step,
inputNum: nextInput.inputNum
}
inputNum: input[i].inputNum
});
}
}
}
/**
* Asks the ChefWorker to run a silent bake, forcing the browser to load and cache all the relevant
* JavaScript code needed to do a real bake.
@ -267,7 +361,11 @@ class WorkerWaiter {
* @param {Object[]} [recipeConfig]
*/
silentBake(recipeConfig) {
this.chefWorkers[0].worker.postMessage({
// If there aren't any active ChefWorkers, addChefWorker will
// return an inactive worker instead of creating a new one
const workerId = this.addChefWorker();
if (workerId === -1) return;
this.chefWorkers[workerId].worker.postMessage({
action: "silentBake",
data: {
recipeConfig: recipeConfig
@ -275,28 +373,6 @@ class WorkerWaiter {
});
}
/**
* Asks the ChefWorker to calculate highlight offsets if possible.
*
* @param {Object[]} recipeConfig
* @param {string} direction
* @param {Object} pos - The position object for the highlight.
* @param {number} pos.start - The start offset.
* @param {number} pos.end - The end offset.
*/
highlight(recipeConfig, direction, pos) {
this.chefWorkers[0].postMessage({
action: "highlight",
data: {
recipeConfig: recipeConfig,
direction: direction,
pos: pos
}
});
}
/**
* Asks the ChefWorker to return the dish as the specified type
*
@ -306,8 +382,11 @@ class WorkerWaiter {
*/
getDishAs(dish, type, callback) {
const id = this.callbackID++;
const workerId = this.addChefWorker();
if (workerId === -1) return;
this.callbacks[id] = callback;
this.chefWorkers[0].worker.postMessage({
this.chefWorkers[workerId].worker.postMessage({
action: "getDishAs",
data: {
dish: dish,
@ -317,15 +396,12 @@ class WorkerWaiter {
});
}
/**
* Sets the console log level in the worker.
* Sets the console log level in the workers.
*
* @param {string} level
*/
setLogLevel(level) {
if (!this.chefWorkers || !this.chefWorkers.length > 0) return;
for (let i = 0; i < this.chefWorkers.length; i++) {
this.chefWorkers[i].worker.postMessage({
action: "setLogLevel",
@ -333,7 +409,46 @@ class WorkerWaiter {
});
}
}
/**
* Display the bake progress in the output bar and bake button
*/
displayProgress() {
const progress = this.getBakeProgress();
const percentComplete = ((progress.pending + progress.baking) / progress.total) * 100;
const bakeButton = document.getElementById("bake");
if (this.app.baking) {
if (percentComplete < 100) {
document.getElementById("bake").style.background = `linear-gradient(to left, #fea79a ${percentComplete}%, #f44336 ${percentComplete}%)`;
} else {
bakeButton.style.background = "";
}
} else {
// not baking
bakeButton.style.background = "";
}
const bakeInfo = document.getElementById("bake-info");
let width = progress.total.toString().length;
width = width < 2 ? 2 : width;
const totalStr = progress.total.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
const bakedStr = progress.baked.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
const pendingStr = progress.pending.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
const bakingStr = progress.baking.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
let msg = "Total: " + totalStr;
msg += "<br>Baked: " + bakedStr;
if (progress.pending > 0) {
msg += "<br>Pending: " + pendingStr;
} else if (progress.baking > 0) {
msg += "<br>Baking: " + bakingStr;
}
bakeInfo.innerHTML = msg;
}
}
export default WorkerWaiter;

View File

@ -45,8 +45,8 @@
-moz-padding-start: 1px; /* Fixes bug in Firefox */
}
#input-tabs ul,
#output-tabs ul {
#input-tabs-wrapper #input-tabs,
#output-tabs-wrapper #output-tabs {
list-style: none;
background-color: var(--title-background-colour);
padding: 0;
@ -59,8 +59,8 @@
height: var(--tab-height);
}
#input-tabs ul li,
#output-tabs ul li {
#input-tabs li,
#output-tabs li {
display: flex;
flex-direction: row;
width: 100%;
@ -73,8 +73,8 @@
vertical-align: middle;
}
#input-tabs ul li:hover,
#output-tabs ul li:hover {
#input-tabs li:hover,
#output-tabs li:hover {
cursor: pointer;
background-color: var(--primary-background-colour);
}
@ -85,6 +85,11 @@
background-color: var(--primary-background-colour);
}
.tab-buttons {
transition: 1s all ease;
display: none;
}
.input-tab-content,
.output-tab-content {
width: 100%;
@ -142,10 +147,10 @@
#output-loader {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
height: calc(100% - var(--title-height));
margin: 0;
background-color: var(--primary-background-colour);
visibility: hidden;