feat(tui): YAML configuration

This commit is contained in:
saberzero1 2026-02-22 21:34:16 +01:00
parent 292c1a1f2c
commit c1ae26eb31
No known key found for this signature in database
15 changed files with 455 additions and 377 deletions

16
package-lock.json generated
View File

@ -67,6 +67,7 @@
"vfile": "^6.0.3",
"workerpool": "^10.0.1",
"ws": "^8.19.0",
"yaml": "^2.8.2",
"yargs": "^18.0.0"
},
"bin": {
@ -8242,6 +8243,21 @@
"node": ">=10"
}
},
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yargs": {
"version": "18.0.0",
"license": "MIT",

View File

@ -96,6 +96,7 @@
"vfile": "^6.0.3",
"workerpool": "^10.0.1",
"ws": "^8.19.0",
"yaml": "^2.8.2",
"yargs": "^18.0.0"
},
"devDependencies": {

228
quartz.config.default.yaml Normal file
View File

@ -0,0 +1,228 @@
# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json
configuration:
pageTitle: Quartz 5
pageTitleSuffix: ""
enableSPA: true
enablePopovers: true
analytics:
provider: plausible
locale: en-US
baseUrl: quartz.jzhao.xyz
ignorePatterns:
- private
- templates
- .obsidian
defaultDateType: modified
theme:
fontOrigin: googleFonts
cdnCaching: true
typography:
header: Schibsted Grotesk
body: Source Sans Pro
code: IBM Plex Mono
colors:
lightMode:
light: "#faf8f8"
lightgray: "#e5e5e5"
gray: "#b8b8b8"
darkgray: "#4e4e4e"
dark: "#2b2b2b"
secondary: "#284b63"
tertiary: "#84a59d"
highlight: rgba(143, 159, 169, 0.15)
textHighlight: "#fff23688"
darkMode:
light: "#161618"
lightgray: "#393639"
gray: "#646464"
darkgray: "#d4d4d4"
dark: "#ebebec"
secondary: "#7b97aa"
tertiary: "#84a59d"
highlight: rgba(143, 159, 169, 0.15)
textHighlight: "#b3aa0288"
plugins:
- source: github:quartz-community/created-modified-date
enabled: true
options:
priority:
- frontmatter
- git
- filesystem
order: 10
- source: github:quartz-community/syntax-highlighting
enabled: true
options:
theme:
light: github-light
dark: github-dark
keepBackground: false
order: 20
- source: github:quartz-community/obsidian-flavored-markdown
enabled: true
options:
enableInHtmlEmbed: false
enableCheckbox: true
order: 30
- source: github:quartz-community/github-flavored-markdown
enabled: true
order: 40
- source: github:quartz-community/table-of-contents
enabled: true
order: 50
- source: github:quartz-community/crawl-links
enabled: true
options:
markdownLinkResolution: shortest
order: 60
- source: github:quartz-community/description
enabled: true
order: 70
- source: github:quartz-community/latex
enabled: true
options:
renderEngine: katex
order: 80
- source: github:quartz-community/citations
enabled: false
order: 85
- source: github:quartz-community/hard-line-breaks
enabled: false
order: 90
- source: github:quartz-community/ox-hugo
enabled: false
order: 91
- source: github:quartz-community/roam
enabled: false
order: 92
- source: github:quartz-community/remove-draft
enabled: true
- source: github:quartz-community/explicit-publish
enabled: false
- source: github:quartz-community/alias-redirects
enabled: true
- source: github:quartz-community/content-index
enabled: true
options:
enableSiteMap: true
enableRSS: true
- source: github:quartz-community/favicon
enabled: true
- source: github:quartz-community/og-image
enabled: true
- source: github:quartz-community/cname
enabled: true
- source: github:quartz-community/canvas-page
enabled: true
- source: github:quartz-community/content-page
enabled: true
- source: github:quartz-community/folder-page
enabled: true
- source: github:quartz-community/tag-page
enabled: true
- source: github:quartz-community/explorer
enabled: true
layout:
position: left
priority: 50
- source: github:quartz-community/graph
enabled: true
layout:
position: right
priority: 10
- source: github:quartz-community/search
enabled: true
layout:
position: left
priority: 20
group: toolbar
groupOptions:
grow: true
- source: github:quartz-community/backlinks
enabled: true
layout:
position: right
priority: 30
- source: github:quartz-community/article-title
enabled: true
layout:
position: beforeBody
priority: 10
- source: github:quartz-community/content-meta
enabled: true
layout:
position: beforeBody
priority: 20
- source: github:quartz-community/tag-list
enabled: true
layout:
position: beforeBody
priority: 30
- source: github:quartz-community/page-title
enabled: true
layout:
position: left
priority: 10
- source: github:quartz-community/darkmode
enabled: true
layout:
position: left
priority: 30
group: toolbar
- source: github:quartz-community/reader-mode
enabled: true
layout:
position: left
priority: 35
group: toolbar
- source: github:quartz-community/spacer
enabled: true
layout:
position: left
priority: 15
display: mobile-only
- source: github:quartz-community/breadcrumbs
enabled: true
layout:
position: beforeBody
priority: 5
condition: not-index
- source: github:quartz-community/comments
enabled: false
options:
provider: giscus
options: {}
layout:
position: afterBody
priority: 10
- source: github:quartz-community/footer
enabled: true
options:
links:
GitHub: https://github.com/jackyzha0/quartz
Discord Community: https://discord.gg/cRFFHYye7t
- source: github:quartz-community/recent-notes
enabled: false
layout:
groups:
toolbar:
direction: row
gap: 0.5rem
byPageType:
"404":
positions:
beforeBody: []
left: []
right: []
content: {}
folder:
exclude:
- reader-mode
positions:
right: []
tag:
exclude:
- reader-mode
positions:
right: []
canvas: {}

View File

@ -1,318 +0,0 @@
{
"$schema": "./quartz/plugins/quartz-plugins.schema.json",
"configuration": {
"pageTitle": "Quartz 5",
"pageTitleSuffix": "",
"enableSPA": true,
"enablePopovers": true,
"analytics": { "provider": "plausible" },
"locale": "en-US",
"baseUrl": "quartz.jzhao.xyz",
"ignorePatterns": ["private", "templates", ".obsidian"],
"defaultDateType": "modified",
"theme": {
"fontOrigin": "googleFonts",
"cdnCaching": true,
"typography": {
"header": "Schibsted Grotesk",
"body": "Source Sans Pro",
"code": "IBM Plex Mono"
},
"colors": {
"lightMode": {
"light": "#faf8f8",
"lightgray": "#e5e5e5",
"gray": "#b8b8b8",
"darkgray": "#4e4e4e",
"dark": "#2b2b2b",
"secondary": "#284b63",
"tertiary": "#84a59d",
"highlight": "rgba(143, 159, 169, 0.15)",
"textHighlight": "#fff23688"
},
"darkMode": {
"light": "#161618",
"lightgray": "#393639",
"gray": "#646464",
"darkgray": "#d4d4d4",
"dark": "#ebebec",
"secondary": "#7b97aa",
"tertiary": "#84a59d",
"highlight": "rgba(143, 159, 169, 0.15)",
"textHighlight": "#b3aa0288"
}
}
}
},
"plugins": [
{
"source": "github:quartz-community/created-modified-date",
"enabled": true,
"options": { "priority": ["frontmatter", "git", "filesystem"] },
"order": 10
},
{
"source": "github:quartz-community/syntax-highlighting",
"enabled": true,
"options": {
"theme": { "light": "github-light", "dark": "github-dark" },
"keepBackground": false
},
"order": 20
},
{
"source": "github:quartz-community/obsidian-flavored-markdown",
"enabled": true,
"options": { "enableInHtmlEmbed": false, "enableCheckbox": true },
"order": 30
},
{
"source": "github:quartz-community/github-flavored-markdown",
"enabled": true,
"order": 40
},
{
"source": "github:quartz-community/table-of-contents",
"enabled": true,
"order": 50
},
{
"source": "github:quartz-community/crawl-links",
"enabled": true,
"options": { "markdownLinkResolution": "shortest" },
"order": 60
},
{
"source": "github:quartz-community/description",
"enabled": true,
"order": 70
},
{
"source": "github:quartz-community/latex",
"enabled": true,
"options": { "renderEngine": "katex" },
"order": 80
},
{
"source": "github:quartz-community/citations",
"enabled": false,
"order": 85
},
{
"source": "github:quartz-community/hard-line-breaks",
"enabled": false,
"order": 90
},
{
"source": "github:quartz-community/ox-hugo",
"enabled": false,
"order": 91
},
{
"source": "github:quartz-community/roam",
"enabled": false,
"order": 92
},
{
"source": "github:quartz-community/remove-draft",
"enabled": true
},
{
"source": "github:quartz-community/explicit-publish",
"enabled": false
},
{
"source": "github:quartz-community/alias-redirects",
"enabled": true
},
{
"source": "github:quartz-community/content-index",
"enabled": true,
"options": { "enableSiteMap": true, "enableRSS": true }
},
{
"source": "github:quartz-community/favicon",
"enabled": true
},
{
"source": "github:quartz-community/og-image",
"enabled": true
},
{
"source": "github:quartz-community/cname",
"enabled": true
},
{
"source": "github:quartz-community/canvas-page",
"enabled": true
},
{
"source": "github:quartz-community/content-page",
"enabled": true
},
{
"source": "github:quartz-community/folder-page",
"enabled": true
},
{
"source": "github:quartz-community/tag-page",
"enabled": true
},
{
"source": "github:quartz-community/explorer",
"enabled": true,
"layout": {
"position": "left",
"priority": 50
}
},
{
"source": "github:quartz-community/graph",
"enabled": true,
"layout": {
"position": "right",
"priority": 10
}
},
{
"source": "github:quartz-community/search",
"enabled": true,
"layout": {
"position": "left",
"priority": 20,
"group": "toolbar",
"groupOptions": { "grow": true }
}
},
{
"source": "github:quartz-community/backlinks",
"enabled": true,
"layout": {
"position": "right",
"priority": 30
}
},
{
"source": "github:quartz-community/article-title",
"enabled": true,
"layout": {
"position": "beforeBody",
"priority": 10
}
},
{
"source": "github:quartz-community/content-meta",
"enabled": true,
"layout": {
"position": "beforeBody",
"priority": 20
}
},
{
"source": "github:quartz-community/tag-list",
"enabled": true,
"layout": {
"position": "beforeBody",
"priority": 30
}
},
{
"source": "github:quartz-community/page-title",
"enabled": true,
"layout": {
"position": "left",
"priority": 10
}
},
{
"source": "github:quartz-community/darkmode",
"enabled": true,
"layout": {
"position": "left",
"priority": 30,
"group": "toolbar"
}
},
{
"source": "github:quartz-community/reader-mode",
"enabled": true,
"layout": {
"position": "left",
"priority": 35,
"group": "toolbar"
}
},
{
"source": "github:quartz-community/spacer",
"enabled": true,
"layout": {
"position": "left",
"priority": 15,
"display": "mobile-only"
}
},
{
"source": "github:quartz-community/breadcrumbs",
"enabled": true,
"layout": {
"position": "beforeBody",
"priority": 5,
"condition": "not-index"
}
},
{
"source": "github:quartz-community/comments",
"enabled": false,
"options": { "provider": "giscus", "options": {} },
"layout": {
"position": "afterBody",
"priority": 10
}
},
{
"source": "github:quartz-community/footer",
"enabled": true,
"options": {
"links": {
"GitHub": "https://github.com/jackyzha0/quartz",
"Discord Community": "https://discord.gg/cRFFHYye7t"
}
}
},
{
"source": "github:quartz-community/recent-notes",
"enabled": false
}
],
"layout": {
"groups": {
"toolbar": {
"direction": "row",
"gap": "0.5rem"
}
},
"byPageType": {
"content": {},
"folder": {
"exclude": ["reader-mode"],
"positions": {
"right": []
}
},
"tag": {
"exclude": ["reader-mode"],
"positions": {
"right": []
}
},
"404": {
"positions": {
"beforeBody": [],
"left": [],
"right": []
}
},
"canvas": {}
}
}
}

View File

@ -97,7 +97,7 @@ yargs(hideBin(process.argv))
.command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => {
await handleBuild(argv)
})
.command("migrate", "Migrate old config to quartz.plugins.json", CommonArgv, async () => {
.command("migrate", "Migrate old config to quartz.config.yaml", CommonArgv, async () => {
await handleMigrate()
})
.command("tui", "Launch interactive plugin manager", CommonArgv, async () => {
@ -138,7 +138,7 @@ yargs(hideBin(process.argv))
)
.command(
"enable <names..>",
"Enable plugins in quartz.plugins.json",
"Enable plugins in quartz.config.yaml",
CommonArgv,
async (argv) => {
await handlePluginEnable(argv.names)
@ -146,7 +146,7 @@ yargs(hideBin(process.argv))
)
.command(
"disable <names..>",
"Disable plugins in quartz.plugins.json",
"Disable plugins in quartz.config.yaml",
CommonArgv,
async (argv) => {
await handlePluginDisable(argv.names)

View File

@ -24,6 +24,7 @@ import {
stashContentFolder,
} from "./helpers.js"
import { handlePluginRestore, handlePluginCheck } from "./plugin-git-handlers.js"
import { configExists, createConfigFromDefault } from "./plugin-data.js"
import {
UPSTREAM_NAME,
QUARTZ_SOURCE_BRANCH,
@ -217,11 +218,9 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
)
await fs.promises.writeFile(configFilePath, configContent)
const pluginsJsonPath = path.join(cwd, "quartz.plugins.json")
const defaultPluginsJsonPath = path.join(cwd, "quartz.plugins.default.json")
if (!fs.existsSync(pluginsJsonPath) && fs.existsSync(defaultPluginsJsonPath)) {
await fs.promises.copyFile(defaultPluginsJsonPath, pluginsJsonPath)
console.log(styleText("green", "Created quartz.plugins.json from defaults"))
if (!configExists()) {
createConfigFromDefault()
console.log(styleText("green", "Created quartz.config.yaml from defaults"))
}
// setup remote
@ -230,7 +229,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
})
outro(`You're all set! Not sure what to do next? Try:
Customizing Quartz a bit more by editing \`quartz.plugins.json\`
Customizing Quartz a bit more by editing \`quartz.config.yaml\`
Running \`npx quartz build --serve\` to preview your Quartz locally
Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting)
`)

View File

@ -2,12 +2,14 @@ import fs from "fs"
import path from "path"
import { spawnSync } from "child_process"
import { styleText } from "util"
import YAML from "yaml"
const CWD = process.cwd()
const CONFIG_PATH = path.join(CWD, "quartz.config.ts")
const LAYOUT_PATH = path.join(CWD, "quartz.layout.ts")
const PLUGINS_JSON_PATH = path.join(CWD, "quartz.plugins.json")
const DEFAULT_PLUGINS_JSON_PATH = path.join(CWD, "quartz.plugins.default.json")
const CONFIG_YAML_PATH = path.join(CWD, "quartz.config.yaml")
const DEFAULT_CONFIG_YAML_PATH = path.join(CWD, "quartz.config.default.yaml")
const LEGACY_DEFAULT_JSON_PATH = path.join(CWD, "quartz.plugins.default.json")
const LOCKFILE_PATH = path.join(CWD, "quartz.lock.json")
const PLUGINS_DIR = path.join(CWD, ".quartz", "plugins")
const PACKAGE_JSON_PATH = path.join(CWD, "package.json")
@ -117,11 +119,11 @@ export async function handleMigrate() {
return
}
if (fs.existsSync(PLUGINS_JSON_PATH)) {
console.log(styleText("yellow", "⚠ quartz.plugins.json already exists. Overwriting."))
if (fs.existsSync(CONFIG_YAML_PATH)) {
console.log(styleText("yellow", "⚠ quartz.config.yaml already exists. Overwriting."))
}
const defaultJson = readJson(DEFAULT_PLUGINS_JSON_PATH)
const defaultJson = readJson(DEFAULT_CONFIG_YAML_PATH) ?? readJson(LEGACY_DEFAULT_JSON_PATH)
let configuration = defaultJson?.configuration ?? {}
let layout = ensureLayoutDefaults(defaultJson?.layout ?? {})
let layoutInfo = null
@ -165,13 +167,13 @@ export async function handleMigrate() {
}
const outputJson = {
$schema: "./quartz/plugins/quartz-plugins.schema.json",
configuration,
plugins,
layout,
}
fs.writeFileSync(PLUGINS_JSON_PATH, JSON.stringify(outputJson, null, 2) + "\n")
const header = "# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json\n"
fs.writeFileSync(CONFIG_YAML_PATH, header + YAML.stringify(outputJson, { lineWidth: 120 }))
const configTemplate =
'import { loadQuartzConfig } from "./quartz/plugins/loader/config-loader"\n' +
@ -183,10 +185,10 @@ export async function handleMigrate() {
fs.writeFileSync(CONFIG_PATH, configTemplate)
fs.writeFileSync(LAYOUT_PATH, layoutTemplate)
console.log(styleText("green", "✓ Created quartz.plugins.json"))
console.log(styleText("green", "✓ Created quartz.config.yaml"))
console.log(styleText("green", "✓ Replaced quartz.config.ts"))
console.log(styleText("green", "✓ Replaced quartz.layout.ts"))
console.log()
console.log(styleText("yellow", "⚠ Verify plugin options in quartz.plugins.json"))
console.log(styleText("yellow", "⚠ Verify plugin options in quartz.config.yaml"))
console.log(styleText("gray", `Plugins migrated: ${plugins.length}`))
}

View File

@ -1,32 +1,63 @@
import fs from "fs"
import path from "path"
import { execSync } from "child_process"
import YAML from "yaml"
const LOCKFILE_PATH = path.join(process.cwd(), "quartz.lock.json")
const PLUGINS_DIR = path.join(process.cwd(), ".quartz", "plugins")
const PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
const DEFAULT_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.default.json")
const CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.yaml")
const DEFAULT_CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.default.yaml")
export function readPluginsJson() {
if (!fs.existsSync(PLUGINS_JSON_PATH)) return null
const LEGACY_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
const LEGACY_DEFAULT_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.default.json")
function resolveConfigPath() {
if (fs.existsSync(CONFIG_YAML_PATH)) return CONFIG_YAML_PATH
if (fs.existsSync(LEGACY_PLUGINS_JSON_PATH)) return LEGACY_PLUGINS_JSON_PATH
return CONFIG_YAML_PATH
}
function resolveDefaultConfigPath() {
if (fs.existsSync(DEFAULT_CONFIG_YAML_PATH)) return DEFAULT_CONFIG_YAML_PATH
if (fs.existsSync(LEGACY_DEFAULT_PLUGINS_JSON_PATH)) return LEGACY_DEFAULT_PLUGINS_JSON_PATH
return DEFAULT_CONFIG_YAML_PATH
}
function readFileAsData(filePath) {
if (!fs.existsSync(filePath)) return null
try {
return JSON.parse(fs.readFileSync(PLUGINS_JSON_PATH, "utf-8"))
const raw = fs.readFileSync(filePath, "utf-8")
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
return YAML.parse(raw)
}
return JSON.parse(raw)
} catch {
return null
}
}
function writeDataToFile(filePath, data) {
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
const header = "# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json\n"
fs.writeFileSync(filePath, header + YAML.stringify(data, { lineWidth: 120 }))
} else {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n")
}
}
export function readPluginsJson() {
const configPath = resolveConfigPath()
return readFileAsData(configPath)
}
export function writePluginsJson(data) {
fs.writeFileSync(PLUGINS_JSON_PATH, JSON.stringify(data, null, 2) + "\n")
const { $schema, ...rest } = data
writeDataToFile(CONFIG_YAML_PATH, rest)
}
export function readDefaultPluginsJson() {
if (!fs.existsSync(DEFAULT_PLUGINS_JSON_PATH)) return null
try {
return JSON.parse(fs.readFileSync(DEFAULT_PLUGINS_JSON_PATH, "utf-8"))
} catch {
return null
}
const defaultPath = resolveDefaultConfigPath()
return readFileAsData(defaultPath)
}
export function readLockfile() {
@ -116,7 +147,7 @@ export function ensurePluginsDir() {
}
/**
* Merges quartz.plugins.json, quartz.lock.json, and on-disk manifest data
* Merges quartz.config.yaml, quartz.lock.json, and on-disk manifest data
* into enriched plugin entries with: name, displayName, source, enabled,
* options, order, layout, category, installed, locked, manifest,
* currentCommit, modified.
@ -216,15 +247,14 @@ export function addPluginEntry(entry) {
}
export function configExists() {
return fs.existsSync(PLUGINS_JSON_PATH)
return fs.existsSync(CONFIG_YAML_PATH) || fs.existsSync(LEGACY_PLUGINS_JSON_PATH)
}
export function createConfigFromDefault() {
const defaultJson = readDefaultPluginsJson()
if (!defaultJson) {
const defaultData = readDefaultPluginsJson()
if (!defaultData) {
// No default available — create minimal config
const minimal = {
$schema: "./quartz/plugins/quartz-plugins.schema.json",
configuration: {
pageTitle: "Quartz",
enableSPA: true,
@ -273,8 +303,12 @@ export function createConfigFromDefault() {
writePluginsJson(minimal)
return minimal
}
writePluginsJson(defaultJson)
return defaultJson
const { $schema, ...rest } = defaultData
writePluginsJson(rest)
return rest
}
export { LOCKFILE_PATH, PLUGINS_DIR, PLUGINS_JSON_PATH, DEFAULT_PLUGINS_JSON_PATH }
export const PLUGINS_JSON_PATH = CONFIG_YAML_PATH
export const DEFAULT_PLUGINS_JSON_PATH = DEFAULT_CONFIG_YAML_PATH
export { LOCKFILE_PATH, PLUGINS_DIR }

View File

@ -320,7 +320,7 @@ export async function handlePluginRemove(names) {
export async function handlePluginEnable(names) {
const json = readPluginsJson()
if (!json) {
console.log(styleText("red", "✗ No quartz.plugins.json found. Cannot enable plugins."))
console.log(styleText("red", "✗ No quartz.config.yaml found. Cannot enable plugins."))
return
}
@ -329,7 +329,7 @@ export async function handlePluginEnable(names) {
(e) => extractPluginName(e.source) === name || e.source === name,
)
if (!entry) {
console.log(styleText("yellow", `⚠ Plugin "${name}" not found in quartz.plugins.json`))
console.log(styleText("yellow", `⚠ Plugin "${name}" not found in quartz.config.yaml`))
continue
}
if (entry.enabled) {
@ -346,7 +346,7 @@ export async function handlePluginEnable(names) {
export async function handlePluginDisable(names) {
const json = readPluginsJson()
if (!json) {
console.log(styleText("red", "✗ No quartz.plugins.json found. Cannot disable plugins."))
console.log(styleText("red", "✗ No quartz.config.yaml found. Cannot disable plugins."))
return
}
@ -355,7 +355,7 @@ export async function handlePluginDisable(names) {
(e) => extractPluginName(e.source) === name || e.source === name,
)
if (!entry) {
console.log(styleText("yellow", `⚠ Plugin "${name}" not found in quartz.plugins.json`))
console.log(styleText("yellow", `⚠ Plugin "${name}" not found in quartz.config.yaml`))
continue
}
if (!entry.enabled) {
@ -372,13 +372,13 @@ export async function handlePluginDisable(names) {
export async function handlePluginConfig(name, options = {}) {
const json = readPluginsJson()
if (!json) {
console.log(styleText("red", "✗ No quartz.plugins.json found."))
console.log(styleText("red", "✗ No quartz.config.yaml found."))
return
}
const entry = json.plugins.find((e) => extractPluginName(e.source) === name || e.source === name)
if (!entry) {
console.log(styleText("red", `✗ Plugin "${name}" not found in quartz.plugins.json`))
console.log(styleText("red", `✗ Plugin "${name}" not found in quartz.config.yaml`))
return
}

View File

@ -19,7 +19,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
{
key: "default" as Choice,
label: "Use default configuration",
description: "Copy quartz.plugins.default.json as your starting config",
description: "Copy quartz.config.default.yaml as your starting config",
},
]
: []),
@ -46,7 +46,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
</text>
<text>
<span fg="#888888">
quartz.plugins.json does not exist yet. How would you like to set up your configuration?
quartz.config.yaml does not exist yet. How would you like to set up your configuration?
</span>
</text>
</box>

View File

@ -295,7 +295,41 @@ export function LayoutPanel({ notify, onFocusChange }: LayoutPanelProps) {
enterView("confirm-remove-layout")
}
if (event.name === "k" && event.shift && selectedComp && selectedComponent > 0) {
if (event.name === "d" && event.shift && selectedComp) {
const arrIdx = findPluginArrayIndex(selectedComp.pluginIndex)
if (arrIdx >= 0) {
const plugin = plugins[arrIdx]
const manifest = plugin.manifest as Record<string, unknown> | null
const components = manifest?.components as
| Record<string, Record<string, unknown>>
| undefined
if (components) {
const compEntry = Object.values(components)[0]
if (compEntry) {
const defaultPosition = (compEntry.defaultPosition as string) || "left"
const defaultPriority = (compEntry.defaultPriority as number) || 50
updateLayout(arrIdx, {
position: defaultPosition,
priority: defaultPriority,
display: "all",
})
notify(`Restored ${selectedComp.displayName} to defaults`, "success")
} else {
notify("No component defaults found in manifest", "error")
}
} else {
// No manifest components — restore sensible defaults
updateLayout(arrIdx, {
position: selectedComp.position,
priority: 50,
display: "all",
})
notify(`Reset ${selectedComp.displayName} display settings`, "success")
}
}
}
if (event.name === "up" && event.shift && selectedComp && selectedComponent > 0) {
const above = currentComponents[selectedComponent - 1]
const arrIdx = findPluginArrayIndex(selectedComp.pluginIndex)
const aboveArrIdx = findPluginArrayIndex(above.pluginIndex)
@ -312,7 +346,7 @@ export function LayoutPanel({ notify, onFocusChange }: LayoutPanelProps) {
}
if (
event.name === "j" &&
event.name === "down" &&
event.shift &&
selectedComp &&
selectedComponent < currentComponents.length - 1
@ -685,7 +719,7 @@ export function LayoutPanel({ notify, onFocusChange }: LayoutPanelProps) {
<text>
<span fg="#888888">
{drillMode
? "↑↓ select │ K/J reorder │ m move │ p priority │ v display │ c condition │ x remove │ Esc: back"
? "↑↓ select │ ⇧↑↓ reorder │ m move │ p priority │ v display │ c condition │ x remove │ ⇧D restore │ Esc: back"
: `←→ columns │ ↑↓ zones │ Enter: edit zone │ g groups │ t page-types${pageTypes.length > 1 ? " │ [ prev / ] next page type" : ""}`}
</span>
</text>

View File

@ -367,11 +367,11 @@ export function PluginsPanel({ notify, onFocusChange, maxHeight }: PluginsPanelP
}
}
if (event.name === "a") {
if (event.name === "n") {
enterView("add")
}
if (event.name === "r" && selectedPlugin) {
if (event.name === "x" && selectedPlugin) {
enterView("confirm-remove")
}

View File

@ -1,6 +1,7 @@
import { useState, useMemo, useCallback, useEffect } from "react"
import { useKeyboard } from "@opentui/react"
import { useSettings } from "../hooks/useSettings.js"
import { readDefaultPluginsJson } from "../../plugin-data.js"
type View = "list" | "edit-string" | "edit-boolean" | "edit-enum" | "edit-array" | "edit-color"
@ -116,6 +117,37 @@ function formatStringValue(value: unknown): string {
return String(value)
}
function getDefaultSettingValue(
defaultConfig: Record<string, unknown> | null,
keyPath: string[],
): unknown | undefined {
if (!defaultConfig) return undefined
let current: unknown = defaultConfig
for (const key of keyPath) {
if (current === null || current === undefined || typeof current !== "object") return undefined
current = (current as Record<string, unknown>)[key]
}
return current
}
function deepEqual(a: unknown, b: unknown): boolean {
if (a === b) return true
if (a === null || b === null) return false
if (typeof a !== typeof b) return false
if (typeof a !== "object") return false
if (Array.isArray(a) !== Array.isArray(b)) return false
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false
return a.every((val, i) => deepEqual(val, b[i]))
}
const keysA = Object.keys(a as Record<string, unknown>)
const keysB = Object.keys(b as Record<string, unknown>)
if (keysA.length !== keysB.length) return false
return keysA.every((key) =>
deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),
)
}
export function SettingsPanel({ notify, onFocusChange }: SettingsPanelProps) {
const { config, updateField } = useSettings()
const [view, setView] = useState<View>("list")
@ -130,6 +162,11 @@ export function SettingsPanel({ notify, onFocusChange }: SettingsPanelProps) {
const [colorError, setColorError] = useState<string | null>(null)
const [collapsed, setCollapsed] = useState<Set<string>>(new Set())
const defaultConfig = useMemo(() => {
const data = readDefaultPluginsJson()
return (data?.configuration as Record<string, unknown>) ?? null
}, [])
const allEntries = useMemo(() => {
if (!config) return []
return flattenConfig(config)
@ -252,6 +289,21 @@ export function SettingsPanel({ notify, onFocusChange }: SettingsPanelProps) {
enterEdit(entry)
}
}
if (event.name === "d" && event.shift) {
const entry = visibleEntries[highlightedIndex]
if (!entry) return
const defaultValue = getDefaultSettingValue(defaultConfig, entry.keyPath)
if (defaultValue === undefined) {
notify("No default available for " + entry.keyPath.join("."), "error")
return
}
if (deepEqual(entry.value, defaultValue)) {
notify(entry.keyPath.join(".") + " is already default", "info")
return
}
applyValue(entry.keyPath, defaultValue)
notify("Restored " + entry.keyPath.join(".") + " to default", "success")
}
})
useKeyboard((event) => {
@ -416,6 +468,10 @@ export function SettingsPanel({ notify, onFocusChange }: SettingsPanelProps) {
}
const displayFg = isHighlighted ? (valueFg ?? highlightFg) : (valueFg ?? baseFg)
const isDefault =
!entry.isObject &&
deepEqual(entry.value, getDefaultSettingValue(defaultConfig, entry.keyPath))
const defaultTag = isDefault ? " (default)" : ""
return (
<text key={entry.keyPath.join(".")}>
@ -435,7 +491,19 @@ export function SettingsPanel({ notify, onFocusChange }: SettingsPanelProps) {
</span>
)}
{swatchColor ? <span fg={swatchColor}> </span> : null}
<span fg={displayFg}>{isHighlighted ? <strong>{valueText}</strong> : valueText}</span>
<span fg={displayFg}>
{isHighlighted ? (
<strong>
{valueText}
{defaultTag}
</strong>
) : (
<>
{valueText}
<span fg="#555555">{defaultTag}</span>
</>
)}
</span>
</text>
)
})
@ -705,6 +773,9 @@ export function SettingsPanel({ notify, onFocusChange }: SettingsPanelProps) {
>
{renderTree(false)}
</box>
<text>
<span fg="#888888">: navigate Enter: edit/expand Shift+D: restore default</span>
</text>
</box>
)
}

View File

@ -1,5 +1,6 @@
import fs from "fs"
import path from "path"
import YAML from "yaml"
import { styleText } from "util"
import { QuartzConfig, GlobalConfiguration, FullPageLayout } from "../../cfg"
import { QuartzComponent } from "../../components/types"
@ -18,13 +19,23 @@ import { loadComponentsFromPackage } from "./componentLoader"
import { componentRegistry } from "../../components/registry"
import { getCondition } from "./conditions"
const PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
const CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.yaml")
const LEGACY_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
function resolveConfigPath(): string {
if (fs.existsSync(CONFIG_YAML_PATH)) return CONFIG_YAML_PATH
if (fs.existsSync(LEGACY_PLUGINS_JSON_PATH)) return LEGACY_PLUGINS_JSON_PATH
return CONFIG_YAML_PATH
}
function readPluginsJson(): QuartzPluginsJson | null {
if (!fs.existsSync(PLUGINS_JSON_PATH)) {
const configPath = resolveConfigPath()
if (!fs.existsSync(configPath)) {
return null
}
const raw = fs.readFileSync(PLUGINS_JSON_PATH, "utf-8")
const raw = fs.readFileSync(configPath, "utf-8")
if (configPath.endsWith(".yaml") || configPath.endsWith(".yml")) {
return YAML.parse(raw) as QuartzPluginsJson
}
return JSON.parse(raw) as QuartzPluginsJson
}

View File

@ -120,7 +120,7 @@ export type PluginSpecifier =
| { name: string; options?: unknown }
| { plugin: LoadedPlugin["plugin"]; manifest?: Partial<PluginManifest> }
/** Layout declaration for a component-providing plugin in quartz.plugins.json */
/** Layout declaration for a component-providing plugin in quartz.config.yaml */
export interface PluginLayoutDeclaration {
position: LayoutPosition
priority: number
@ -137,7 +137,7 @@ export interface PluginLayoutDeclaration {
}
}
/** A single plugin entry in quartz.plugins.json */
/** A single plugin entry in quartz.config.yaml */
export interface PluginJsonEntry {
source: string
enabled: boolean
@ -159,13 +159,13 @@ export interface PageTypeLayoutOverride {
positions?: Partial<Record<LayoutPosition, PluginLayoutDeclaration[]>>
}
/** Top-level layout section of quartz.plugins.json */
/** Top-level layout section of quartz.config.yaml */
export interface LayoutConfig {
groups?: Record<string, FlexGroupConfig>
byPageType?: Record<string, PageTypeLayoutOverride>
}
/** Root type for quartz.plugins.json */
/** Root type for quartz.config.yaml */
export interface QuartzPluginsJson {
$schema?: string
configuration: Record<string, unknown>