Changes to ScrollHub

Breck Yunits
Breck Yunits
5 hours ago
public/scrollHubEditor.js
Changed around line 358: class EditorApp {
- "ds_store thumbs.db desktop.ini pdf png jpg jpeg gif webp bmp tiff ico svg eps raw cr2 nef heic doc docx xls xlsx ppt pptx odt ods odp pages numbers key zip tar gz 7z rar bz2 dmg iso tgz exe dll so dylib bin app msi deb rpm mp3 wav ogg mp4 avi mov wmv flv mkv".split(
+ "ds_store thumbs.db pdf png jpg jpeg gif webp bmp tiff ico eps raw cr2 nef heic doc docx xls xlsx ppt pptx odt ods odp pages numbers key zip tar gz 7z rar bz2 dmg iso tgz exe dll so dylib bin app msi deb rpm mp3 wav ogg mp4 avi mov wmv flv mkv".split(
Breck Yunits
Breck Yunits
5 hours ago
package.json
Changed around line 1
- "version": "0.82.0",
+ "version": "0.83.0",
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 馃摝 0.83.0 1/21/2025
+ 馃帀 handle binary files better
+
Breck Yunits
Breck Yunits
5 hours ago
Better binary file handling
public/scrollHubEditor.js
Changed around line 309: class EditorApp {
- const response = await fetch(`/readFile.htm?folderName=${folderName}&filePath=${encodeURIComponent(filePath)}`)
- const content = await response.text()
- this.setFileContent(content)
+ // Update UI state for binary files
+ if (this.isBinaryFile(fileName)) {
+ this.setFileContent("Binary file not shown.")
+ this.codeMirrorInstance.setOption("readOnly", true)
+ this.updateUIForBinaryFile(true)
+ } else {
+ // Regular file handling
+ const response = await fetch(`/readFile.htm?folderName=${folderName}&filePath=${encodeURIComponent(filePath)}`)
+ const content = await response.text()
+ this.setFileContent(content)
+ this.codeMirrorInstance.setOption("readOnly", false)
+ this.updateUIForBinaryFile(false)
+ await this.refreshParserCommand()
+ this.updateEditorMode(this.getEditorMode(fileName))
+ }
+
- await this.refreshParserCommand()
- this.updateEditorMode(this.getEditorMode(fileName))
Changed around line 355: class EditorApp {
+ // Add a method to check if a file is binary
+ isBinaryFile(fileName) {
+ const binaryExtensions = new Set(
+ "ds_store thumbs.db desktop.ini pdf png jpg jpeg gif webp bmp tiff ico svg eps raw cr2 nef heic doc docx xls xlsx ppt pptx odt ods odp pages numbers key zip tar gz 7z rar bz2 dmg iso tgz exe dll so dylib bin app msi deb rpm mp3 wav ogg mp4 avi mov wmv flv mkv".split(
+ " "
+ )
+ )
+ const extension = fileName.split(".").pop().toLowerCase()
+ return binaryExtensions.has(extension)
+ }
+
+ updateUIForBinaryFile(isBinary) {
+ // Get UI elements
+ const saveButton = document.querySelector('[onclick*="saveAndPublishCommand"]')
+ const formatButton = document.querySelector('[onclick*="formatFileCommand"]')
+
+ if (saveButton) {
+ saveButton.style.display = isBinary ? "none" : "inline-block"
+ }
+ if (formatButton) {
+ formatButton.style.display = isBinary ? "none" : "inline-block"
+ }
+
+ // Update editor styling for binary files
+ const editorElement = this.codeMirrorInstance.getWrapperElement()
+ if (isBinary) {
+ editorElement.classList.add("binary-file")
+ this.codeMirrorInstance.setOption("lineNumbers", false)
+ this.codeMirrorInstance.refresh()
+ } else {
+ editorElement.classList.remove("binary-file")
+ this.codeMirrorInstance.setOption("lineNumbers", this.showLineNumbers)
+ this.codeMirrorInstance.refresh()
+ }
+ }
+
public/scrollHubStyle.css
Changed around line 385: textarea {
+
+ .binary-file .CodeMirror-lines {
+ background-color: #f5f5f5;
+ color: #666;
+ font-style: italic;
+ cursor: not-allowed;
+ }
+ .binary-file .CodeMirror-cursor {
+ display: none !important;
+ }
Breck Yunits
Breck Yunits
5 hours ago
Only load in preview iframe when it makes sense
public/edit.scroll
Changed around line 12: libs.js
-
+
public/scrollHubEditor.js
Changed around line 861: I'd love to hear your requests and feedback! Find me on Warpcast.
+ get isPreviewableFile() {
+ if (!this.fileName) return false
+ const previewableExtensions = "html htm scroll parsers md txt css svg png jpg jpeg gif webp pdf".split(" ")
+ return previewableExtensions.some(ext => this.fileName.toLowerCase().endsWith(ext))
+ }
+
- this.previewIFrame.src = this.permalink
+
+ if (this.isPreviewableFile) this.previewIFrame.src = this.permalink
+ else this.previewIFrame.src = "about:blank"
Breck Yunits
Breck Yunits
7 hours ago
add server status route
ScrollHub.js
Changed around line 190: class ScrollHub {
+ this.requestsServed = 0
+ this.startCpuUsage = process.cpuUsage()
+ this.lastCpuUsage = this.startCpuUsage
+ this.lastCpuCheck = Date.now()
Changed around line 515: If you'd like to create this folder, visit our main site to get started.
+ this.requestsServed++
Changed around line 1320: If you'd like to create this folder, visit our main site to get started.
+ getCpuUsagePercent() {
+ const currentCpuUsage = process.cpuUsage(this.lastCpuUsage)
+ const currentTime = Date.now()
+ const timeDiff = currentTime - this.lastCpuCheck
+
+ // Calculate CPU usage percentage
+ const totalTicks = (currentCpuUsage.user + currentCpuUsage.system) / 1000 // Convert to microseconds
+ const cpuPercent = (totalTicks / timeDiff) * 100
+
+ // Update tracking variables
+ this.lastCpuUsage = process.cpuUsage()
+ this.lastCpuCheck = currentTime
+
+ return Math.min(100, cpuPercent.toFixed(1))
+ }
+
Changed around line 1363: If you'd like to create this folder, visit our main site to get started.
+ // Add the status route (in initCommandRoutes or as a separate method)
+ app.get("/status.htm", async (req, res) => {
+ const uptime = Math.floor((Date.now() - this.startTime) / 1000)
+ const memUsage = process.memoryUsage()
+ const totalMem = os.totalmem()
+ const freeMem = os.freemem()
+ const cpuUsage = this.getCpuUsagePercent()
+ const loadAvg = os.loadavg()
+
+ const status = {
+ server: {
+ version: this.version,
+ uptime: {
+ seconds: uptime,
+ formatted: `${Math.floor(uptime / 86400)}d ${Math.floor((uptime % 86400) / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${uptime % 60}s`
+ },
+ startTime: new Date(this.startTime).toISOString(),
+ hostname: this.hostname,
+ platform: process.platform,
+ nodeVersion: process.version
+ },
+ performance: {
+ cpu: {
+ usage: `${cpuUsage}%`,
+ cores: os.cpus().length,
+ loadAverage: {
+ "1m": loadAvg[0].toFixed(2),
+ "5m": loadAvg[1].toFixed(2),
+ "15m": loadAvg[2].toFixed(2)
+ }
+ },
+ memory: {
+ total: `${(totalMem / 1024 / 1024 / 1024).toFixed(2)} GB`,
+ free: `${(freeMem / 1024 / 1024 / 1024).toFixed(2)} GB`,
+ used: `${((totalMem - freeMem) / 1024 / 1024 / 1024).toFixed(2)} GB`,
+ process: {
+ heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
+ heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
+ rss: `${(memUsage.rss / 1024 / 1024).toFixed(2)} MB`
+ }
+ }
+ },
+ activity: {
+ requestsServed: this.requestsServed,
+ requestsPerSecond: (this.requestsServed / uptime).toFixed(2),
+ activeFolders: Object.keys(this.folderCache).length,
+ buildRequests: Object.keys(this.buildRequests).length,
+ sseClients: this.sseClients.size
+ }
+ }
+
+ const particle = new Particle(status)
+ particle.topDownArray.forEach(particle => particle.setCue("- " + particle.cue))
+ const html = await ScrollToHtml(particle.toString())
+
+ res.send(html)
+ })
+
Breck Yunits
Breck Yunits
8 hours ago
build speed improvements
ScrollHub.js
Changed around line 1342: If you'd like to create this folder, visit our main site to get started.
- app.get("/build/:folderName", checkWritePermissions, async (req, res) => {
- await this.runScrollCommand(req, res, "build")
- this.updateFolderAndBuildList(this.getFolderName(req))
- })
+ const buildFolder = async (req, res) => {
+ const now = Date.now()
+ const folderName = this.getFolderName(req)
+ if (!this.folderCache[folderName]) return res.status(404).send("Folder not found")
+ await this.buildFolder(folderName)
+ res.send((Date.now() - now).toString())
+ this.updateFolderAndBuildList(folderName)
+ }
- app.post("/build.htm", checkWritePermissions, async (req, res) => {
- await this.runScrollCommand(req, res, "build")
- this.updateFolderAndBuildList(this.getFolderName(req))
- })
+ app.get("/build/:folderName", checkWritePermissions, buildFolder)
+ app.post("/build.htm", checkWritePermissions, buildFolder)
+
+ app.get("/building.htm", async (req, res) => res.send(JSON.stringify(this.buildRequests)))
Changed around line 1754: If you'd like to create this folder, visit our main site to get started.
+ // todo: keep some folders in memory
- this.buildRequests[folderName] = 0
+ delete this.buildRequests[folderName]
public/scrollHubEditor.js
Changed around line 406: class EditorApp {
-
- console.log(`'${this.folderName}' built`)
+ console.log(`'${this.folderName}' built in ${message}ms`)
Breck Yunits
Breck Yunits
8 hours ago
Update release notes
package.json
Changed around line 1
- "version": "0.81.0",
+ "version": "0.82.0",
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 馃摝 0.82.0 1/21/2025
+ 馃帀 ability to set Git author
+ 馃帀 upgraded Scroll
+ 馃帀 better diff UI
+ 馃帀 persist hidden files setting
+ 馃帀 add deepseek reasoner model
+ 馃帀 better prompt logging
+ 馃帀 prettier format json
+ 馃帀 create folder by just uploading files
+ 馃彞 dont attempt to make ssl certs for non-existant folders
+ 馃彞 fix commits.json route
+ 馃彞 fix mjs file serving
+
Breck Yunits
Breck Yunits
9 hours ago
add ability to set git author
ScrollHub.js
Changed around line 654: If you'd like to create this folder, visit our main site to get started.
- const clientIp = req.ip || req.connection.remoteAddress
- const hostname = req.hostname?.toLowerCase()
- await execAsync(`git checkout ${targetHash} . && git add . && git commit --author="${clientIp} <${clientIp}@${hostname}>" -m "Reverted to ${targetHash}" --allow-empty`, { cwd: folderPath })
+ await execAsync(`git checkout ${targetHash} . && git add . && git commit ${this.getCommitAuthor(req)} -m "Reverted to ${targetHash}" --allow-empty`, { cwd: folderPath })
Changed around line 669: If you'd like to create this folder, visit our main site to get started.
+ getCommitAuthor(req) {
+ let author = ""
+ if (req.body.author?.match(/^[^<>]+\s<[^<>@\s]+@[^<>@\s]+>$/)) author = req.body.author
+ else {
+ const clientIp = req.ip || req.connection.remoteAddress
+ const hostname = req.hostname?.toLowerCase()
+ const author = `${clientIp} <${clientIp}@${hostname}>`
+ }
+ return `--author="${author}"`
+ }
+
Changed around line 1167: If you'd like to create this folder, visit our main site to get started.
- const clientIp = req.ip || req.connection.remoteAddress
- const hostname = req.hostname?.toLowerCase()
- await execAsync(`git add "${fileName}"; git commit --author="${clientIp} <${clientIp}@${hostname}>" -m 'Inserted particles into ${fileName}'`, { cwd: folderPath })
+ await execAsync(`git add "${fileName}"; git commit ${this.getCommitAuthor(req)} -m 'Inserted particles into ${fileName}'`, { cwd: folderPath })
Changed around line 1185: If you'd like to create this folder, visit our main site to get started.
- const filePath = path.join(rootFolder, folderName, decodeURIComponent(req.query.filePath))
+ const filePath = path.join(rootFolder, folderName, req.body.filePath)
Changed around line 1197: If you'd like to create this folder, visit our main site to get started.
- await execAsync(`git rm ${fileName}; git commit -m 'Deleted ${fileName}'`, { cwd: folderPath })
+ await execAsync(`git rm ${fileName}; git commit -m 'Deleted ${fileName}' ${this.getCommitAuthor(req)}`, { cwd: folderPath })
Changed around line 1252: If you'd like to create this folder, visit our main site to get started.
- const clientIp = req.ip || req.connection.remoteAddress
- const hostname = req.hostname?.toLowerCase()
- await execAsync(`git mv ${oldFileName} ${newFileName}; git commit --author="${clientIp} <${clientIp}@${hostname}>" -m 'Renamed ${oldFileName} to ${newFileName}'`, { cwd: folderPath })
+ await execAsync(`git mv ${oldFileName} ${newFileName}; git commit ${this.getCommitAuthor(req)} -m 'Renamed ${oldFileName} to ${newFileName}'`, { cwd: folderPath })
Changed around line 1658: If you'd like to create this folder, visit our main site to get started.
- const clientIp = req.ip || req.connection.remoteAddress
- const hostname = req.hostname?.toLowerCase()
- const author = `${clientIp} <${clientIp}@${hostname}>`
+ const author = this.getCommitAuthor(req)
Changed around line 1684: If you'd like to create this folder, visit our main site to get started.
- await execAsync(`git add -f ${relativePath}; git commit --author="${author}" -m '${action} ${relativePath}'`, { cwd: folderPath })
+ await execAsync(`git add -f ${relativePath}; git commit ${author} -m '${action} ${relativePath}'`, { cwd: folderPath })
public/scrollHubEditor.js
Changed around line 302: class EditorApp {
- this.updateFooterLinks()
+ await this.updateFooterLinks()
Changed around line 379: class EditorApp {
- const { folderName } = this
+ const { folderName, author } = this
- const formData = new FormData()
- formData.append("filePath", filePath)
- formData.append("folderName", folderName)
- formData.append("content", content)
- const response = await fetch("/writeFile.htm", {
- method: "POST",
- body: formData
- })
+ const response = await this.postData("/writeFile.htm", { filePath, content })
Changed around line 395: class EditorApp {
- const formData = new FormData()
- const { folderName } = this
- formData.append("folderName", folderName)
- const response = await fetch("/build.htm", {
- method: "POST",
- body: formData
- })
+ const response = await this.postData("/build.htm")
Changed around line 407: class EditorApp {
- console.log(`'${folderName}' built`)
+ console.log(`'${this.folderName}' built`)
Changed around line 834: I'd love to hear your requests and feedback! Find me on Warpcast.
- const formData = new FormData()
- formData.append("file", file)
- formData.append("folderName", this.folderName)
-
- const response = await fetch("/uploadFile.htm", {
- method: "POST",
- body: formData
- })
-
+ const response = await this.postData("/uploadFile.htm", { file })
Changed around line 915: I'd love to hear your requests and feedback! Find me on Warpcast.
- // Update the updateFooterLinks method to use the new modal
- updateFooterLinks() {
+ async updateFooterLinks() {
- document.getElementById("folderLinks").innerHTML =
- `trafficrevisionsclonedownloadduplicatemovedelete`
+ const code = `a traffic
+ class folderActionLink
+ href /globe.html?folderName=${folderName}
+ onclick if (!event.ctrlKey && !event.metaKey) { window.app.openIframeModalFromClick(event); return false; }
+ span 路
+ a revisions
+ class folderActionLink
+ href /commits.htm?folderName=${folderName}&count=30
+ onclick if (!event.ctrlKey && !event.metaKey) { window.app.openIframeModalFromClick(event); return false; }
+ span 路
+ a clone
+ class folderActionLink
+ href #
+ onclick window.app.copyClone()
+ span 路
+ a download
+ class folderActionLink
+ href ${folderName}.zip
+ span 路
+ a duplicate
+ class folderActionLink
+ href #
+ onclick window.app.duplicate()
+ span 路
+ a move
+ href #
+ class folderActionLink
+ onclick window.app.renameFolder()
+ span 路
+ a delete
+ href #
+ class folderActionLink
+ onclick window.app.deleteFolder()
+ span 路
+ a ${this.authorDisplayName}
+ href #
+ class folderActionLink
+ linkify false
+ onclick window.app.loginCommand()`
+ const html = await this.fusionEditor.scrollToHtml(code)
+ document.getElementById("folderLinks").innerHTML = html
+ }
+
+ get author() {
+ return localStorage.getItem("author")
+ }
+
+ get authorDisplayName() {
+ const { author } = this
+ if (!author) return "anon"
+ return author.split("<")[1].split(">")[0]
+ }
+
+ async loginCommand() {
+ const { author } = this
+ const defaultAuthor = "Anon "
+ const newAuthorName = prompt(`Your name and email:`, author || defaultAuthor)
+ if (newAuthorName === "" || newAuthorName === defaultAuthor) {
+ localStorage.removeItem("author", undefined)
+ } else if (newAuthorName && newAuthorName.match(/^[^<>]+\s<[^<>@\s]+@[^<>@\s]+>$/)) {
+ localStorage.setItem("author", newAuthorName)
+ }
+
+ await this.updateFooterLinks()
- const response = await fetch("/mv.htm", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- body: `oldFolderName=${this.folderName}&newFolderName=${newFolderName}`
- })
-
+ const response = await this.postData("/mv.htm", { oldFolderName: this.folderName, newFolderName })
Changed around line 997: I'd love to hear your requests and feedback! Find me on Warpcast.
- const response = await fetch("/trashFolder.htm", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- body: `folderName=${encodeURIComponent(this.folderName)}`
- })
-
+ const response = await this.postData("/trashFolder.htm")
Changed around line 1008: I'd love to hear your requests and feedback! Find me on Warpcast.
- const response = await fetch("/cloneFolder.htm", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- body: `folderName=${this.folderName}&redirect=false`
- })
-
+ const response = await this.postData("/cloneFolder.htm", { redirect: "false" })
Changed around line 1104: I'd love to hear your requests and feedback! Find me on Warpcast.
+ async postData(url, params = {}) {
+ const { folderName, author } = this
+ const formData = new FormData()
+ formData.append("folderName", folderName)
+ if (author) formData.append("author", author)
+ Object.keys(params).forEach(key => {
+ formData.append(key, params[key])
+ })
+ const response = await fetch(url, {
+ method: "POST",
+ body: formData
+ })
+ return response
+ }
+
- const response = await fetch("/renameFile.htm", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- body: `folderName=${encodeURIComponent(folderName)}&oldFileName=${encodeURIComponent(oldFileName)}&newFileName=${encodeURIComponent(newFileName)}`
- })
-
+ const response = await this.postData("/renameFile.htm", { oldFileName, newFileName })
Changed around line 1197: I'd love to hear your requests and feedback! Find me on Warpcast.
- const response = await fetch(`/deleteFile.htm?folderName=${folderName}&filePath=${encodeURIComponent(fileName)}`, {
- method: "POST"
- })
-
+ const response = await this.postData("/deleteFile.htm", { filePath: fileName })
Breck Yunits
Breck Yunits
16 hours ago
better diff styling
FolderIndex.js
Changed around line 1
- const AnsiToHtml = require("ansi-to-html")
Changed around line 180: class FolderIndex {
- // Get detailed commit information with a specific format
+ // Get detailed commit information with a custom format that's easier to parse
- const gitLogProcess = spawn("git", ["log", `-${count}`, "--color=always", "--date=iso", "--format=commit %H%nAuthor: %an <%ae>%nDate: %ad%nSubject: %s%n%n%b%n", "-p"], { cwd: folderPath })
+ const gitLogProcess = spawn("git", ["log", `-${count}`, "--color=always", "--date=iso", "--format=COMMIT_START%n%H%n%an%n%ae%n%ad%n%B%nCOMMIT_DIFF_START%n", "-p"], { cwd: folderPath })
Changed around line 200: class FolderIndex {
- const commitChunks = logOutput.split(/(?=commit [0-9a-f]{40}\n)/)
+ const commitChunks = logOutput.split("COMMIT_START\n").filter(Boolean)
- if (!chunk.trim()) continue
-
- const commitMatch = chunk.match(/commit (\w+)\n/)
- const authorMatch = chunk.match(/Author: ([^<]+)<([^>]+)>/)
- const dateMatch = chunk.match(/Date:\s+(.+)\n/)
- const messageMatch = chunk.match(/\n\n\s+(.+?)\n/)
- const diffContent = chunk.split(/\n\n/)[2] || ""
-
- if (commitMatch && authorMatch && dateMatch) {
- commits.push({
- id: commitMatch[1],
- name: authorMatch[1].trim(),
- email: authorMatch[2].trim(),
- time: new Date(dateMatch[1]),
- message: messageMatch ? messageMatch[1].trim() : "",
- diff: diffContent,
- rawOutput: chunk // Keep raw output for HTML generation
- })
+ const [commitInfo, ...diffParts] = chunk.split("COMMIT_DIFF_START\n")
+ const [hash, name, email, date, ...messageLines] = commitInfo.split("\n")
+
+ // Remove any trailing empty lines from the message
+ while (messageLines.length > 0 && messageLinesessageLines.length - 1].trim() === "") {
+ messageLines.pop()
+
+ // Join the message lines back together to preserve formatting
+ const message = messageLines.join("\n").trim()
+ const diff = diffParts.join("COMMIT_DIFF_START\n") // Restore any split diff parts
+
+ commits.push({
+ id: hash,
+ name: name.trim(),
+ email: email.trim(),
+ time: new Date(date),
+ message,
+ diff
+ })
Changed around line 232: class FolderIndex {
+ formatTimeAgo(date) {
+ const seconds = Math.floor((new Date() - date) / 1000)
+ const minutes = Math.floor(seconds / 60)
+ const hours = Math.floor(minutes / 60)
+ const days = Math.floor(hours / 24)
+ const months = Math.floor(days / 30)
+ const years = Math.floor(months / 12)
+
+ if (years > 0) return `${years} ${years === 1 ? "year" : "years"} ago`
+ if (months > 0) return `${months} ${months === 1 ? "month" : "months"} ago`
+ if (days > 0) return `${days} ${days === 1 ? "day" : "days"} ago`
+ if (hours > 0) return `${hours} ${hours === 1 ? "hour" : "hours"} ago`
+ if (minutes > 0) return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`
+ return `${seconds} ${seconds === 1 ? "second" : "seconds"} ago`
+ }
+
+ stripAnsi(text) {
+ // Remove ANSI escape codes
+ return text
+ .replace(/\u001b\[\d+m/g, "")
+ .replace(/\u001b\/g, "")
+ .replace(/\[\d+m/g, "")
+ .replace(/\/g, "")
+ }
+
+ parseDiff(diffText) {
+ const files = []
+ let currentFile = null
+
+ // Clean the diff text of ANSI codes first
+ const cleanDiff = this.stripAnsi(diffText)
+ const lines = cleanDiff.split("\n")
+
+ for (const line of lines) {
+ const cleanLine = line.trim()
+ if (!cleanLine) continue
+
+ if (cleanLine.startsWith("diff --git")) {
+ if (currentFile) files.push(currentFile)
+ const fileMatch = cleanLine.match(/b\/(.*?)(\s+|$)/)
+ const filename = fileMatch ? fileMatch[1] : "unknown file"
+ currentFile = { filename, changes: [] }
+ } else if (cleanLine.startsWith("+++") || cleanLine.startsWith("---") || cleanLine.startsWith("index")) {
+ continue // Skip these technical git lines
+ } else if (cleanLine.startsWith("+") && !cleanLine.startsWith("+++")) {
+ currentFile?.changes.push({ type: "addition", content: cleanLine.substring(1) })
+ } else if (cleanLine.startsWith("-") && !cleanLine.startsWith("---")) {
+ currentFile?.changes.push({ type: "deletion", content: cleanLine.substring(1) })
+ } else if (cleanLine.startsWith("@@")) {
+ const match = cleanLine.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@(.*)/)
+ if (match) {
+ const contextInfo = match[3].trim()
+ currentFile?.changes.push({
+ type: "info",
+ content: contextInfo ? `Changed around line ${match[2]}: ${contextInfo}` : `Changed around line ${match[2]}`
+ })
+ }
+ }
+ }
+ if (currentFile) files.push(currentFile)
+ return files
+ }
+
+ commitToHtml(commit, folderName) {
+ const timeAgo = this.formatTimeAgo(commit.time)
+ const files = this.parseDiff(commit.diff)
+
+ let html = `
+
+
+
+ + .trim()
+ .toLowerCase()
+ .split("")
+ .reduce((hash, char) => {
+ const chr = char.charCodeAt(0)
+ return ((hash << 5) - hash + chr) >>> 0
+ }, 0)}?s=40&d=identicon" alt="${commit.name}" class="avatar">
+
+
${commit.name}
+
${timeAgo}
+
+
+
+
+
+
+
+
+
+
+ Restore this version
+
+
+
+
+
+
${commit.message}
+
+
+ `
+
+ for (const file of files) {
+ html += `
+
+
+
+
+
+ ${file.filename}
+
+
+ `
+
+ for (const change of file.changes) {
+ if (change.type === "info") {
+ html += `
${change.content}
`
+ } else if (change.type === "addition") {
+ html += `
+ ${change.content}
`
+ } else if (change.type === "deletion") {
+ html += `
- ${change.content}
`
+ }
+ }
+
+ html += `
+
+
+ `
+ }
+
+ html += `
+
+
+ `
+
+ return html
+ }
+
- res.status(200).send(`No commits available for ${folderName}.`)
+ res.status(200).send(`No changes have been made to ${folderName} yet.`)
- const convert = new AnsiToHtml({ escapeXML: true })
-
- // Send HTML header
Changed around line 387: class FolderIndex {
- Last ${count} Commits for ${folderName}
+ Changes to ${folderName}
- body { font-family: monospace; white-space: pre-wrap; word-wrap: break-word; padding: 5px; }
- h2 { color: #333; }
- .aCommit {background-color: rgba(238, 238, 238, 0.8); padding: 8px; border-radius: 3px; margin-bottom: 10px;}
- .commit { border-bottom: 1px solid #ccc; padding-bottom: 20px; margin-bottom: 20px; }
- .commit-message { font-weight: bold; color: #005cc5; }
- input[type="submit"] { font-size: 0.8em; padding: 2px 5px; margin-left: 10px; }
+ :root {
+ --color-bg: #ffffff;
+ --color-text: #24292f;
+ --color-border: #d0d7de;
+ --color-addition-bg: #e6ffec;
+ --color-deletion-bg: #ffebe9;
+ --color-info-bg: #f6f8fa;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ :root {
+ --color-bg: #0d1117;
+ --color-text: #c9d1d9;
+ --color-border: #30363d;
+ --color-addition-bg: #0f2f1a;
+ --color-deletion-bg: #2d1214;
+ --color-info-bg: #161b22;
+ }
+ }
+
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--color-text);
+ background: var(--color-bg);
+ margin: 0;
+ padding: 20px;
+ }
+
+ .container {
+ max-width: 900px;
+ margin: 0 auto;
+ }
+
+ .header {
+ margin-bottom: 30px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ .header h1 {
+ font-size: 24px;
+ margin: 0;
+ }
+
+ .header h1 a{
+ color: var(--color-text);
+ text-decoration-color: transparent;
+ }
+
+ .header h1 a:hover{
+ color: var(--color-text);
+ text-decoration-color: var(--color-text);
+ }
+
+ .commit {
+ background: var(--color-bg);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ margin-bottom: 24px;
+ overflow: hidden;
+ }
+
+ .commit-header {
+ padding: 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ .commit-author {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ }
+
+ .author-name {
+ font-weight: 600;
+ }
+
+ .commit-time {
+ color: #57606a;
+ font-size: 12px;
+ }
+
+ .restore-button {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ border-radius: 6px;
+ border: 1px solid var(--color-border);
+ background: var(--color-bg);
+ color: var(--color-text);
+ cursor: pointer;
+ font-size: 12px;
+ transition: all 0.2s;
+ }
+
+ .restore-button:hover {
+ background: var(--color-info-bg);
+ }
+
+ .commit-message {
+ padding: 16px;
+ font-size: 14px;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ .commit-details {
+ padding: 16px;
+ }
+
+ .file-change {
+ margin-bottom: 24px;
+ }
+
+ .file-change:last-child {
+ margin-bottom: 0;
+ }
+
+ .filename {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
+ font-size: 12px;
+ margin-bottom: 8px;
+ }
+
+ .changes {
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
+ font-size: 12px;
+ line-height: 1.5;
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ overflow: hidden;
+ }
+
+ .line {
+ padding: 4px 8px;
+ white-space: pre-wrap;
+ word-break: break-all;
+ }
+
+ .addition {
+ background: var(--color-addition-bg);
+ }
+
+ .deletion {
+ background: var(--color-deletion-bg);
+ }
+
+ .change-info {
+ padding: 4px 8px;
+ background: var(--color-info-bg);
+ color: #57606a;
+ font-size: 12px;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ @media (max-width: 600px) {
+ .commit-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+ }
+
+ .restore-button {
+ width: 100%;
+ justify-content: center;
+ }
+ }
+
- // Process each commit
- let output = commit.rawOutput
- // Convert ANSI color codes to HTML
- output = convert.toHtml(output)
- // Add restore version button
- output = output.replace(/(commit\s)([0-9a-f]{40})/, (match, prefix, hash) => {
- return `
- ${prefix}${hash}
-
-
-
- `
- })
- res.write(output + "
\n")
+ res.write(this.commitToHtml(commit, folderName))
- res.end("")
+ res.end(`
+
+
+ `)
- res.status(500).send("An error occurred while fetching the git log")
+ res.status(500).send("Sorry, we couldn't load the change history right now. Please try again.")
-
package.json
Changed around line 7
- "ansi-to-html": "^0.7.2",
public/scrollHubEditor.js
Changed around line 940: I'd love to hear your requests and feedback! Find me on Warpcast.
- `trafficrevisionsclonedownloadduplicatemovedelete`
+ `trafficrevisionsclonedownloadduplicatemovedelete`
Breck Yunits
Breck Yunits
17 hours ago
ScrollHub.js
Changed around line 640: If you'd like to create this folder, visit our main site to get started.
+ res.send(JSON.stringify(commits, undefined, 2))