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'