
Command.pudu
Pudu197 lines7.9 KB
1/** @Docgen.Command.Seam — the docgen command line and its exit statuses */2module PuduLangDocgen.Command34import Std.Concurrent as Concurrent5import Std.Io as Io6import Std.Path as Path7import Std.Process as Process8import PuduLangDocgen.Build as Build9import PuduLangDocgen.Command.Actions as Actions10import PuduLangDocgen.Command.Arguments as Arguments11import PuduLangDocgen.Constants.Package as Package12import PuduLangDocgen as Docgen13import PuduLangDocgen.Docset as Docset14import PuduLangDocgen.Serve as Serve15import PuduLangLog.Logger as Logger16import PuduLangLog.Value as Value171819const PORT: Int = 8080202122const WATCH_MILLIS: Int = 1000232425const OPEN_DELAY_MILLIS: Int = 600262728const OPENERS: Array[Str] = ["open", "xdg-open", "wslview"]293031export fn run(arguments: &Array[Str]) -> Int {32 let parsed = match Arguments.parse(arguments) {33 case Ok(found) => found34 case Err(reason) => {35 let _said = Io.writeErrorLine(reason + "\n\n" + Arguments.usage())36 return 237 }38 }39 if Arguments.has(&parsed, "--help") || parsed.command == "help" {40 let _said = Io.writeLine(Arguments.usage())41 return 042 }43 if parsed.command == "version" {44 let _said = Io.writeLine(Package.NAME + " " + Package.VERSION)45 return 046 }47 let log = match Actions.logger(&parsed) {48 case Ok(found) => found49 case Err(reason) => {50 let _said = Io.writeErrorLine(reason)51 return 252 }53 }54 let status = dispatch(&log, &parsed)55 Logger.close(&log)56 status57}585960fn dispatch(log: &Logger.Logger, parsed: &Arguments.Parsed) -> Int {61 if parsed.command == "serve" { return serve(log, if parsed.positional.isEmpty() { "_site" } else { parsed.positional[0] }, parsed, "") }62 if parsed.command == "init" { return Actions.initialize(log, parsed) }63 if parsed.command == "template" { return Actions.template(log, parsed) }64 if parsed.command == "download" { return Actions.download(log, parsed) }65 if parsed.command == "merge" { return Actions.merge(log, parsed) }66 let config = if parsed.positional.isEmpty() { Package.CONFIG_FILE } else { Arguments.configPath(parsed.positional[0]) }67 let chosen = match options(parsed) {68 case Ok(found) => found69 case Err(reason) => {70 log.error("\{Reason\}", [Value.text(reason)])71 return 272 }73 }74 if parsed.command == "metadata" {75 return match Docset.metadata(config, &chosen) {76 case Ok(written) => {77 for path in written { Actions.say(log, "wrote " + path) }78 079 }80 case Err(problems) => Actions.failed(log, &problems)81 }82 }83 if parsed.command == "pdf" {84 return match Docset.pdf(config, &chosen) {85 case Ok(produced) => {86 for path in produced { Actions.say(log, "wrote " + path) }87 088 }89 case Err(problems) => Actions.failed(log, &problems)90 }91 }92 let outcome = if parsed.command == "build" {93 match Docset.build(config, &chosen, &Build.extensions()) {94 case Ok(report) => Ok((report, []))95 case Err(problems) => Err(problems)96 }97 } else { Docset.buildAll(config, &chosen) }98 match outcome {99 case Ok((report, produced)) => {100 Actions.report(log, &report.diagnostics)101 for path in report.written { Actions.detail(log, "wrote " + path) }102 for path in report.removed { Actions.detail(log, "removed " + path) }103 for path in produced { Actions.say(log, "wrote " + path) }104 Actions.say(log, "Build succeeded: " + show(report.written.length()) + " written, " + show(report.unchanged.length()) + " unchanged, " + show(report.removed.length()) + " removed.")105 if Arguments.has(parsed, "--serve") {106 let folder = Path.join(Path.directoryOf(config), if chosen.output.isEmpty() { "_site" } else { chosen.output })107 return serve(log, folder, parsed, config)108 }109 0110 }111 case Err(problems) => Actions.failed(log, &problems)112 }113}114115116fn options(parsed: &Arguments.Parsed) -> Result[Docset.Options, Str] {117 var metadata: Array[(Str, Docgen.Meta)] = []118 for written in Arguments.valuesOf(parsed, "--metadata") {119 let at = written.indexOf("=")120 if at < 1 { return Err("metadata must be written key=value: " + written) }121 let value = written.drop(at + 1)122 metadata = metadata.push((written.take(at), if value == "true" { Docgen.Flag(true) } else if value == "false" { Docgen.Flag(false) } else { Docgen.Text(value) }))123 }124 Ok(Docset.Options{..Docset.options(),125 output: Arguments.valueOf(parsed, "--output", ""), metadata: metadata, xref: Arguments.valuesOf(parsed, "--xref"),126 templates: Arguments.valuesOf(parsed, "--template").concat(Arguments.valuesOf(parsed, "--theme")),127 force: Arguments.has(parsed, "--force"), dryRun: Arguments.has(parsed, "--dryRun"), warningsAsErrors: Arguments.has(parsed, "--warningsAsErrors"),128 disableGitFeatures: Arguments.has(parsed, "--disableGitFeatures"), exportRawModel: Arguments.has(parsed, "--exportRawModel"),129 exportViewModel: Arguments.has(parsed, "--exportViewModel") })130}131132133134135fn serve(log: &Logger.Logger, folder: Str, parsed: &Arguments.Parsed, config: Str) -> Int {136 let host = Arguments.valueOf(parsed, "--hostname", "localhost")137 let port = match Arguments.valueOf(parsed, "--port", show(PORT)).toInt() {138 case Some(found) => found139 case None => {140 log.error("port must be a number", [])141 return 2142 }143 }144 if !config.isEmpty() && Arguments.has(parsed, "--watch") { watch(log, config, parsed) }145 let file = Arguments.valueOf(parsed, "--open-file", "")146 if Arguments.has(parsed, "--open-browser") || !file.isEmpty() {147 let address = "http://" + (if host == "0.0.0.0" { "localhost" } else { host }) + ":" + show(port) + "/" + file148 let _opening = Concurrent.start(fn() -> () {149 let _paused = Concurrent.sleep(OPEN_DELAY_MILLIS)150 for opener in OPENERS {151 if Process.isAvailable(opener) {152 let _opened = Process.run(opener, &[address])153 return154 }155 }156 })157 }158 match Serve.serve(folder, host, port) {159 case Ok(_) => 0160 case Err(reason) => {161 log.error("cannot serve \{Folder\}: \{Reason\}", [Value.text(folder), Value.text(reason)])162 1163 }164 }165}166167168fn watch(log: &Logger.Logger, config: Str, parsed: &Arguments.Parsed) -> () {169 let chosen = match options(parsed) {170 case Ok(found) => found171 case Err(_) => Docset.options()172 }173 let output = if chosen.output.isEmpty() { "_site" } else { chosen.output }174 let reporter = *log175 let started = Concurrent.start(fn() -> () {176 var seen = Docset.fingerprint(config, output)177 while true {178 let _paused = Concurrent.sleep(WATCH_MILLIS)179 let current = Docset.fingerprint(config, output)180 if current != seen {181 seen = current182 match Docset.build(config, &chosen, &Build.extensions()) {183 case Ok(report) => {184 Actions.report(&reporter, &report.diagnostics)185 Actions.say(&reporter, "Rebuilt: " + show(report.written.length()) + " written, " + show(report.removed.length()) + " removed.")186 }187 case Err(problems) => Actions.report(&reporter, &problems)188 }189 }190 }191 })192 match started {193 case Ok(_) => Actions.say(log, "Watching " + config + " for changes")194 case Err(_) => log.warning("cannot watch for changes; serving without rebuilding", [])195 }196}197