Initial implementation of songbook toolset

Kotlin/JVM multi-module project for generating a scout songbook PDF
from ChordPro-format text files. Includes ChordPro parser, layout engine
with greedy spread packing for double-page songs, OpenPDF renderer,
CLI (Clikt), Compose Desktop GUI, and 5 sample songs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
shahondin1624
2026-03-17 08:35:42 +01:00
commit e386501b57
56 changed files with 5152 additions and 0 deletions

17
cli/build.gradle.kts Normal file
View File

@@ -0,0 +1,17 @@
plugins {
id("songbook-conventions")
application
}
application {
mainClass.set("de.pfadfinder.songbook.cli.MainKt")
}
dependencies {
implementation(project(":app"))
implementation(project(":model"))
implementation(project(":parser"))
implementation("com.github.ajalt.clikt:clikt:5.0.3")
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
implementation("ch.qos.logback:logback-classic:1.5.16")
}

View File

@@ -0,0 +1,37 @@
package de.pfadfinder.songbook.cli
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.ProgramResult
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import de.pfadfinder.songbook.app.SongbookPipeline
import java.io.File
class BuildCommand : CliktCommand(name = "build") {
override fun help(context: Context) = "Build the songbook PDF"
private val projectDir by option("-d", "--dir", help = "Project directory").default(".")
override fun run() {
val dir = File(projectDir).absoluteFile
echo("Building songbook from: ${dir.path}")
val pipeline = SongbookPipeline(dir)
val result = pipeline.build()
if (result.success) {
echo("Build successful!")
echo(" Songs: ${result.songCount}")
echo(" Pages: ${result.pageCount}")
echo(" Output: ${result.outputFile?.absolutePath}")
} else {
echo("Build failed with ${result.errors.size} error(s):", err = true)
for (error in result.errors) {
val location = listOfNotNull(error.file, error.line?.toString()).joinToString(":")
echo(" [$location] ${error.message}", err = true)
}
throw ProgramResult(1)
}
}
}

View File

@@ -0,0 +1,15 @@
package de.pfadfinder.songbook.cli
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.main
import com.github.ajalt.clikt.core.subcommands
class SongbookCli : CliktCommand(name = "songbook") {
override fun run() = Unit
}
fun main(args: Array<String>) {
SongbookCli()
.subcommands(BuildCommand(), ValidateCommand())
.main(args)
}

View File

@@ -0,0 +1,34 @@
package de.pfadfinder.songbook.cli
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.ProgramResult
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import de.pfadfinder.songbook.app.SongbookPipeline
import java.io.File
class ValidateCommand : CliktCommand(name = "validate") {
override fun help(context: Context) = "Validate all song files"
private val projectDir by option("-d", "--dir", help = "Project directory").default(".")
override fun run() {
val dir = File(projectDir).absoluteFile
echo("Validating songbook in: ${dir.path}")
val pipeline = SongbookPipeline(dir)
val errors = pipeline.validate()
if (errors.isEmpty()) {
echo("All songs are valid!")
} else {
echo("Found ${errors.size} error(s):", err = true)
for (error in errors) {
val location = listOfNotNull(error.file, error.line?.toString()).joinToString(":")
echo(" [$location] ${error.message}", err = true)
}
throw ProgramResult(1)
}
}
}