UNPKG

clipaste

Version:

Cross-platform CLI tool for clipboard operations - paste, copy, and manage text and images

192 lines (172 loc) 5.96 kB
const fs = require('fs').promises const path = require('path') const os = require('os') function getConfigDir () { const platform = process.platform if (platform === 'darwin') { return path.join(os.homedir(), 'Library', 'Application Support', 'clipaste') } else if (platform === 'win32') { const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming') return path.join(appData, 'clipaste') } else { const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config') return path.join(xdg, 'clipaste') } } class LibraryStore { constructor (opts = {}) { const base = opts.baseDir || process.env.CLIPASTE_CONFIG_DIR || getConfigDir() this.templatesDir = opts.templatesDir || path.join(base, 'templates') this.snippetsDir = opts.snippetsDir || path.join(base, 'snippets') this.verbose = !!opts.verbose } async _ensureDirs () { await fs.mkdir(this.templatesDir, { recursive: true }) await fs.mkdir(this.snippetsDir, { recursive: true }) } // Utilities _nameToPath (type, name) { const safeName = name.replace(/\\/g, '/').replace(/\.+\//g, '') const base = type === 'template' ? this.templatesDir : this.snippetsDir const ext = type === 'template' ? '.tmpl' : '.txt' return path.join(base, safeName + ext) } _indexPath (type) { const base = type === 'template' ? this.templatesDir : this.snippetsDir return path.join(base, 'index.json') } async _loadIndex (type) { const file = this._indexPath(type) try { const raw = await fs.readFile(file, 'utf8') const idx = JSON.parse(raw) return (idx && typeof idx === 'object') ? idx : {} } catch { return {} } } async _saveIndex (type, idx) { const file = this._indexPath(type) await fs.mkdir(path.dirname(file), { recursive: true }) await fs.writeFile(file, JSON.stringify(idx, null, 2), 'utf8') } async addSnippet (name, content) { await this._ensureDirs() const p = this._nameToPath('snippet', name) await fs.mkdir(path.dirname(p), { recursive: true }) await fs.writeFile(p, content, 'utf8') return p } async getSnippet (name) { const p = this._nameToPath('snippet', name) const content = await fs.readFile(p, 'utf8') return { path: p, content } } async deleteSnippet (name) { const p = this._nameToPath('snippet', name) await fs.unlink(p) } async listSnippets () { // naive recursive list const result = [] const base = this.snippetsDir async function walk (dir) { let entries = [] try { entries = await fs.readdir(dir, { withFileTypes: true }) } catch { return } for (const e of entries) { const full = path.join(dir, e.name) if (e.isDirectory()) await walk(full) else if (e.isFile() && e.name.endsWith('.txt')) { const rel = path.relative(base, full).replace(/\\/g, '/') const name = rel.replace(/\.txt$/, '') result.push({ name, path: full }) } } } await walk(base) return result } async saveTemplate (name, content) { await this._ensureDirs() const p = this._nameToPath('template', name) await fs.mkdir(path.dirname(p), { recursive: true }) await fs.writeFile(p, content, 'utf8') return p } async getTemplate (name) { const p = this._nameToPath('template', name) const content = await fs.readFile(p, 'utf8') return { path: p, content } } async deleteTemplate (name) { const p = this._nameToPath('template', name) await fs.unlink(p) } async listTemplates () { const result = [] const base = this.templatesDir async function walk (dir) { let entries = [] try { entries = await fs.readdir(dir, { withFileTypes: true }) } catch { return } for (const e of entries) { const full = path.join(dir, e.name) if (e.isDirectory()) await walk(full) else if (e.isFile() && e.name.endsWith('.tmpl')) { const rel = path.relative(base, full).replace(/\\/g, '/') const name = rel.replace(/\.tmpl$/, '') result.push({ name, path: full }) } } } await walk(base) return result } async addTags (type, name, tags) { const idx = await this._loadIndex(type) const key = name const entry = idx[key] || { tags: [] } const set = new Set(entry.tags || []) for (const t of (tags || [])) if (t) set.add(String(t)) entry.tags = Array.from(set) idx[key] = entry await this._saveIndex(type, idx) return entry.tags } async removeTags (type, name, tags) { const idx = await this._loadIndex(type) const key = name const entry = idx[key] || { tags: [] } const remove = new Set((tags || []).map(String)) entry.tags = (entry.tags || []).filter(t => !remove.has(t)) idx[key] = entry await this._saveIndex(type, idx) return entry.tags } async search (opts = {}) { const target = opts.target || 'templates' const q = (opts.query || '').toLowerCase() const tag = opts.tag const body = !!opts.body const list = target === 'snippets' ? await this.listSnippets() : await this.listTemplates() const idx = await this._loadIndex(target === 'snippets' ? 'snippet' : 'template') const out = [] for (const item of list) { const meta = idx[item.name] || {} const matchesTag = tag ? Array.isArray(meta.tags) && meta.tags.includes(tag) : true let matches = !!matchesTag if (matches && q) { let hay = item.name.toLowerCase() if (body) { try { const text = await fs.readFile(item.path, 'utf8') hay += '\n' + text.toLowerCase() } catch {} } matches = hay.includes(q) } if (matches) out.push({ name: item.name, path: item.path, tags: meta.tags || [] }) } return out } } module.exports = LibraryStore