diff --git a/src/components/editor.js b/src/components/editor.js index a6f3eed..9da3f26 100644 --- a/src/components/editor.js +++ b/src/components/editor.js @@ -81,7 +81,7 @@ class EditorTabs extends Tonic { index: this.index++ } - tab.model.onDidChangeContent((...args) => editor.changes(tab, ...args)) + tab.model.onDidChangeContent((...args) => parent.editor.changes(tab, ...args)) this.state.tabs.set(node.id, tab) parent.editor.setModel(tab.model) @@ -94,7 +94,7 @@ class EditorTabs extends Tonic { if (this.state.tabs.has(id)) { this.state.tabs.delete(id) } - + this.reRender() } @@ -265,7 +265,7 @@ class AppEditor extends Tonic { const coTabs = document.querySelector('editor-tabs') const coEditor = document.querySelector('app-editor') - if (!coTabs.tab) return + if (!coTabs.tab || coTabs.tab.isReadOnly) return const app = this.props.parent const value = this.editor.getValue() @@ -290,12 +290,16 @@ class AppEditor extends Tonic { coTabs.tab.unsaved = false coTabs.reRender() } catch (err) { - console.error(`Unable to write to ${pathToFile}`, err) + console.error(`Unable to write to ${coTabs.tab.path}`, err) } app.reloadPreviewWindows() } + async reload () { + this.loadProjectNode(this.state.projectNode) + } + async loadProjectNode (projectNode) { if (!projectNode) return @@ -313,7 +317,7 @@ class AppEditor extends Tonic { const mappings = app.state.settings.extensionLanguageMappings const lang = mappings[ext] || ext.slice(1) monaco.editor.setModelLanguage(this.editor.getModel(), lang) - let data = await fs.promises.readFile(projectNode.id, 'utf8') + let data = projectNode.value || await fs.promises.readFile(projectNode.id, 'utf8') if (path.extname(projectNode.id) === '.json') { try { @@ -342,7 +346,6 @@ class AppEditor extends Tonic { success: rgbaToHex(styles.getPropertyValue('--tonic-success').trim()) } - const userColors = this.props.parent.state.settings?.userColors ?? [] const base = `vs${theme.includes('dark') ? '-dark' : ''}` @@ -350,6 +353,11 @@ class AppEditor extends Tonic { base, inherit: true, rules: [ + { token: 'lineFile', foreground: colors.accent }, // Green for added lines + { token: 'lineHeader', foreground: colors.info }, // Green for added lines + { token: 'lineAdded', foreground: colors.success }, // Green for added lines + { token: 'lineRemoved', foreground: colors.error }, // Red for removed lines + { token: 'identifier', foreground: colors.primary }, { token: 'keyword', foreground: colors.accent }, { token: 'punctuation', foreground: colors.primary }, @@ -471,7 +479,6 @@ class AppEditor extends Tonic { connected () { let theme - const app = this.props.parent this.editor = monaco.editor.create(this.querySelector('.editor'), { value: '', @@ -486,6 +493,78 @@ class AppEditor extends Tonic { this.updateSettings() + monaco.languages.registerFoldingRangeProvider('patch', { + provideFoldingRanges: function (model, context, token) { + const hunkStartRegex = /^@@ -\d+(,\d+)? \+\d+(,\d+)? @@.*/ + const diffStartRegex = /^diff --git a\/.+ b\/.+/ + const foldingRanges = [] + let currentHunkStart = -1 + + for (let i = 0; i < model.getLineCount(); i++) { + const lineContent = model.getLineContent(i + 1) + + if (hunkStartRegex.test(lineContent)) { + if (currentHunkStart !== -1) { + foldingRanges.push({ + start: currentHunkStart, + end: i, + kind: monaco.languages.FoldingRangeKind.Region + }) + } + currentHunkStart = i + 1 + } else if (diffStartRegex.test(lineContent) && currentHunkStart !== -1) { + foldingRanges.push({ + start: currentHunkStart, + end: i, + kind: monaco.languages.FoldingRangeKind.Region + }) + currentHunkStart = -1 + } + } + + if (currentHunkStart !== -1) { + foldingRanges.push({ + start: currentHunkStart, + end: model.getLineCount(), + kind: monaco.languages.FoldingRangeKind.Region + }) + } + + return foldingRanges + } + }) + + monaco.languages.register({ id: 'patch' }) + + monaco.languages.setMonarchTokensProvider('patch', { + tokenizer: { + root: [ + [/^index \w+\.\.\w+( \d+)?/, 'lineHeader'], + [/^---.*/, 'lineHeader'], + [/^\+\+\+.*/, 'lineHeader'], + [/^@@ -\d+(,\d+)? \+\d+(,\d+)? @@.*/, 'lineHeader'], + [/^diff --git a\/.+ b\/.+/, 'lineFile'], + [/^From:.*<[^>]+>/, 'lineHeader'], + [/^Date:.+/, 'lineHeader'], + [/^Subject: \[PATCH\].+/, 'lineHeader'], + [/^From [0-9a-fA-F]+ Mon .+/, 'lineHeader'], + + [/^\+(?!\+\+).*/, 'lineAdded'], + [/^-(?!-).*/, 'lineRemoved'] + ] + } + }) + + this.editor.onDidChangeModelContent(event => { + const coTabs = document.querySelector('editor-tabs') + this.editor.updateOptions({ readOnly: false }) + + if (coTabs.tab?.label.endsWith('.patch')) { + this.editor.updateOptions({ readOnly: true }) + this.editor.getAction('editor.foldAll').run() + } + }) + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { this.refreshColors(event) }) diff --git a/src/components/patch-requests.js b/src/components/patch-requests.js index c37a898..5a0c617 100644 --- a/src/components/patch-requests.js +++ b/src/components/patch-requests.js @@ -1,24 +1,51 @@ import Tonic from '@socketsupply/tonic' -import { exec } from 'socket:child_process' class PatchRequests extends Tonic { async click (e) { const el = Tonic.match(e.target, '[data-event]') if (!el) return - const { event, value } = el.dataset const app = this.props.app + const row = el.closest('.row') + const value = row.dataset.value + + const { data: patch } = await app.db.patches.get(value) + + const { event } = el.dataset + + if (event === 'view') { + const coProjectSummary = document.querySelector('view-project-summary') + const coEditor = document.querySelector('app-editor') + const name = (patch.patchId || patch.headers.parent.slice(6)) + '.patch' + + coEditor.loadProjectNode({ + isReadOnly: true, + id: name, + label: name, + value: patch.src + }) + + coProjectSummary.hide() + } + + if (event === 'apply') { + console.log('APPLY', patch) + } + + if (event === 'trash') { + console.log('TRASH', patch) + } if (event === 'trust') { - const { data: hasKey } = await app.db.keys.has(value.headers.from) + const { data: hasKey } = await app.db.keys.has(patch.headers.from) if (!hasKey) { - await app.db.keys.put(value.headers.from, value.publicKey) + await app.db.keys.put(patch.headers.from, patch.publicKey) this.reRender() return } - await app.db.keys.del(value.headers.from) + await app.db.keys.del(patch.headers.from) this.reRender() } } @@ -28,17 +55,18 @@ class PatchRequests extends Tonic { const { data: dataPatches } = await app.db.patches.readAll() - let patches = [] + const patches = [] for (const [patchId, patch] of dataPatches.entries()) { const ts = (new Date(patch.headers.date)).getTime() let statusIcon = 'warning' let statusTitle = 'This patch is not associated with a trusted public key. Click here to trust it.' + const { data: hasKey } = await app.db.keys.has(patch.headers.from) - if (patch.publicKey) { + if (hasKey && patch.publicKey && patch.headers.from) { const { data: dataKey } = await app.db.keys.get(patch.headers.from) - const savedB64pk = Buffer.from(dataKey).toString('base64') + const savedB64pk = Buffer.from(dataKey || '').toString('base64') const thisB64pk = Buffer.from(patch.publicKey || '').toString('base64') if (dataKey && savedB64pk === thisB64pk) { @@ -51,14 +79,14 @@ class PatchRequests extends Tonic { } patches.push(this.html` -
+
${patch.headers.from} ${patch.summary.split('\n').pop()} - - - + + +
`) diff --git a/src/components/project.js b/src/components/project.js index 19e5013..d91b88a 100644 --- a/src/components/project.js +++ b/src/components/project.js @@ -502,7 +502,6 @@ class AppProject extends Tonic { async onSelection (node, isToggle) { if (!isToggle) { - const app = this.props.parent const projectNode = this.getProjectNode(node) const coImagePreview = document.querySelector('view-image-preview') const coProjectSummary = document.querySelector('view-project-summary') @@ -629,7 +628,7 @@ class AppProject extends Tonic { async load () { const app = this.props.parent - let oldState = this.state.tree + const oldState = this.state.tree let oldChild = this.getNodeByProperty('id', 'home', oldState) const tree = { diff --git a/src/components/properties.js b/src/components/properties.js index 1fd671a..3f8e7e7 100644 --- a/src/components/properties.js +++ b/src/components/properties.js @@ -1,6 +1,4 @@ import Tonic from '@socketsupply/tonic' -import fs from 'socket:fs' -import path from 'socket:path' import process from 'socket:process' import Config from '../lib/config.js' @@ -14,8 +12,6 @@ class AppProperties extends Tonic { const app = this.props.parent const notifications = document.querySelector('#notifications') - const editor = document.querySelector('app-editor') - const project = document.querySelector('app-project') const config = new Config(app.state.currentProject?.id) // @@ -37,12 +33,17 @@ class AppProperties extends Tonic { // if (event === 'property') { await config.set(section, el.id, el.value) - editor.loadProjectNode(node) + const coTabs = document.querySelector('editor-tabs') + const coEditor = document.querySelector('app-editor') + + if (coTabs.tab && coTabs.tab.isRootSettingsFile) { + coEditor.reload() + } notifications?.create({ type: 'info', title: 'Note', - message: 'A restart of the app your building may be required.' + message: 'A rebuild of your app may be required.' }) } } diff --git a/src/components/publish.js b/src/components/publish.js index 94737ec..a47e3d9 100644 --- a/src/components/publish.js +++ b/src/components/publish.js @@ -5,8 +5,6 @@ import { exec, execSync } from 'socket:child_process' import Tonic from '@socketsupply/tonic' import { TonicDialog } from '@socketsupply/components/dialog' -import Config from '../lib/config.js' - export class DialogPublish extends TonicDialog { click (e) { super.click(e) @@ -125,10 +123,6 @@ export class DialogPublish extends TonicDialog { // // Try to commit the changes. // - const msg = { - parent: currentHash, - message: commitMessage - } try { output = await exec(`git commit -m "${currentHash}" -m "${commitMessage}"`, { cwd }) diff --git a/src/components/relative-date.js b/src/components/relative-date.js new file mode 100644 index 0000000..9837df1 --- /dev/null +++ b/src/components/relative-date.js @@ -0,0 +1,49 @@ +import Tonic from '@socketsupply/tonic' + +const T_YEARS = 1000 * 60 * 60 * 24 * 365 +const T_MONTHS = 1000 * 60 * 60 * 24 * 30 +const T_WEEKS = 1000 * 60 * 60 * 24 * 7 +const T_DAYS = 1000 * 60 * 60 * 24 +const T_HOURS = 1000 * 60 * 60 +const T_MINUTES = 1000 * 60 +const T_SECONDS = 1000 + +class RelativeDate extends Tonic { + calculate () { + const ts = this.props.ts + const t = Math.abs(ts - new Date().getTime()) + const toString = i => String(parseInt(i, 10)) + + if (t > T_YEARS) { + return { value: `${toString(t / T_YEARS)}y` } + } else if (t > T_MONTHS) { + return { value: `${toString(t / T_MONTHS)}m` } + } else if (t > T_WEEKS) { + return { value: `${toString(t / T_WEEKS)}w` } + } else if (t > T_DAYS) { + return { value: `${toString(t / T_DAYS)}d` } + } else if (t > T_HOURS) { + return { value: `${toString(t / T_HOURS)}h` } + } else if (t > T_MINUTES) { + return { value: `${toString(t / T_MINUTES)}m`, timer: true } + } + return { value: `${toString(t / T_SECONDS)}s`, timer: true } + } + + render () { + let updates = 60 + const o = this.calculate() + const timer = setInterval(() => { + if (--updates === 0) return clearInterval(timer) + const o = this.calculate() + this.innerHTML = o.value + }, 1000) + + return this.html` + ${o.value} + ` + } +} + +export default RelativeDate +export { RelativeDate } diff --git a/src/components/subscribe.js b/src/components/subscribe.js index 759d4e2..3a472c8 100644 --- a/src/components/subscribe.js +++ b/src/components/subscribe.js @@ -1,7 +1,6 @@ import fs from 'socket:fs' import path from 'socket:path' import { Encryption, sha256 } from 'socket:network' -import { spawn, exec } from 'socket:child_process' import Tonic from '@socketsupply/tonic' import { TonicDialog } from '@socketsupply/components/dialog' @@ -83,22 +82,6 @@ export class DialogSubscribe extends TonicDialog { } async render () { - const app = this.props.parent - const { data: dataProjects } = await app.db.projects.readAll() - - /* const existingProjects - - for (const [projectId, project] of dataProjects.entries()) { - - tree.children.push({ - id: project.bundleId, - label: project.bundleId, - isDirectory: false, - icon: 'package', - children: [] - }) - } */ - return this.html`
Create Subscription diff --git a/src/git-data.js b/src/git-data.js index b853d23..188bfa9 100644 --- a/src/git-data.js +++ b/src/git-data.js @@ -1,6 +1,7 @@ +// import fs from 'fs' /** * Represents a parsed Git patch, including its headers, summary, and body. - * The `Patch` class takes a raw patch string as input and parses it into + * The \`Patch\` class takes a raw patch string as input and parses it into * distinct components. It specifically looks for the headers section at the * beginning of the patch, followed by a summary section indicating files * changed, insertions, and deletions, and finally the diff content as the body. @@ -74,4 +75,50 @@ export class Patch { this.headers.parent = this.headers.subject.split(' ')[1].trim() this.headers.subject = this.headers.subject.replace(this.headers.parent, '') } + + extractHunks (filePath) { + const hunks = [] + const lines = this.src.split('\n') + let capturingFile = false + let currentHunk = null + + // Adjusted regex for file section detection + const fileSectionRegex = new RegExp(`^diff --git a/${filePath.replace(/\./g, '\\.')} b/${filePath.replace(/\./g, '\\.')}`) + + // Adjusted regex for hunk header detection + const hunkHeaderRegex = /^@@ -\d+(,\d+)? \+\d+(,\d+)? @@/ + + lines.forEach(line => { + if (fileSectionRegex.test(line)) { + capturingFile = true + return // Skip the diff --git line itself + } else if (capturingFile && line.startsWith('diff --git')) { + capturingFile = false // Stop capturing when a new file section starts + } + + if (capturingFile) { + const match = hunkHeaderRegex.exec(line) + if (match) { + // Start of a new hunk + currentHunk = { headers: [match[0]], changes: [] } + hunks.push(currentHunk) + // Check if there's additional content on the same line following the hunk header + if (match[0].length < line.length) { + // Add the remaining part of the line to the changes + currentHunk.changes.push(' ' + line.substring(match[0].length).trim()) + } + } else if (currentHunk) { + // Add non-header lines to the current hunk's changes + currentHunk.changes.push(line) + } + } + }) + + return hunks + } } + +// const p = new Patch(fs.readFileSync('0001-wip-ui.patch', 'utf8')) +// const di = p.extractHunks('src/components/git-status.js') + +// console.log(di) diff --git a/src/index.js b/src/index.js index 71df639..3029976 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import fs from 'socket:fs' import path from 'socket:path' import process from 'socket:process' import application from 'socket:application' -import { network, Encryption, sha256 } from 'socket:network' +import { network, Encryption } from 'socket:network' import vm from 'socket:vm' import { inspect, format } from 'socket:util' import { spawn, exec } from 'socket:child_process' @@ -101,7 +101,7 @@ class AppView extends Tonic { clearTimeout(this.debounce) this.debounce = setTimeout(() => { - const currentProjectPath = this.getCurrentProjectPath() + const currentProjectPath = this.getCurrentProjectPath() for (const w of Object.values(this.previewWindows)) { const indexParams = new URLSearchParams({ @@ -167,7 +167,7 @@ class AppView extends Tonic { zoom: this.state.zoom[index] || '1' }).toString() - let currentProjectPath = this.getCurrentProjectPath() + const currentProjectPath = this.getCurrentProjectPath() if (!currentProjectPath) return const opts = { @@ -181,7 +181,7 @@ class AppView extends Tonic { webview_service_worker_frame: false }, path: [currentProjectPath, indexParams].join('?'), - index: index, + index, frameless: preview.frameless, closable: true, maximizable: false, @@ -242,13 +242,6 @@ class AppView extends Tonic { const { data: dataPeer } = await this.db.state.get('peer') const { data: dataUser } = await this.db.state.get('user') - // - // once awaited, we know that we have discovered our nat type and advertised - // to the network that we can accept inbound connections from other peers - // the socket is now ready to be read from and written to. - // - const pk = Buffer.from(dataUser.publicKey).toString('base64') - const signingKeys = { publicKey: dataUser.publicKey, privateKey: dataUser.privateKey @@ -292,7 +285,6 @@ class AppView extends Tonic { if (packet.index !== -1) return // not interested const pid = Buffer.from(packet.packetId).toString('hex') - const scid = Buffer.from(packet.subclusterId).toString('base64') const key = [projectId, pid].join('\xFF') const { data: hasPacket } = await this.db.patches.has(key) @@ -301,29 +293,15 @@ class AppView extends Tonic { const message = Buffer.from(value.data).toString() const patch = new Patch(message) - // we can hold onto the pk to compare against friends and warn on untrusted patches patch.publicKey = Buffer.from(packet.usr2) + patch.patchId = pid - await this.db.patches.put(patch.headers.parent, patch) + await this.db.patches.put(pid, patch) // if the project is showing, re-render it to show the new patch const coProject = document.querySelector('view-project-summary.show') if (coProject) coProject.reRender() }) - - subcluster.on('clone', async (value, packet) => { - console.log('GOT CLONE!', value, packet) - if (!packet.verified) return // gtfoa - if (packet.index !== -1) return // not interested - - const pid = Buffer.from(packet.packetId).toString('hex') - const scid = Buffer.from(packet.subclusterId).toString('base64') - const key = [projectId, pid].join('\xFF') - - const { data: hasPacket } = await this.db.patches.has(key) - if (hasPacket) return - - }) } } @@ -418,14 +396,12 @@ class AppView extends Tonic { } async initApplication () { - const notifications = document.querySelector('#notifications') const userSettingsFile = path.join(path.DATA, 'settings.json') - let exists let settings try { - exists = await fs.promises.stat(userSettingsFile) + await fs.promises.stat(userSettingsFile) } catch (err) { const settings = await fs.promises.readFile('settings.json') await fs.promises.writeFile(userSettingsFile, settings) @@ -452,14 +428,13 @@ class AppView extends Tonic { const coPreviewModeButton = document.querySelector('#toggle-preview-mode') coPreviewModeButton.classList.toggle('selected') - const coProperties = document.querySelector('app-properties') this.state.settings.previewMode = !this.state.settings.previewMode this.saveSettingsFile() } async saveSettingsFile () { - const currentProject = this.state.currentProject const pathToSettingsFile = path.join(path.DATA, 'settings.json') + const notifications = document.querySelector('#notifications') const coTabs = document.querySelector('editor-tabs') const coEditor = document.querySelector('app-editor') @@ -492,9 +467,6 @@ class AppView extends Tonic { // this app must bundle the platform-specific ssc binary // async exportProject () { - const project = document.querySelector('app-project') - const node = project.getNodeByProperty('id', 'project') - const args = [ 'build', '-r'