From 5ec9ea7d9f8b92b48bff90eebfdf10b74ac4acce Mon Sep 17 00:00:00 2001 From: Lava <63625087+cosmixcom@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:13:37 +0000 Subject: [PATCH] Revamped to flipper Studio --- README.md | 2 +- cli/flipper.js | 322 ++++++++++++++++++++ cli/index.html | 28 ++ cli/script.js | 106 +++++++ cli/styles.css | 135 +++++++++ editor/index.html | 25 ++ editor/script.js | 277 +++++++++++++++++ editor/styles.css | 109 +++++++ projects/index.html | 76 +++++ projects/script.js | 80 +++++ projects/styles.css | 198 ++++++++++++ script.js | 48 +-- usb/index.html | 65 ++++ usb/script.js | 715 ++++++++++++++++++++++++++++++++++++++++++++ usb/styles.css | 325 ++++++++++++++++++++ 15 files changed, 2488 insertions(+), 23 deletions(-) create mode 100644 cli/flipper.js create mode 100644 cli/index.html create mode 100644 cli/script.js create mode 100644 cli/styles.css create mode 100644 editor/index.html create mode 100644 editor/script.js create mode 100644 editor/styles.css create mode 100644 projects/index.html create mode 100644 projects/script.js create mode 100644 projects/styles.css create mode 100644 usb/index.html create mode 100644 usb/script.js create mode 100644 usb/styles.css diff --git a/README.md b/README.md index a42c9d8..cd2eaf3 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Here are all the current and upcoming features along with their roadmap. | ------- | ------- | | Syntax Highlighting | ✅ | | Integrated AI | ✅ | -| Documentation | - (1-2 Weeks) | +| Documentation | ❎ (1-2 Weeks) | | Export to TXT | ✅ | | AI Model Choice | ❎ (1-2 Weeks) | diff --git a/cli/flipper.js b/cli/flipper.js new file mode 100644 index 0000000..05768d3 --- /dev/null +++ b/cli/flipper.js @@ -0,0 +1,322 @@ +let port; +let reader; +let inputDone; +let outputDone; +let outputStream; +let previousCommands = [] +let historyIndex = -1; + +// document.getElementById('terminal').addEventListener('submit', async (e) => { +// e.preventDefault(); + +// const commandInput = document.getElementById('commandInput'); +// const command = commandInput.value + '\r\n'; + +// // Append command to the output +// const output = document.getElementById('output'); +// output.textContent += `\n$ ${commandInput.value}`; + +// try { +// const writer = outputStream.getWriter(); +// await writer.write(command); +// writer.releaseLock(); +// } catch (error) { +// console.error('Error sending command:', error); +// } + +// commandInput.value = ''; // Clear the input field after sending + +// // Scroll the output to the bottom to show the latest command +// output.scrollTop = output.scrollHeight; + +// // Move input to the end +// const terminalBody = document.querySelector('.terminal-body'); +// terminalBody.appendChild(document.getElementById('terminal')); +// }); + +async function readLoop() { + while (true) { + const { value, done } = await reader.read(); + if (done) { + reader.releaseLock(); + break; + } + + const output = document.getElementById('output'); + output.innerHTML += `${value}`; + + // Remove the bell character + output.innerHTML = output.innerHTML.replace(/\u0007/g, ''); + + // Move cursor to the end of contenteditable div + const range = document.createRange(); + const selection = window.getSelection(); + range.selectNodeContents(output); + range.collapse(false); // Collapse the range to the end of the content + selection.removeAllRanges(); + selection.addRange(range); + output.focus(); + + // Scroll to the bottom using scrollIntoView + setTimeout(function() { + output.lastElementChild.scrollIntoView({ behavior: 'smooth' }); + }, 10) + } +} + +document.getElementById('connectButton').addEventListener('click', async () => { + try { + const savedPortInfo = JSON.parse(localStorage.getItem('serialPortInfo')); + // Request access to the serial port + if (savedPortInfo) { + // Get all available serial ports + const ports = await navigator.serial.getPorts(); + + // Try to find the saved port based on vendorId and productId + port = ports.find(p => { + const info = p.getInfo(); + return info.vendorId === savedPortInfo.vendorId && info.productId === savedPortInfo.productId; + }); + + if (port) { + // Try to open the saved port + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + console.log('Reconnected to the saved port'); + + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + } else { + console.log('Saved port not found, requesting a new port.'); + } + } + + // If no valid port was found, prompt the user to select a new port + if (!port) { + port = await navigator.serial.requestPort(); + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + + // Save the selected port's information to localStorage + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + console.log('New port selected and saved'); + } + + document.getElementById('connectionStatus').textContent = 'Connected'; + + const textEncoder = new TextEncoderStream(); + outputDone = textEncoder.readable.pipeTo(port.writable); + outputStream = textEncoder.writable; + + const textDecoder = new TextDecoderStream(); + inputDone = port.readable.pipeTo(textDecoder.writable); + reader = textDecoder.readable.getReader(); + + readLoop(); + } catch (error) { + console.error('There was an error opening the serial port:', error); + document.getElementById('connectionStatus').textContent = 'Connection Failed'; + document.getElementById('output').innerHTML += `${error}`; + } +}); + + +window.addEventListener('unload', async () => { + if (reader) { + reader.cancel(); + await inputDone.catch(() => { }); + reader = null; + } + if (outputStream) { + outputStream.getWriter().close(); + await outputDone; + } + if (port) { + await port.close(); + } +}); + +const output = document.getElementById('output'); + +// Function to prevent removal of specific spans +function preventDeletion(event) { + const target = event.target; + // console.log(event) + if (target && target.lastChild && target.lastChild.contentEditable === 'false') { + // console.log('attempted removal' + event.inputType) + // Prevent removal if the target is a protected span + if (event.inputType === 'deleteContentBackward' || event.inputType === 'deleteContentForward') { + event.preventDefault(); + // alert('This content cannot be removed.'); + } + } +} + +// Attach event listener +output.addEventListener('beforeinput', preventDeletion); + +// Listen for Enter key +output.addEventListener('keydown', async (event) => { + + + if (event.ctrlKey && event.key === 'Enter') { + event.preventDefault(); // Prevent default action (new line) + + document.getElementById('output').innerHTML += '\n'; + + placeCaretAtEnd(document.getElementById('output')) + + setTimeout(function() { + output.lastElementChild.scrollIntoView({ behavior: 'smooth' }); + }, 10) + return + } + + + if (event.ctrlKey && event.key === 'c') { + // Prevent the default action (e.g., copying text) + event.preventDefault(); + + const writer = outputStream.getWriter(); + await writer.write('\x03'); + writer.releaseLock(); + + placeCaretAtEnd(document.getElementById('output')) + + setTimeout(function() { + output.lastElementChild.scrollIntoView({ behavior: 'smooth' }); + }, 10) + // You can add any additional actions you want to take when Ctrl+C is pressed here + } + + if (event.key === 'Enter') { + event.preventDefault(); // Prevent the default action (new line) + const output = document.getElementById('output'); + + const textNodes = Array.from(output.childNodes).filter(node => node.nodeType === Node.TEXT_NODE); + const textContent = textNodes.map(node => node.nodeValue).join(''); + // console.log('Text not inside span:', textContent); + + previousCommands.push(textContent); + + const command = textContent + '\r\n'; + + textNodes[0].remove() + try { + const writer = outputStream.getWriter(); + await writer.write(command); + writer.releaseLock(); + } catch (error) { + console.error('Error sending command:', error); + } + } +}); + +let savedCommand = ''; +let currentIndex = -1; + +document.addEventListener('keydown', (event) => { + if (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'ArrowLeft' || event.key === 'ArrowRight') { + event.preventDefault(); + // let currentIndex = previousCommands.length - 1; + return + const outputTextarea = document.getElementById('output'); + + if (event.key === 'ArrowUp') { + console.log(currentIndex) + if (currentIndex >= -1) { + if (savedCommand !== '') { + outputTextarea.innerHTML = String(outputTextarea.innerHTML).replaceAll(savedCommand, previousCommands[currentIndex]); + + savedCommand = previousCommands[currentIndex]; + currentIndex = Math.max(0, currentIndex - 1); + } else { + outputTextarea.textContent += previousCommands[currentIndex]; + + savedCommand = previousCommands[currentIndex]; + console.log(previousCommands[currentIndex]) + currentIndex = Math.max(0, currentIndex - 1); + } + + placeCaretAtEnd(outputTextarea) + } + } else if (event.key === 'ArrowDown') { + if (currentIndex < previousCommands.length - 1) { + currentIndex = Math.min(previousCommands.length - 1, currentIndex + 1); + outputTextarea.value = previousCommands[currentIndex]; + } else { + outputTextarea.value = ''; + } + } + + // Append the command to the existing value + outputTextarea.value += `\n$ {outputTextarea.value}`; + } +}); + +// Reference to the contenteditable div +// const output = document.getElementById('output'); +function placeCaretAtEnd(el) { + const range = document.createRange(); + const sel = window.getSelection(); + range.selectNodeContents(el); + range.collapse(false); // Collapse the range to the end of the content + sel.removeAllRanges(); + sel.addRange(range); + el.focus(); +} + +// Listen for page load +window.addEventListener('load', async () => { + try { + const savedPortInfo = JSON.parse(localStorage.getItem('serialPortInfo')); + // Request access to the serial port + if (savedPortInfo) { + // Get all available serial ports + const ports = await navigator.serial.getPorts(); + + // Try to find the saved port based on vendorId and productId + port = ports.find(p => { + const info = p.getInfo(); + return info.vendorId === savedPortInfo.vendorId && info.productId === savedPortInfo.productId; + }); + + if (port) { + // Try to open the saved port + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + console.log('Reconnected to the saved port'); + + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + } else { + console.log('Saved port not found, requesting a new port.'); + } + } + + // If no valid port was found, prompt the user to select a new port + if (!port) { + port = await navigator.serial.requestPort(); + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + + // Save the selected port's information to localStorage + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + console.log('New port selected and saved'); + } + + document.getElementById('connectionStatus').textContent = 'Connected'; + + const textEncoder = new TextEncoderStream(); + outputDone = textEncoder.readable.pipeTo(port.writable); + outputStream = textEncoder.writable; + + const textDecoder = new TextDecoderStream(); + inputDone = port.readable.pipeTo(textDecoder.writable); + reader = textDecoder.readable.getReader(); + + readLoop(); + + document.getElementById('connectButton').remove() + } catch (error) { + document.getElementById('output').innerHTML += `${error}`; + } +}); \ No newline at end of file diff --git a/cli/index.html b/cli/index.html new file mode 100644 index 0000000..2f8004e --- /dev/null +++ b/cli/index.html @@ -0,0 +1,28 @@ + + + + + + Flipper Studio CLI + + + +
+
+
Flipper Studio CLI
+
+
Not Connected
+ +
+
+
+
+ +
+
+ + + + diff --git a/cli/script.js b/cli/script.js new file mode 100644 index 0000000..6ce0774 --- /dev/null +++ b/cli/script.js @@ -0,0 +1,106 @@ +const connectButton = document.getElementById('connect'); +const appListDiv = document.getElementById('app-list'); + +let port; + +connectButton.addEventListener('click', async () => { + try { + // Request a port and open a connection + port = await navigator.serial.requestPort(); + await port.open({ baudRate: 230400 }); + + // Create a stream reader to read data from the serial port + const decoder = new TextDecoderStream(); + const inputDone = port.readable.pipeTo(decoder.writable); + const inputStream = decoder.readable.getReader(); + + // Command to list apps in the apps folder + const command = "device_info"; + const encoder = new TextEncoder(); + const writer = port.writable.getWriter(); + await writer.write(encoder.encode(command)); + writer.releaseLock(); + + let appList = ''; + + // Read the data from the serial port + while (true) { + const { value, done } = await inputStream.read(); + if (done) break; + appList += value; + + // Optionally display data as it's received + displayData(value); + } + + inputStream.releaseLock(); + await inputDone; + + // Display the complete app list once finished + displayApps(appList); + + // await port.close(); + } catch (error) { + console.error('Error:', error); + } +}); + +function displayData(data) { + const div = document.createElement('div'); + div.className = 'app-item'; + div.textContent = data; + appListDiv.appendChild(div); +} + +function displayApps(apps) { + const appItems = apps.split('\n').filter(app => app.trim() !== ''); + appListDiv.innerHTML = ''; + appItems.forEach(app => { + const div = document.createElement('div'); + div.className = 'app-item'; + div.textContent = app; + appListDiv.appendChild(div); + }); +} + +async function runSerialCommand(command) { + try { + // Request a serial port + const port = await navigator.serial.requestPort(); + // Open the port with a specific baud rate + await port.open({ baudRate: 9600 }); + + // Set up the reader to read incoming data from the serial port + const decoder = new TextDecoderStream(); + const readableStreamClosed = port.readable.pipeTo(decoder.writable); + const inputStream = decoder.readable.getReader(); + + // Send the command to the device + const encoder = new TextEncoder(); + const writer = port.writable.getWriter(); + await writer.write(encoder.encode(command + "\n")); + writer.releaseLock(); + + let output = ''; + + // Read the data from the serial port and log it to the console + while (true) { + const { value, done } = await inputStream.read(); + if (done) break; + output += value; + } + + // Close the input stream and the port + inputStream.releaseLock(); + await readableStreamClosed; + await port.close(); + + // Log the output to the console + console.log('Command Output:', output); + } catch (error) { + console.error('Error:', error); + } +} + +// Example usage: run a command and display the output +// runSerialCommand("ls /apps"); diff --git a/cli/styles.css b/cli/styles.css new file mode 100644 index 0000000..3537a37 --- /dev/null +++ b/cli/styles.css @@ -0,0 +1,135 @@ +body { + margin: 0; + padding: 0; + font-family: 'Courier New', Courier, monospace; + background-color: #121212; + color: #ffffff; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.terminal { + width: 100%; + /* max-width: 800px; */ + background-color: #121212; + /* border-radius: 8px; */ + box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + overflow: hidden; + height: 100%; +} + +.terminal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + background-color: #3a3a3a; + border-bottom: 2px solid #4d4d4d; +} + +.terminal-title { + font-size: 1.2em; +} + +.button { + background-color: #007acc; + color: #ffffff; + border: none; + padding: 8px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 1em; + transition: background-color 0.3s ease; +} + +.button:hover { + background-color: #005f9c; +} + +.status { + font-size: 0.9em; + color: #aaaaaa; +} + +.terminal-body { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 0; + position: relative; /* Make sure the body is positioned relative */ + overflow-y: auto; /* Add scrollbar when content overflows */; +} + +.output { + background-color: #121212; + color: #00ff00; + padding: 10px; + border-radius: 4px; + /* overflow-x: auto; + overflow-y: scroll; */ + /* overflow: hidden; */ + /* height: 300px; */ + /* margin-bottom: 10px; */ + margin: 0; + white-space: pre-wrap; + /* height: 100%; */ + + caret-color: #ffffff; + /* caret-width: 2px; */ +} + +.output:focus { + outline: none; +} + +.terminal-input { + display: flex; + align-items: center; + background: none; + border: none; + outline: none; + position: absolute; + bottom: 10px; + left: 10px; +} + +input { + /* flex-grow: 1; + background-color: #3a3a3a; + color: #ffffff; + border: 2px solid #4d4d4d; + border-radius: 4px; + padding: 8px; + font-size: 1em; + resize: none; + outline: none; */ + outline: none; + border: none; + background:none; + color: #00ff00; +} + +input::placeholder { + color: #aaaaaa; +} + +input:focus { + border-color: #007acc; +} + +.terminal-input .button { + margin-left: 10px; +} + +.terminal-buttons { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + /* padding: 10px; */ + gap: 20px; +} \ No newline at end of file diff --git a/editor/index.html b/editor/index.html new file mode 100644 index 0000000..3b08b7e --- /dev/null +++ b/editor/index.html @@ -0,0 +1,25 @@ + + + + + + Simple IDE + + + + + +
+ +
+
+
+
+ + + + diff --git a/editor/script.js b/editor/script.js new file mode 100644 index 0000000..c760168 --- /dev/null +++ b/editor/script.js @@ -0,0 +1,277 @@ +// script.js +require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.30.1/min/vs' } }); +require(['vs/editor/editor.main'], function() { + let editor = monaco.editor.create(document.getElementById('editor'), { + value: '', + language: 'c', + theme: 'vs-dark' // Set dark theme + }); + + let currentProjectKey = sessionStorage.getItem('activeProject'); + let currentFileName = 'main.c'; // Default to main.c + + // Load project from localStorage + loadProject(); + + // Event listener to update the editor content when a file is selected + document.getElementById('fileList').addEventListener('click', function(e) { + if (e.target.tagName === 'LI') { + let fileName = e.target.innerHTML.split(' f.name === fileName); + editor.setValue(file ? file.code : ''); + } + }); + + // Save the editor content to localStorage whenever the content changes + editor.getModel().onDidChangeContent(function() { + if (currentProjectKey && currentFileName) { + let project = JSON.parse(localStorage.getItem(currentProjectKey)); + let file = project.files.find(f => f.name === currentFileName); + if (file) { + file.code = editor.getValue(); + } else { + // If file doesn't exist, create a new one + project.files.push({ name: currentFileName, code: editor.getValue() }); + } + localStorage.setItem(currentProjectKey, JSON.stringify(project)); + } + }); + + // Load project from localStorage and initialize the editor and file list + function loadProject() { + if (currentProjectKey) { + let project = JSON.parse(localStorage.getItem(currentProjectKey)); + if (project && project.files) { + if (project.files.length === 0) { + // No files, create default files + project.files.push({ name: 'main.c', code: '//Design something great', type: 'file', }, { name: 'application.fam', code: `App( + appid="${String(currentProjectKey)}", + name="${project.name}", + apptype=FlipperAppType.EXTERNAL, + entry_point="main", + requires=["gui"], + stack_size=4 * 1024, + order=100, + fap_icon="icon.png", + fap_category="Tools", + fap_icon_assets="assets", + fap_author="flipperstudio.", + fap_weburl="https://cosmixcom.github.io/flipper-studio/", + fap_description="Flipper Studio is a free and open-source application development platform for creating apps and writing scripts for the Flipper Zero." + fap_version="1.0", +)`, type: 'file', }); + + project.files.push(); + + localStorage.setItem(currentProjectKey, JSON.stringify(project)); + } + project.files.forEach(file => { + addFileToList(file.name, false); + }); + // Load the default file + editor.setValue(project.files.find(f => f.name === currentFileName)?.code || ''); + } else { + console.error('Invalid project format or no project found.'); + } + } else { + console.error('No active project in sessionStorage.'); + } + } + + // Add file to the list in the sidebar + function addFileToList(fileName, created) { + let fileList = document.getElementById('fileList'); + let li = document.createElement('li'); + li.innerHTML = `${fileName} delete`; + fileList.appendChild(li); + + if (created) { + li.click(); + } + } + + // Handle file creation + document.getElementById('createFile').addEventListener('click', function() { + const filename = document.getElementById('fileName').value.trim(); + + if (filename) { + let project = JSON.parse(localStorage.getItem(currentProjectKey)); + if (project.files.find(f => f.name === filename)) { + alert('File already exists.'); + return; + } + addFileToList(filename, true); + project.files.push({ name: filename, code: '' }); + localStorage.setItem(currentProjectKey, JSON.stringify(project)); + document.getElementById('fileName').value = ''; + } + }); + + // Handle file removal + window.removeFile = function(fileName) { + let project = JSON.parse(localStorage.getItem(currentProjectKey)); + project.files = project.files.filter(f => f.name !== fileName); + localStorage.setItem(currentProjectKey, JSON.stringify(project)); + + console.log(fileName) + + // Remove from the list and reset editor + let fileList = document.getElementById('fileList'); + Array.from(fileList.children).forEach(li => { + if (li.textContent.includes(fileName)) { + fileList.removeChild(li); + } + }); + + // if (currentFileName === fileName) { + // // Load a default or another file if the current file is removed + // let newFile = project.files[0]; + // currentFileName = newFile ? newFile.name : 'main.c'; + // editor.setValue(newFile ? newFile.code : ''); + // } + } +}); + +let port; +let reader; +let inputDone; +let outputDone; +let outputStream; +let connected = false; + +window.addEventListener('load', async () => { + const savedPortInfo = JSON.parse(localStorage.getItem('serialPortInfo')); + // Request access to the serial port + if (savedPortInfo) { + // Get all available serial ports + const ports = await navigator.serial.getPorts(); + + // Try to find the saved port based on vendorId and productId + port = ports.find(p => { + const info = p.getInfo(); + return info.vendorId === savedPortInfo.vendorId && info.productId === savedPortInfo.productId; + }); + + if (port) { + // Try to open the saved port + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + console.log('Reconnected to the saved port'); + + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + + connected = true; + } else { + console.log('Saved port not found, requesting a new port.'); + } + } + + // If no valid port was found, prompt the user to select a new port + if (!port) { + port = await navigator.serial.requestPort(); + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + + // Save the selected port's information to localStorage + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + console.log('New port selected and saved'); + } +}) + +const buildFap = app => new Promise((resolve, reject) => { + fetch(`https://corsproxy.io/?https://flipc.org/api/v2/${app.path}?branch=${app.branch}&nowerr=1`, { + "headers": { + "accept": "application/json" + }, + "method": "GET", + }).then(res => res.json()).then(res => resolve(res.app.id + ".fap")).catch(reject); +}); + +const getFap = app => new Promise((resolve, reject) => { + fetch(`https://corsproxy.io/?https://flipc.org/api/v2/${app.path}/elf?branch=${app.branch}&nowerr=1`, { + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + "method": "GET" + }).then(res => res.arrayBuffer()).then(resolve).catch(reject); +}); + +const sleep = time => new Promise(resolve => setTimeout(resolve, time)); + +const install = async app => { + // Setup text encoder for writing to the port + const textEncoder = new TextEncoderStream(); + outputDone = textEncoder.readable.pipeTo(port.writable); + outputStream = textEncoder.writable; + + // Setup text decoder for reading from the port + const textDecoder = new TextDecoderStream(); + inputDone = port.readable.pipeTo(textDecoder.writable); + reader = textDecoder.readable.getReader(); + + // Create a writer to send commands to the serial device + const writer = outputStream.getWriter(); + + const writeText = data => { + writer.write(textEncoder.encode(data)); + } + + if(port == null) return alert("The Flipper Zero is not connected!"); + state = 1; + const name = await buildFap(app); + state = 2; + const fap = new Uint8Array(await getFap(app)); + state = 3; + await writer.write(`storage mkdir /ext/apps/${app.category}\r\n"`); + await sleep(500); + await writer.write(`storage remove /ext/apps/${app.category}/${name}\r\n"`); + await sleep(500); + writeText(`storage write_chunk /ext/apps/${app.category}/${name} ${fap.byteLength}\r`); + await sleep(500); + await writer.write(fap); + state = 0; + return fap.byteLength; +} + +document.getElementById('install').addEventListener('click', async (e) => { + if (connected == false) { + const savedPortInfo = JSON.parse(localStorage.getItem('serialPortInfo')); + // Request access to the serial port + if (savedPortInfo) { + // Get all available serial ports + const ports = await navigator.serial.getPorts(); + + // Try to find the saved port based on vendorId and productId + port = ports.find(p => { + const info = p.getInfo(); + return info.vendorId === savedPortInfo.vendorId && info.productId === savedPortInfo.productId; + }); + + if (port) { + // Try to open the saved port + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + console.log('Reconnected to the saved port'); + + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + } else { + console.log('Saved port not found, requesting a new port.'); + } + } + + // If no valid port was found, prompt the user to select a new port + if (!port) { + port = await navigator.serial.requestPort(); + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + + // Save the selected port's information to localStorage + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + console.log('New port selected and saved'); + } + } + + +}) \ No newline at end of file diff --git a/editor/styles.css b/editor/styles.css new file mode 100644 index 0000000..79dbc88 --- /dev/null +++ b/editor/styles.css @@ -0,0 +1,109 @@ +/* styles.css */ +body { + margin: 0; + font-family: Arial, sans-serif; + background-color: #1e1e1e; + color: #e0e0e0; +} + +.container { + display: flex; + height: 100vh; +} + +.sidebar { + width: 250px; + background-color: #1e1e1e; + padding: 10px; + box-shadow: 2px 0 5px #333; + overflow-y: auto; + color: #e0e0e0; +} + +.sidebar input { + width: calc(100% - 22px); + padding: 10px; + margin-bottom: 10px; + background-color: #555; + border: 1px solid #444; + color: #fff; + border-radius: 4px; +} + +.sidebar input::placeholder { + color: #e0e0e0; +} + +.sidebar input:focus { + border-color: #1a73e8; + outline: none; + box-shadow: 0 0 3px #1a73e8; +} + +.sidebar button { + width: 100%; + padding: 10px; + background-color: #1a73e8; + color: white; + border: none; + cursor: pointer; + border-radius: 4px; +} + +.sidebar button:hover { + background-color: #135ab3; +} + +.sidebar ul { + list-style: none; + padding: 0; +} + +.sidebar li { + padding: 10px; + cursor: pointer; + border-bottom: 1px solid #555; + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row; +} + +.sidebar li:hover { + background-color: #444; +} + +.editor { + flex: 1; + background-color: #222; + color: #e0e0e0; +} + +.nested { + display: none; + padding-left: 20px; +} + +.active { + display: block; +} + +.file-manager { + margin-bottom: 20px; +} + +.file-manager div { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.file-manager input { + padding: 5px; + margin-right: 10px; +} + +.file-manager button { + padding: 5px 10px; + cursor: pointer; +} \ No newline at end of file diff --git a/projects/index.html b/projects/index.html new file mode 100644 index 0000000..f1c07f3 --- /dev/null +++ b/projects/index.html @@ -0,0 +1,76 @@ + + + + + + Flipper Studio Projects + + + + + +
+
+

Flipper Studio Projects

+ + +
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/projects/script.js b/projects/script.js new file mode 100644 index 0000000..5b01189 --- /dev/null +++ b/projects/script.js @@ -0,0 +1,80 @@ +document.addEventListener('DOMContentLoaded', () => { + const projectList = document.getElementById('projectList'); + const newProjectButton = document.getElementById('newProjectButton'); + const popup = document.getElementById('popup'); + const cancelButton = document.getElementById('cancelButton'); + const projectForm = document.getElementById('projectForm'); + + function loadProjects() { + // projectList.innerHTML = ''; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith('project-')) { + const project = JSON.parse(localStorage.getItem(key)); + const li = document.createElement('li'); + li.textContent = project.name; + li.addEventListener('click', () => { + sessionStorage.setItem('activeProject', key); + if (project.type === 'Bad USB') { + window.location.href = '/usb/'; + } else if (project.type === 'Native App') { + window.location.href = '/editor/'; + } + }); + projectList.appendChild(li); + } + } + } + + newProjectButton.addEventListener('click', () => { + popup.classList.remove('hidden'); + }); + + cancelButton.addEventListener('click', () => { + popup.classList.add('hidden'); + projectForm.reset(); + }); + + projectForm.addEventListener('submit', (e) => { + e.preventDefault(); + const projectName = document.getElementById('projectName').value; + const projectDescription = document.getElementById('projectDescription').value; + const projectAuthor = document.getElementById('projectAuthor').value; + const projectVersion = document.getElementById('projectVersion').value; + const projectType = document.getElementById('projectType').value; + + const project = { + name: projectName, + description: projectDescription, + author: projectAuthor, + version: projectVersion, + type: projectType, + code: 'REM Create Something Amazing With Flipper Studio', + files: [], + }; + + const projectId = `project-${Date.now()}`; + localStorage.setItem(projectId, JSON.stringify(project)); + + loadProjects(); + popup.classList.add('hidden'); + projectForm.reset(); + }); + + loadProjects(); + + + const projectTypeInput = document.getElementById('projectType'); + const projectVersionLabel = document.querySelector('label[for="projectVersion"]'); + const projectVersionInput = document.getElementById('projectVersion'); + + projectTypeInput.addEventListener('change', () => { + if (projectTypeInput.value === 'Native App') { + projectVersionLabel.classList.add('hidden'); + projectVersionInput.classList.add('hidden'); + } else if (projectTypeInput.value === 'Bad USB') { + projectVersionLabel.classList.remove('hidden'); + projectVersionInput.classList.remove('hidden'); + } + }); +}); diff --git a/projects/styles.css b/projects/styles.css new file mode 100644 index 0000000..253b816 --- /dev/null +++ b/projects/styles.css @@ -0,0 +1,198 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); + +/* General Styles */ +/* General Styles */ +body { + font-family: "Inter", system-ui; + background-color: #121212; + color: #e0e0e0; + margin: 0; + padding: 20px; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + overflow: hidden; +} + +.container { + text-align: center; + width: 100%; + max-width: 800px; +} + +#projectList { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + grid-template-columns: repeat(3, minmax(150px, 1fr)); + gap: 20px; + padding: 0; + list-style: none; + margin-top: 20px; + overflow-y: auto; + max-height: 450px; + margin-bottom: 50px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 6px 20px rgb(26, 26, 26, 0.19); +} + +#projectList li { + background: #1e1e1e; + padding: 20px; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + height: 150px; + text-align: center; + border: 2px solid transparent; + transition: border-color 0.3s; + border-color: #007bff; +} + +#projectList li:hover { + border-color: #2f6cac; +} + +#newProjectButton { + padding: 10px 20px; + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; +} + +#newProjectButton:hover { + background-color: #0056b3; +} + +/* Popup Styles */ +.hidden { + display: none !important; +} + +.popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; +} + +.popup-content { + background: #1e1e1e; + padding: 20px; + width: 80%; + max-width: 70%; + height: 60%; + border-radius: 20px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; +} + +form { + display: flex; + flex-direction: column; + gap: 10px; + flex-grow: 1; +} + +input, textarea, select { + padding: 10px; + border: 1px solid #444; + border-radius: 5px; + background-color: #2c2c2c; + color: #e0e0e0; + /* width: 100%; */ +} + +button[type="submit"], #cancelButton { + padding: 10px; + border: none; + border-radius: 5px; + cursor: pointer; + margin-top: 20px; + max-width: 130px; + align-self: right; + font-size: 16px; +} + +button[type="submit"] { + background-color: #28a745; + color: white; +} + +button[type="submit"]:hover { + background-color: #218838; +} + +.header { + max-height: 20px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +#cancelButton { + background-color: transparent; + color: white; + /* position: absolute; + top: 10px; + right: 10px; */ + font-size: 24px; + border: none; + cursor: pointer; +} + +#cancelButton:hover { + color: #c82333; +} + +.taskbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.taskbar .iconsbar { + justify-content: space-around; + flex-direction: row; + display: flex; + gap: 20px; + align-items: center; +} + +/* Apply custom scrollbar for all elements */ +* { + scrollbar-width: thin; /* For Firefox */ + scrollbar-color: #007bff #121212; /* For Firefox */ +} + +/* Custom Scrollbar for Webkit browsers (Chrome, Safari, etc.) */ +*::-webkit-scrollbar { + width: 10px; /* Width of the scrollbar */ + margin-left: 10px; + border-radius: 20px; +} + +/* Track of the scrollbar */ +*::-webkit-scrollbar-track { + background-color: #121212; /* Dark background */ + border-radius: 10px; /* Rounded track */ +} + +/* Thumb of the scrollbar (the part you drag) */ +*::-webkit-scrollbar-thumb { + background-color: #007bff; /* Scrollbar color */ + border-radius: 20px; /* Pill shape */ + border: 2px solid #242424; /* Creates spacing from the track */ +} diff --git a/script.js b/script.js index d0cc5b3..226fb46 100644 --- a/script.js +++ b/script.js @@ -1,25 +1,29 @@ -function runFunctionForProjectKeys() { - for (var i = 0; i < localStorage.length; i++) { - var key = localStorage.key(i); - if (key.startsWith("project-")) { - // Run your function here for keys starting with "project-" - // For example: - console.log(String(key).split('project-')[1]) - const project = String(key).split('project-')[1] - // yourFunction(localStorage.getItem(key)); - console.log("Key:", key); - const div = document.createElement('div') - div.innerHTML = `` +// function runFunctionForProjectKeys() { +// for (var i = 0; i < localStorage.length; i++) { +// var key = localStorage.key(i); +// if (key.startsWith("project-")) { +// // Run your function here for keys starting with "project-" +// // For example: +// console.log(String(key).split('project-')[1]) +// const project = String(key).split('project-')[1] +// // yourFunction(localStorage.getItem(key)); +// console.log("Key:", key); +// const div = document.createElement('div') +// div.innerHTML = `` - document.body.append(div) +// document.body.append(div) - div.addEventListener('click', () => { - window.location = `/studio/?project=${project}` - }) - // Execute your function here - } - } -} +// div.addEventListener('click', () => { +// window.location = `/studio/?project=${project}` +// }) +// // Execute your function here +// } +// } +// } -// Call the function to run it -runFunctionForProjectKeys(); \ No newline at end of file +// // Call the function to run it +// runFunctionForProjectKeys(); + +window.location = '/projects/' + +document.body.innerHTML = `Redirecting you to /projects/...` \ No newline at end of file diff --git a/usb/index.html b/usb/index.html new file mode 100644 index 0000000..52957e2 --- /dev/null +++ b/usb/index.html @@ -0,0 +1,65 @@ + + + + + + Ducky Script IDE + + + +
+

Flipper Studio

+ +
+ +
+ + +
+
+ + + + + + + + + + diff --git a/usb/script.js b/usb/script.js new file mode 100644 index 0000000..59d4c8b --- /dev/null +++ b/usb/script.js @@ -0,0 +1,715 @@ +const keywords = ['DEFAULT_DELAY', 'DEFAULTDELAY', 'DELAY', 'STRING', 'WINDOWS', 'GUI', 'APP', 'MENU', 'SHIFT', 'ALT', 'CTRL', 'CONTROL', 'DOWNARROW', 'DOWN', 'LEFTARROW', 'LEFT', 'RIGHTARROW', 'RIGHT', 'UPARROW', 'UP', 'REPEAT', 'ALTCHAR', 'ALTSTRING', 'ALTCODE', 'ENTER']; + +const commands = ['BREAK', 'PAUSE', 'CAPSLOCK', 'DELETE', 'END', 'ESC', 'ESCAPE', 'HOME', 'INSERT', 'NUMLOCK', 'PAGEDOWN', 'PAGEUP', 'PRINTSCREEN', 'SCROLLLOCK', 'SPACE', 'TAB', 'FN'] + +const editor = document.getElementById('editor'); +const suggestionsContainer = document.getElementById('suggestions'); + +// editor.addEventListener('input', () => { +// highlightSyntax(); +// // autocomplete(); +// }); + +editor.addEventListener('keydown', (event) => { + if (event.key === 'Tab') { + event.preventDefault(); + applySuggestion(); + } +}); + +function highlightSyntax(text) { + // Escape HTML to avoid injection issues + text = text.replace(/&/g, "&").replace(//g, ">"); + + // Highlight keywords + text = text.replace( + new RegExp(`\\b(${keywords.join('|')})\\b`, 'g'), + '$1' + ); + + // Highlight commands + text = text.replace( + new RegExp(`\\b(${commands.join('|')})\\b`, 'g'), + '$1' + ); + + // Highlight numbers + text = text.replace(/\b\d+\b/g, '$&'); + + // Highlight REM comments (darker gray) + text = text.replace(/REM.*/g, '$&'); + + return text; +} + +function updateHighlighting() { + let text = editor.value; + highlighting.innerHTML = highlightSyntax(text); + + + const project = JSON.parse(window.localStorage.getItem(window.sessionStorage.getItem('activeProject'))) + project.code = text + window.localStorage.setItem(window.sessionStorage.getItem('activeProject'), JSON.stringify(project)) + // console.log('Autosaved') +} + +editor.addEventListener('input', updateHighlighting); + +editor.addEventListener('scroll', syncScroll); + +function syncScroll() { + highlighting.scrollTop = editor.scrollTop; + highlighting.scrollLeft = editor.scrollLeft; +} + + +// Function to handle comment/uncomment action +function toggleComment() { + let selection = window.getSelection(); + let selectedText = selection.toString(); + let lines = editor.value.split('\n'); + let startLine = editor.value.substr(0, editor.value.indexOf(selection.anchorNode.value)).split('\n').length - 1; + let endLine = startLine + selectedText.split('\n').length - 1; + + if (selectedText.trim() === '') { + startLine = endLine = editor.value.substr(0, editor.selectionStart).split('\n').length - 1; + } + + for (let i = startLine; i <= endLine; i++) { + if (lines[i].startsWith('REM ')) { + lines[i] = lines[i].replace(/^REM\s+/, ''); + } else { + lines[i] = 'REM ' + lines[i]; + } + } + + editor.value = lines.join('\n'); + updateHighlighting(); + + // setCaretPosition(editor, caretPosition); // Restore cursor position +} + +// Event listener for Ctrl + / +document.addEventListener('keydown', function(event) { + if (event.ctrlKey && event.key === '/') { + event.preventDefault(); + toggleComment(); + } +}); + +function placeCaretAtEnd(el) { + el.focus(); + const range = document.createRange(); + range.selectNodeContents(el); + range.collapse(false); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); +} + +function autocomplete() { + const cursorPos = getCaretPosition(); + const textBeforeCursor = editor.innerText.substring(0, cursorPos); + const lastWordMatch = textBeforeCursor.match(/\b(\w*)$/); + if (lastWordMatch) { + const lastWord = lastWordMatch[1].toUpperCase(); + const suggestions = keywords.filter(keyword => keyword.startsWith(lastWord)); + + if (suggestions.length > 0) { + showSuggestions(suggestions, lastWord.length); + } else { + suggestionsContainer.style.display = 'none'; + } + } else { + suggestionsContainer.style.display = 'none'; + } +} + +function showSuggestions(suggestions, lastWordLength) { + const range = window.getSelection().getRangeAt(0); + const rect = range.getBoundingClientRect(); + const editorRect = editor.getBoundingClientRect(); + + suggestionsContainer.innerHTML = suggestions.map(suggestion => + `${suggestion}` + ).join(' '); + + suggestionsContainer.style.display = 'block'; + suggestionsContainer.style.left = `${rect.left - editorRect.left}px`; + suggestionsContainer.style.top = `${rect.bottom - editorRect.top + window.scrollY}px`; + + suggestionsContainer.querySelectorAll('.suggestion-item').forEach(item => { + item.addEventListener('click', () => { + insertSuggestion(item.dataset.suggestion, lastWordLength); + }); + }); +} + +// Function to get the current caret position in the contenteditable element +// Function to get the current caret position in the contenteditable element +function getCaretPosition(element) { + let caretOffset = 0; + let selection = window.getSelection(); + + if (selection.rangeCount > 0) { + let range = selection.getRangeAt(0); + let preCaretRange = range.cloneRange(); + + preCaretRange.selectNodeContents(element); + preCaretRange.setEnd(range.endContainer, range.endOffset); + console.log(range.endContainer.nodeType) + + caretOffset = preCaretRange.toString().length; + } + + console.log(selection.anchorOffset) + return selection.anchorOffset; +} + +editor.addEventListener('click', () => { + var selection = window.getSelection(); + var caretLocation = selection.getRangeAt(0).endOffset; + if(caretLocation === 0) + { + var range = document.createRange(); + range.selectNodeContents(this); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + } else if(caretLocation === this.innerText.length) { + var range = document.createRange(); + range.selectNodeContents(this); + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + } +}) + +// Function to set the caret position in the contenteditable element +function setCaretPosition(element, offset) { + let selection = window.getSelection(); + let range = document.createRange(); + let node = element.firstChild; + let currentOffset = 0; + + while (node) { + let nodeLength = node.textContent.length; + + if (currentOffset + nodeLength >= offset) { + if (node.nodeType === Node.TEXT_NODE) { + range.setStart(node, offset - currentOffset); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + return; + } + } else { + currentOffset += nodeLength; + } + + node = node.nextSibling; + } +} + +// Event listener for text input to highlight syntax +editor.addEventListener('input', function(event) { + // Debouncing to prevent excessive calls + // clearTimeout(editor.highlightTimeout); + // editor.highlightTimeout = setTimeout(() => { + // highlightSyntax(); + // }, 50); + // highlightSyntax(); +}); + +// Handle Enter key +// editor.addEventListener('keydown', function(event) { +// if (event.key === 'Enter') { +// // event.preventDefault(); // Prevent the default behavior + +// const caretPosition = getCaretPosition(editor); +// // document.execCommand('insertHTML', false, '
'); // Insert a new line + +// // Preserve caret position +// setCaretPosition(editor, caretPosition + 1); +// console.log(caretPosition) +// highlightSyntax(); +// } + +// // Handle Backspace key +// if (event.key === 'Backspace') { +// const caretPosition = getCaretPosition(editor); + +// if (caretPosition === 0) return; // If caret is at the start, do nothing + +// highlightSyntax(); +// setCaretPosition(editor, caretPosition - 1); // Adjust the caret after backspace +// } +// }); + +document.getElementById('download').addEventListener('click', () => { + const text = `REM Made In Flipper Studio https://github.com/cosmixcom/Flipper-Studio +REM Project Name: ${JSON.parse(window.localStorage.getItem(window.sessionStorage.getItem('activeProject'))).name} +REM Project Description: ${JSON.parse(window.localStorage.getItem(window.sessionStorage.getItem('activeProject'))).description} +REM Project Author: ${JSON.parse(window.localStorage.getItem(window.sessionStorage.getItem('activeProject'))).author} + +` + editor.value + const blob = new Blob([text], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `flipperstudio-${JSON.parse(window.localStorage.getItem(window.sessionStorage.getItem('activeProject'))).name}-${Math.random().toString(36).substring(7)}.txt`;; + a.click(); + URL.revokeObjectURL(url); + +}) + +const aiBot = `You are a bot to help with the coding language rubber ducky, here are the docs below. JUST PROVIDE PLAIN CODE WHEN ANSWERING NO MARKDOWN OR MESSAGES! They have been translated from markdown to plain text: + +Ducky Script +Ducky Script is the language of the USB Rubber Ducky. Writing scripts for can be done from any common ascii text editor such as Notepad, vi, emacs, nano, gedit, kedit, TextEdit, etc. + +Syntax +Ducky Script syntax is simple. Each command resides on a new line and may have options follow. Commands are written in ALL CAPS, because ducks are loud and like to quack with pride. Most commands invoke keystrokes, key-combos or strings of text, while some offer delays or pauses. Below is a list of commands and their function, followed by some example usage. + +Note: In the tables below //n// represents a number and //Char// represents characters A-Z, a-z. + +REM +Similar to the REM command in Basic and other languages, lines beginning with REM will not be processed. REM is a comment. + +Command +Rem +REM The next three lines execute a command prompt in Windows +GUI r +STRING cmd +ENTER +DEFAULT_DELAY or DEFAULTDELAY +DEFAULT_DELAY or DEFAULTDELAY is used to define how long (milliseconds) to wait between each subsequent command. DEFAULT_DELAY must be issued at the beginning of the ducky script and is optional. Not specifying the DEFAULT_DELAY will result in faster execution of ducky scripts. This command is mostly useful when debugging. + +Command Parameters +DEFAULT_DELAY 0..-> +DEFAULTDELAY 0..-> +DEFAULT_DELAY 100 +REM delays 100ms between each subsequent command sequence +DELAY +DELAY creates a momentary pause in the ducky script. It is quite handy for creating a moment of pause between sequential commands that may take the target computer some time to process. DELAY time is specified in milliseconds from 1 to 10000. Multiple DELAY commands can be used to create longer delays. + +Command Parameters +DELAY 0..-> +DELAY 500 +REM will wait 500ms before continuing to the next command. +STRING +STRING processes the text following taking special care to auto-shift. STRING can accept a single or multiple characters. + +Command Parameters +STRING a...z A...Z 0..9 !...) \`~ += _- "' :; <, >. ?/ \ and pipe +GUI r +DELAY 500 +STRING notepad.exe +ENTER +DELAY 1000 +STRING Hello World! +WINDOWS or GUI +Emulates the Windows-Key, sometimes referred to as the Super-key. + +Command Optional Parameters +GUI Single Char +WINDOWS Single Char +GUI r +REM will hold the Windows-key and press r, on windows systems resulting in the Run menu. +MENU or APP +Emulates the App key, sometimes referred to as the menu key or context menu key. On Windows systems this is similar to the SHIFT F10 key combo, producing the menu similar to a right-click. + +Command +APP +MENU +GUI d +MENU +STRING v +STRING d +//Switch to desktop, pull up context menu and choose actions v, then d toggles displaying Windows desktop icons// + +SHIFT +Unlike CAPSLOCK, cruise control for cool, the SHIFT command can be used when navigating fields to select text, among other functions. + +Command Optional Parameter +SHIFT DELETE, HOME, INSERT, PAGEUP, PAGEDOWN, WINDOWS, GUI, UPARROW, DOWNARROW, LEFTARROW, RIGHTARROW, TAB +SHIFT INSERT +REM this is paste for most operating systems +ALT +Found to the left of the space key on most keyboards, the ALT key is instrumental in many automation operations. ALT is envious of CONTROL + +Command Optional Parameter +ALT END, ESC, ESCAPE, F1...F12, Single Char, SPACE, TAB +GUI r +DELAY 50 +STRING notepad.exe +ENTER +DELAY 100 +STRING Hello World +ALT f +STRING s +REM alt-f pulls up the File menu and s saves. This two keystroke combo is why ALT is jealous of CONTROL's leetness and CTRL+S +CONTROL or CTRL +The king of key-combos, CONTROL is all mighty. + +Command Optional Parameters +CONTROL BREAK, PAUSE, F1...F12, ESCAPE, ESC, Single Char +CTRL BREAK, PAUSE, F1...F12, ESCAPE, ESC, Single Char +CONTROL ESCAPE +REM this is equivalent to the GUI key in Windows +Arrow Keys +Command +DOWNARROW or DOWN +LEFTARROW or LEFT +RIGHTARROW or RIGHT +UPARROW or UP +Extended Commands +Command Notes +BREAK or PAUSE For the infamous combo CTRL BREAK +CAPSLOCK Cruise control for cool. Toggles +DELETE +END When will it ever +ESC or ESCAPE You can never +HOME There's no place like +INSERT +NUMLOCK Toggles number lock +PAGEUP +PAGEDOWN +PRINTSCREEN Typically takes screenshots +SCROLLLOCK Hasn't been nearly as useful since the GUI was invented +SPACE the final frontier +TAB not just a cola +FN another modifier key +REPEAT +Repeats the last command n times + +Command n +REPEAT number of times to repeat +DOWN +REPEAT 100 +REM The previous command is repeated 100 times (thus performed 101 times total)` + +if (!window.localStorage.getItem('key')) { + document.getElementById('ai').placeholder = 'Please enter a Cohere API key' +} + +document.getElementById('aiForm').addEventListener('submit', async (event) => { + event.preventDefault() + if (window.localStorage.getItem('key')) { + const prompt = document.getElementById('ai').value + + if (prompt) { + const body = JSON.stringify({ + message: prompt, + preamble: aiBot, + model: 'command', + temperature: 0.9, + connectors: [{ "id": "web-search" }] + }); + + document.getElementById('ai').value = '' + document.getElementById('ai').placeholder = 'Loading...' + + const response = await fetch('https://api.cohere.ai/v1/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${window.localStorage.getItem('key')}`, + }, + body, + }); + + // Check for errors + if (!response.ok) { + console.log(response) + } else { + document.getElementById('ai').placeholder = '' + } + + const finished = await response.json() + console.log(finished) + let aiResponse = finished.text + + if (aiResponse.includes('```')) { + aiResponse = aiResponse.split('```')[1] + } + + try { + aiResponse.replaceAll('#', 'REM') + aiResponse.replaceAll('//', 'REM') + aiResponse.replaceAll('!', 'REM') + } catch { } + + document.getElementById('aiForm').style.display = 'none'; + editor.value += aiResponse + + updateHighlighting() + + // document.getElementById('ai').value = aiResponse + // document.getElementById('aiForm').disabled = true; + } + } else { + window.localStorage.setItem('key', document.getElementById('ai').value) + document.getElementById('ai').value = '' + document.getElementById('aiForm').style.display = 'none'; + document.getElementById('ai').placeholder = '' + } +}) + +window.addEventListener('keydown', async function(event) { + + if (event.key === 'Escape') { + // Your code to execute on Escape + document.getElementById('aiForm').style.display = 'none'; + document.getElementById('ai').value = '' + // document.getElementById('aiForm').disabled = false; + } + if (event.ctrlKey && event.key === 'i') { + // Your code to execute on Ctrl+I + document.getElementById('ai').select() + document.getElementById('ai').focus() + document.getElementById('ai').click() + setTimeout(function() { + document.getElementById('ai').select() + document.getElementById('ai').focus() + document.getElementById('ai').click() + }, 500) + document.getElementById('aiForm').style.display = 'block'; + + + } +}); + +//LOAD PROJECT +if (window.sessionStorage.getItem('activeProject')) { + const project = JSON.parse(window.localStorage.getItem(window.sessionStorage.getItem('activeProject'))) + document.getElementById('editor').value = project.code + + updateHighlighting() +} else { + window.location = '/projects/' +} + +document.getElementById(`loadProjectForm`).addEventListener('submit', async (event) => { + event.preventDefault() + + const fileInput = document.getElementById('fileInput'); + const githubUrl = document.getElementById('githubUrl').value.trim(); + const projectContent = document.getElementById('editor'); + + // Handle file input if a file is selected + if (fileInput.files.length > 0) { + const file = fileInput.files[0]; + const reader = new FileReader(); + + reader.onload = function(e) { + projectContent.value = e.target.result; + + updateHighlighting() + document.getElementById('loadProjectPopup').style.display = 'none'; + }; + + reader.readAsText(file); + } + // Handle GitHub URL if provided + else if (githubUrl) { + fetch(githubUrl) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.text(); + }) + .then(data => { + projectContent.value = data; + + updateHighlighting() + document.getElementById('loadProjectPopup').style.display = 'none'; + }) + .catch(error => { + console.error('Error fetching the URL:', error); + alert('Failed to load content from the URL. Please check the URL and try again.'); + }); + } else { + alert('Please provide a file or a GitHub URL.'); + } +}) + +let port; +let reader; +let inputDone; +let outputDone; +let outputStream; +let connected = false; + +window.addEventListener('load', async () => { + const savedPortInfo = JSON.parse(localStorage.getItem('serialPortInfo')); + // Request access to the serial port + if (savedPortInfo) { + // Get all available serial ports + const ports = await navigator.serial.getPorts(); + + // Try to find the saved port based on vendorId and productId + port = ports.find(p => { + const info = p.getInfo(); + return info.vendorId === savedPortInfo.vendorId && info.productId === savedPortInfo.productId; + }); + + if (port) { + // Try to open the saved port + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + console.log('Reconnected to the saved port'); + + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + + connected = true; + } else { + console.log('Saved port not found, requesting a new port.'); + } + } + + // If no valid port was found, prompt the user to select a new port + if (!port) { + port = await navigator.serial.requestPort(); + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + + // Save the selected port's information to localStorage + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + console.log('New port selected and saved'); + } +}) + +document.getElementById('upload').addEventListener('click', async () => { + try { + if (connected == false) { + const savedPortInfo = JSON.parse(localStorage.getItem('serialPortInfo')); + // Request access to the serial port + if (savedPortInfo) { + // Get all available serial ports + const ports = await navigator.serial.getPorts(); + + // Try to find the saved port based on vendorId and productId + port = ports.find(p => { + const info = p.getInfo(); + return info.vendorId === savedPortInfo.vendorId && info.productId === savedPortInfo.productId; + }); + + if (port) { + // Try to open the saved port + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + console.log('Reconnected to the saved port'); + + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + } else { + console.log('Saved port not found, requesting a new port.'); + } + } + + // If no valid port was found, prompt the user to select a new port + if (!port) { + port = await navigator.serial.requestPort(); + await port.open({ baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); + + // Save the selected port's information to localStorage + const info = port.getInfo(); + localStorage.setItem('serialPortInfo', JSON.stringify(info)); + console.log('New port selected and saved'); + } + } + + // Setup text encoder for writing to the port + const textEncoder = new TextEncoderStream(); + outputDone = textEncoder.readable.pipeTo(port.writable); + outputStream = textEncoder.writable; + + // Setup text decoder for reading from the port + const textDecoder = new TextDecoderStream(); + inputDone = port.readable.pipeTo(textDecoder.writable); + reader = textDecoder.readable.getReader(); + + // Create a writer to send commands to the serial device + const writer = outputStream.getWriter(); + + const id = Math.random().toString(36).substring(7); + + var text = `REM Made In Flipper Studio https://github.com/cosmixcom/Flipper-Studio +REM Project Name: ${JSON.parse(window.localStorage.getItem(window.sessionStorage.getItem('activeProject'))).name} +REM Project Description: ${JSON.parse(window.localStorage.getItem(window.sessionStorage.getItem('activeProject'))).description} +REM Project Author: ${JSON.parse(window.localStorage.getItem(window.sessionStorage.getItem('activeProject'))).author} +REM Version: ${id} + +` + document.getElementById('editor').value + + +text = text.replace(/\r?\n|\r/g, '\n'); + + var name = `${JSON.parse(window.localStorage.getItem(window.sessionStorage.getItem('activeProject'))).name}` + + + + name = name.replace(/ /g, '-'); + + // Write the command to store text in the file + await writer.write(`storage mkdir /ext/badusb/flipperStudio\r\n`); + await writer.write(`storage remove /ext/badusb/flipperStudio/${name}.txt\r\n`); + await writer.write(`storage write /ext/badusb/flipperStudio/${name}.txt\r\n`); + await writer.write(`${text}\r\n`); + await writer.write('\x03'); + // Optionally close the writer when done + writer.releaseLock(); + + // // Optionally close the port when finished + // await port.close(); + + // Update connection status on the page + // document.getElementById('connectionStatus').textContent = 'Write Complete'; + console.log(`File Saved With Version ${id}`) + + alert(`Project saved to /ext/badusb/flipperStudio/${name}.txt`) + + readLoop(); + } catch (error) { + console.error('There was an error opening or writing to the serial port:', error); + document.getElementById('upload').textContent = 'Connection Failed'; + // document.getElementById('output').innerHTML += `${error}`; + } +}); + +window.addEventListener('unload', async () => { + if (reader) { + reader.cancel(); + await inputDone.catch(() => { }); + reader = null; + } + if (outputStream) { + outputStream.getWriter().close(); + await outputDone; + } + if (port) { + await port.close(); + } +}); + +async function readLoop() { + while (true) { + const { value, done } = await reader.read(); + if (done) { + reader.releaseLock(); + break; + } + + // const output = document.getElementById('output'); + // output.innerHTML += `${value}`; + console.log(value) + + // Remove the bell character + // output.innerHTML = output.innerHTML.replace(/\u0007/g, ''); + + // Move cursor to the end of contenteditable div + + // Scroll to the bottom using scrollIntoView + } +} \ No newline at end of file diff --git a/usb/styles.css b/usb/styles.css new file mode 100644 index 0000000..59efeac --- /dev/null +++ b/usb/styles.css @@ -0,0 +1,325 @@ +/* General styles */ +/* body { + margin: 0; + font-family: Arial, sans-serif; +} */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); + + +/* Header styles */ +header { + background-color: #121212; + color: #fff; + padding: 10px 20px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 2px solid #444; +} + +header h1 { + margin: 0; +} + +nav { + display: flex; + gap: 10px; +} + +nav button { + background: none; + color: #fff; + border: none; + padding: 8px 15px; + font-size: 16px; + cursor: pointer; + border-radius: 4px; + transition: color 0.3s; +} + +nav button:hover { + /* background-color: #777; */ + color: #d4d4d4; +} + +body { + font-family: "Inter", system-ui; + margin: 0; + padding: 0; + background-color: #121212; + color: #d4d4d4; + overflow: hidden; +} + +.editor-container { + width: 98vw; + height: 90vh; + margin: 0; + position: relative; +} + +.editor { + width: 100%; + height: 100%; + padding: 10px; + font-family: monospace; + font-size: 16px; + border: none; + border-radius: 0; + box-sizing: border-box; + background-color: #121212; + color: #d4d4d4; + outline: none; + overflow: auto; + white-space: pre-wrap; + word-wrap: break-word; + overflow-y: scroll; +} + +.keyword { + color: #ffcc00; + font-weight: bold; +} + +.suggestions { + position: absolute; + background-color: #2c2c2c; + color: #888; + display: none; + font-size: 16px; + padding: 2px 5px; + border: 1px solid #333; + border-radius: 4px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + z-index: 1; + white-space: nowrap; +} + +.suggestion-item { + display: inline; + padding: 2px 5px; + cursor: pointer; +} + +.suggestion-item:hover { + background-color: #444; + color: #ccc; +} + +.number { + color: #08c40b; +} + +.command { + color: #c886fe; +} + +#aiForm { + transition: 0.2s ease all; +} + +#ai { + position: fixed; + z-index: 999999999; + top: 50px; + left: 50%; + transform: translateX(-50%); + padding: 15px; + border-radius: 10px; + width: 50%; + font-size: 16px; + text-align: center; + background: transparent; + /* backdrop-filter: blur(80px); */ + backdrop-filter: saturate(180%) blur(20px); + -webkit-backdrop-filter: saturate(180%) blur(20px); + border: 1px solid rgba(255, 255, 255, 0.125); + color: #DEDEDE; + transition: 0.2s ease; +} + +#ai::placeholder { + color: #575757; +} + +#ai:focus { + outline: none; + border: 1px solid rgba(255, 255, 255, 0.325); +} + +.comment { + color: #666; /* Darker gray for comments */ + font-style: italic; +} + +.editor-container { + position: relative; + width: 100%; + height: 100%; + background-color: white; /* Set to the background color of your editor */ + box-sizing: border-box; +} + +#highlighting { + position: absolute; + top: 0; + left: 0; + /* height: 100%; */ + color: #d4d4d4; + padding: 8px; + white-space: pre-wrap; + overflow-wrap: break-word; + /* font-family: monospace; */ + pointer-events: none; /* Make it non-interactive */ + + font-size: 16px !important; + background-color: #121212; + width: 98vw; + height: 90vh; + caret-color: #d4d4d4; + box-sizing: border-box; + overflow-y: scroll; +} + +#editor { + position: absolute; + top: 0; + left: 0; + width: 98vw; + height: 90vh; + padding: 8px; + /* font-family: monospace; */ + background: transparent; /* Transparent background */ + /* color: black; */ + caret-color: #d4d4d4; + border: none; + outline: none; + white-space: pre-wrap; + overflow-wrap: break-word; + /* resize: none; */ + box-sizing: border-box; + /* color: #d4d4d4; */ + font-size: 16px !important; + z-index: 1; + + + color: transparent; +} + +/* Ensure consistency between the textarea and highlighting */ +#highlighting, #editor { + letter-spacing: normal; + line-height: 19px; + font-family: "Inter", system-ui; + overflow-y: scroll !important; +} + +/* Custom scrollbar styles */ +/* Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: #888 #333; +} + +/* WebKit based browsers */ +*::-webkit-scrollbar { + width: 12px; +} + +*::-webkit-scrollbar-track { + background: #333; +} + +*::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 10px; + border: 3px solid #333; +} + +*::-webkit-scrollbar-thumb:hover { + background-color: #555; +} + +.popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999999999999999999999999999999999; +} + +.popup-content { + background: #1e1e1e; + padding: 20px; + width: 80%; + max-width: 70%; + height: 60%; + border-radius: 20px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; +} + +form { + display: flex; + flex-direction: column; + gap: 10px; + flex-grow: 1; +} + +input, textarea, select { + padding: 10px; + border: 1px solid #444; + border-radius: 5px; + background-color: #2c2c2c; + color: #e0e0e0; + /* width: 100%; */ +} + +button[type="submit"], #cancelButton { + padding: 10px; + border: none; + border-radius: 5px; + cursor: pointer; + margin-top: 20px; +max-width: 130px; +align-self: right; +font-size: 16px; +} + +button[type="submit"] { + background-color: #28a745; + color: white; +} + +button[type="submit"]:hover { + background-color: #218838; +} + +#cancelButton { + background-color: transparent; + color: white; + /* position: absolute; + top: 10px; + right: 10px; */ + font-size: 24px; + border: none; + cursor: pointer; +} + +#cancelButton:hover { + color: #c82333; +} + +.header { + max-height: 20px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} \ No newline at end of file