1182 lines
39 KiB
Odin
1182 lines
39 KiB
Odin
package main
|
|
|
|
import "core:fmt"
|
|
import "core:os"
|
|
import "core:strconv"
|
|
import "core:strings"
|
|
import "../adapters"
|
|
import "../core"
|
|
import "../gui"
|
|
import "../shared"
|
|
import "../ui"
|
|
|
|
CLI_Command_Kind :: enum {
|
|
Demo,
|
|
Status,
|
|
Save,
|
|
Load,
|
|
Tui,
|
|
Gui,
|
|
Help,
|
|
}
|
|
|
|
Parsed_CLI_Command :: struct {
|
|
kind: CLI_Command_Kind,
|
|
path: string,
|
|
}
|
|
|
|
parse_cli_command :: proc(args: []string) -> Parsed_CLI_Command {
|
|
if len(args) == 0 {
|
|
return Parsed_CLI_Command{kind = .Demo}
|
|
}
|
|
|
|
switch args[0] {
|
|
case "status":
|
|
return Parsed_CLI_Command{kind = .Status}
|
|
case "save":
|
|
if len(args) >= 2 {
|
|
return Parsed_CLI_Command{kind = .Save, path = args[1]}
|
|
}
|
|
return Parsed_CLI_Command{kind = .Help}
|
|
case "load":
|
|
if len(args) >= 2 {
|
|
return Parsed_CLI_Command{kind = .Load, path = args[1]}
|
|
}
|
|
return Parsed_CLI_Command{kind = .Help}
|
|
case "tui":
|
|
return Parsed_CLI_Command{kind = .Tui}
|
|
case "gui":
|
|
return Parsed_CLI_Command{kind = .Gui}
|
|
case "help", "-h", "--help":
|
|
return Parsed_CLI_Command{kind = .Help}
|
|
}
|
|
|
|
return Parsed_CLI_Command{kind = .Help}
|
|
}
|
|
|
|
usage_text :: proc() -> string {
|
|
return "Usage: comic_odin [status|save <path>|load <path>|tui|gui|help]"
|
|
}
|
|
|
|
tui_help_text :: proc() -> string {
|
|
return "TUI commands: help|h, status|s, doctor|?, ready|r, next|n, plan|p, auto|x, auto all <pdf|png|cbz> <path>, auto all local <pdf|png|cbz> <path> [pages], new, set idea <text>, set genre <text>, set audience <text>, generate script [pages], generate script local [pages], generate panels [page <n>], generate panels local [page <n>], layout auto, export <pdf|png|cbz> <path>, quick local <pdf|png|cbz> <path> [pages], quick local all <project_path> <pdf|png|cbz> <export_path> [pages], goto <screen> (or 1..8), step <workflow>, save|saveas <path>, load|open <path>, start <script|character|panel|export>, progress <0-100>, done|d, fail <message>, cancel|c, quit|q"
|
|
}
|
|
|
|
bool_text :: proc(v: bool) -> string {
|
|
if v {
|
|
return "yes"
|
|
}
|
|
return "no"
|
|
}
|
|
|
|
export_format_name :: proc(f: core.Export_Format) -> string {
|
|
switch f {
|
|
case .PDF: return "pdf"
|
|
case .PNG: return "png"
|
|
case .CBZ: return "cbz"
|
|
}
|
|
return "pdf"
|
|
}
|
|
|
|
command_available :: proc(name: string) -> bool {
|
|
cmd := [2]string{"which", name}
|
|
desc := os.Process_Desc{command = cmd[:]}
|
|
state, _, _, err := os.process_exec(desc, context.temp_allocator)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return state.exited && state.exit_code == 0
|
|
}
|
|
|
|
build_doctor_report :: proc() -> string {
|
|
cfg := shared.load_config()
|
|
has_deepseek := len(cfg.deepseek_api_key) > 0
|
|
has_fal := len(cfg.fal_api_key) > 0
|
|
has_curl := command_available("curl")
|
|
has_python := command_available("python3")
|
|
return fmt.aprintf("Doctor\n- deepseek key: %s\n- fal key: %s\n- curl: %s\n- python3: %s", bool_text(has_deepseek), bool_text(has_fal), bool_text(has_curl), bool_text(has_python))
|
|
}
|
|
|
|
build_ready_report :: proc(c: ui.App_Controller) -> string {
|
|
has_script := len(c.state.script.pages) > 0
|
|
has_panels := len(c.state.panel_images) > 0
|
|
has_layout := len(c.state.page_layouts) > 0
|
|
can_export := has_layout && has_panels
|
|
return fmt.aprintf("Ready\n- script generated: %s\n- panel images generated: %s\n- layout generated: %s\n- export ready: %s", bool_text(has_script), bool_text(has_panels), bool_text(has_layout), bool_text(can_export))
|
|
}
|
|
|
|
next_action_hint :: proc(c: ui.App_Controller) -> string {
|
|
cfg := shared.load_config()
|
|
if len(c.state.script.pages) == 0 {
|
|
if len(cfg.deepseek_api_key) > 0 {
|
|
return "next: generate script 4"
|
|
}
|
|
return "next: generate script local 2"
|
|
}
|
|
if len(c.state.panel_images) == 0 {
|
|
if len(cfg.fal_api_key) > 0 {
|
|
return "next: generate panels"
|
|
}
|
|
return "next: generate panels local"
|
|
}
|
|
if len(c.state.page_layouts) == 0 {
|
|
return "next: layout auto"
|
|
}
|
|
return "next: export pdf ./comic.pdf"
|
|
}
|
|
|
|
plan_report :: proc(c: ui.App_Controller) -> string {
|
|
script_done := len(c.state.script.pages) > 0
|
|
panels_done := len(c.state.panel_images) > 0
|
|
layout_done := len(c.state.page_layouts) > 0
|
|
export_ready := panels_done && layout_done
|
|
|
|
return fmt.aprintf(
|
|
"Plan\n- [%-3s] 1) Script\n- [%-3s] 2) Panels\n- [%-3s] 3) Layout\n- [%-3s] 4) Export\n%s",
|
|
bool_text(script_done),
|
|
bool_text(panels_done),
|
|
bool_text(layout_done),
|
|
bool_text(export_ready),
|
|
next_action_hint(c),
|
|
)
|
|
}
|
|
|
|
collect_script_panels :: proc(script: core.Comic_Script) -> []core.Panel {
|
|
out: [dynamic]core.Panel
|
|
for p in script.pages {
|
|
for pan in p.panels {
|
|
append(&out, pan)
|
|
}
|
|
}
|
|
return out[:]
|
|
}
|
|
|
|
collect_script_panels_for_page :: proc(script: core.Comic_Script, page_number: int) -> []core.Panel {
|
|
out: [dynamic]core.Panel
|
|
for p in script.pages {
|
|
if p.page_number != page_number {
|
|
continue
|
|
}
|
|
for pan in p.panels {
|
|
append(&out, pan)
|
|
}
|
|
}
|
|
return out[:]
|
|
}
|
|
|
|
parse_generate_script_pages :: proc(input: string) -> (int, bool, shared.App_Error) {
|
|
trimmed := strings.trim_space(input)
|
|
if trimmed == "generate script" {
|
|
return 4, true, shared.ok()
|
|
}
|
|
if !strings.has_prefix(trimmed, "generate script ") || strings.has_prefix(trimmed, "generate script local") {
|
|
return 0, false, shared.ok()
|
|
}
|
|
|
|
raw := strings.trim_space(trimmed[len("generate script "):])
|
|
pages, ok := strconv.parse_int(raw)
|
|
if !ok || pages <= 0 {
|
|
return 0, true, shared.validation_error("generate script pages must be a positive integer")
|
|
}
|
|
return pages, true, shared.ok()
|
|
}
|
|
|
|
parse_generate_script_local_pages :: proc(input: string) -> (int, bool, shared.App_Error) {
|
|
trimmed := strings.trim_space(input)
|
|
if trimmed == "generate script local" {
|
|
return 4, true, shared.ok()
|
|
}
|
|
if !strings.has_prefix(trimmed, "generate script local ") {
|
|
return 0, false, shared.ok()
|
|
}
|
|
|
|
raw := strings.trim_space(trimmed[len("generate script local "):])
|
|
pages, ok := strconv.parse_int(raw)
|
|
if !ok || pages <= 0 {
|
|
return 0, true, shared.validation_error("generate script local pages must be a positive integer")
|
|
}
|
|
return pages, true, shared.ok()
|
|
}
|
|
|
|
local_panel_id_by_index :: proc(i: int) -> string {
|
|
switch i {
|
|
case 0: return "panel_local_001"
|
|
case 1: return "panel_local_002"
|
|
case 2: return "panel_local_003"
|
|
case 3: return "panel_local_004"
|
|
case 4: return "panel_local_005"
|
|
case 5: return "panel_local_006"
|
|
case 6: return "panel_local_007"
|
|
case 7: return "panel_local_008"
|
|
case 8: return "panel_local_009"
|
|
}
|
|
return "panel_local_overflow"
|
|
}
|
|
|
|
build_local_script :: proc(story_idea: string, pages: int) -> core.Comic_Script {
|
|
out_pages: [dynamic]core.Page
|
|
for i in 0..<pages {
|
|
chars_present := make([]string, 1)
|
|
chars_present[0] = "char_001"
|
|
dialogue := make([]core.Dialogue, 1)
|
|
dialogue[0] = core.Dialogue{speaker_id = "char_001", text = "Let's do this.", bubble_type = .Normal, emotion = .Neutral}
|
|
|
|
panel := core.Panel{
|
|
panel_id = local_panel_id_by_index(i),
|
|
panel_number = 1,
|
|
shot_type = .Medium,
|
|
description = story_idea,
|
|
characters_present = chars_present,
|
|
dialogue = dialogue,
|
|
caption = "",
|
|
sound_effects = nil,
|
|
transition_from_previous = .None,
|
|
}
|
|
panels := make([]core.Panel, 1)
|
|
panels[0] = panel
|
|
append(&out_pages, core.Page{page_number = i + 1, layout_type = .Grid, panels = panels})
|
|
}
|
|
|
|
chars := make([]core.Character, 1)
|
|
chars[0] = core.Character{id = "char_001", name = "Protagonist", role = .Protagonist, description = "Main character"}
|
|
return core.Comic_Script{title = "Local Script", synopsis = story_idea, characters = chars, pages = out_pages[:]}
|
|
}
|
|
|
|
parse_generate_panels_page :: proc(input: string) -> (int, bool, shared.App_Error) {
|
|
trimmed := strings.trim_space(input)
|
|
if trimmed == "generate panels" {
|
|
return 0, true, shared.ok()
|
|
}
|
|
if !strings.has_prefix(trimmed, "generate panels page ") || strings.has_prefix(trimmed, "generate panels local") {
|
|
return 0, false, shared.ok()
|
|
}
|
|
|
|
raw := strings.trim_space(trimmed[len("generate panels page "):])
|
|
page, ok := strconv.parse_int(raw)
|
|
if !ok || page <= 0 {
|
|
return 0, true, shared.validation_error("generate panels page must be a positive integer")
|
|
}
|
|
return page, true, shared.ok()
|
|
}
|
|
|
|
parse_generate_panels_local_page :: proc(input: string) -> (int, bool, shared.App_Error) {
|
|
trimmed := strings.trim_space(input)
|
|
if trimmed == "generate panels local" {
|
|
return 0, true, shared.ok()
|
|
}
|
|
if !strings.has_prefix(trimmed, "generate panels local page ") {
|
|
return 0, false, shared.ok()
|
|
}
|
|
|
|
raw := strings.trim_space(trimmed[len("generate panels local page "):])
|
|
page, ok := strconv.parse_int(raw)
|
|
if !ok || page <= 0 {
|
|
return 0, true, shared.validation_error("generate panels local page must be a positive integer")
|
|
}
|
|
return page, true, shared.ok()
|
|
}
|
|
|
|
build_local_panel_images :: proc(panels: []core.Panel) -> (map[string]core.Panel_Image, shared.App_Error) {
|
|
tmp_dir, terr := os.make_directory_temp("", "comic-local-panels-*", context.temp_allocator)
|
|
if terr != nil {
|
|
return nil, shared.new_error(.Generation, "failed to create local panel temp dir", true)
|
|
}
|
|
|
|
images := make(map[string]core.Panel_Image)
|
|
for p, idx in panels {
|
|
name := fmt.aprintf("panel_%03d_%s.png", idx+1, p.panel_id)
|
|
out_path := fmt.aprintf("%s/%s", tmp_dir, name)
|
|
delete(name)
|
|
|
|
// Create a real PNG image using python3
|
|
gen_w := 1024
|
|
gen_h := 1024
|
|
py_script := fmt.aprintf(
|
|
"import struct,zlib,sys;w,h=%d,%d;rows=[]\nfor _ in range(h): rows.append(b'\\x00'+b'\\xff\\xff\\xff'*w)\nraw=b''.join(rows);comp=zlib.compress(raw)\ndef crc(d): return struct.pack('>I',zlib.crc32(d)&0xffffffff)\nf=open(sys.argv[1],'wb')\nf.write(b'\\x89PNG\\r\\n\\x1a\\n')\nihdr_data=struct.pack('>IIBBBBB',w,h,8,2,0,0,0)\nf.write(struct.pack('>I',13)+b'IHDR'+ihdr_data+crc(b'IHDR'+ihdr_data))\nf.write(struct.pack('>I',len(comp))+b'IDAT'+comp+crc(b'IDAT'+comp))\nf.write(struct.pack('>I',0)+b'IEND'+crc(b'IEND'))\nf.close()",
|
|
gen_w, gen_h,
|
|
)
|
|
defer delete(py_script)
|
|
|
|
py_cmd := [4]string{"python3", "-c", py_script, out_path}
|
|
desc := os.Process_Desc{command = py_cmd[:]}
|
|
state, _, stderr, cerr := os.process_exec(desc, context.temp_allocator)
|
|
if cerr != nil || !state.exited || state.exit_code != 0 {
|
|
delete(out_path)
|
|
msg := fmt.aprintf("failed to create panel image: %s", string(stderr))
|
|
return nil, shared.new_error(.Generation, msg, true)
|
|
}
|
|
|
|
url := strings.clone(fmt.aprintf("file://%s", out_path))
|
|
prompt := strings.clone(fmt.aprintf("local panel %d", idx+1))
|
|
images[p.panel_id] = core.Panel_Image{url = url, width = gen_w, height = gen_h, seed = i64(idx + 1), prompt = prompt}
|
|
delete(out_path)
|
|
}
|
|
return images, shared.ok()
|
|
}
|
|
|
|
parse_export_command :: proc(input: string) -> (core.Export_Format, string, bool, shared.App_Error) {
|
|
trimmed := strings.trim_space(input)
|
|
if !strings.has_prefix(trimmed, "export ") {
|
|
return .PDF, "", false, shared.ok()
|
|
}
|
|
|
|
rest := strings.trim_space(trimmed[len("export "):])
|
|
sp := strings.index(rest, " ")
|
|
if sp < 0 {
|
|
return .PDF, "", true, shared.validation_error("usage: export <pdf|png|cbz> <path>")
|
|
}
|
|
|
|
fmt_name := strings.trim_space(rest[:sp])
|
|
out_path := strings.trim_space(rest[sp+1:])
|
|
if len(out_path) == 0 {
|
|
return .PDF, "", true, shared.validation_error("export path is required")
|
|
}
|
|
|
|
switch fmt_name {
|
|
case "pdf": return .PDF, out_path, true, shared.ok()
|
|
case "png": return .PNG, out_path, true, shared.ok()
|
|
case "cbz": return .CBZ, out_path, true, shared.ok()
|
|
}
|
|
|
|
return .PDF, "", true, shared.validation_error("unknown export format")
|
|
}
|
|
|
|
parse_quick_local_command :: proc(input: string) -> (core.Export_Format, string, int, bool, shared.App_Error) {
|
|
trimmed := strings.trim_space(input)
|
|
if !strings.has_prefix(trimmed, "quick local ") || strings.has_prefix(trimmed, "quick local all ") {
|
|
return .PDF, "", 0, false, shared.ok()
|
|
}
|
|
|
|
rest := strings.trim_space(trimmed[len("quick local "):])
|
|
parts, ferr := strings.fields(rest)
|
|
if ferr != nil {
|
|
return .PDF, "", 0, true, shared.new_error(.Validation, "quick local parse failed", true)
|
|
}
|
|
defer delete(parts)
|
|
|
|
if len(parts) < 2 || len(parts) > 3 {
|
|
return .PDF, "", 0, true, shared.validation_error("usage: quick local <pdf|png|cbz> <path> [pages]")
|
|
}
|
|
|
|
fmt_name := parts[0]
|
|
out_path := parts[1]
|
|
if len(out_path) == 0 {
|
|
return .PDF, "", 0, true, shared.validation_error("quick local export path is required")
|
|
}
|
|
|
|
pages := 2
|
|
if len(parts) == 3 {
|
|
v, ok := strconv.parse_int(parts[2])
|
|
if !ok || v <= 0 {
|
|
return .PDF, "", 0, true, shared.validation_error("quick local pages must be a positive integer")
|
|
}
|
|
pages = v
|
|
}
|
|
|
|
switch fmt_name {
|
|
case "pdf": return .PDF, out_path, pages, true, shared.ok()
|
|
case "png": return .PNG, out_path, pages, true, shared.ok()
|
|
case "cbz": return .CBZ, out_path, pages, true, shared.ok()
|
|
}
|
|
|
|
return .PDF, "", 0, true, shared.validation_error("unknown quick local export format")
|
|
}
|
|
|
|
parse_quick_local_all_command :: proc(input: string) -> (string, core.Export_Format, string, int, bool, shared.App_Error) {
|
|
trimmed := strings.trim_space(input)
|
|
if !strings.has_prefix(trimmed, "quick local all ") {
|
|
return "", .PDF, "", 0, false, shared.ok()
|
|
}
|
|
|
|
rest := strings.trim_space(trimmed[len("quick local all "):])
|
|
parts, ferr := strings.fields(rest)
|
|
if ferr != nil {
|
|
return "", .PDF, "", 0, true, shared.new_error(.Validation, "quick local all parse failed", true)
|
|
}
|
|
defer delete(parts)
|
|
|
|
if len(parts) < 3 || len(parts) > 4 {
|
|
return "", .PDF, "", 0, true, shared.validation_error("usage: quick local all <project_path> <pdf|png|cbz> <export_path> [pages]")
|
|
}
|
|
|
|
project_path := parts[0]
|
|
fmt_name := parts[1]
|
|
export_path := parts[2]
|
|
if len(project_path) == 0 || len(export_path) == 0 {
|
|
return "", .PDF, "", 0, true, shared.validation_error("quick local all paths are required")
|
|
}
|
|
|
|
pages := 2
|
|
if len(parts) == 4 {
|
|
v, ok := strconv.parse_int(parts[3])
|
|
if !ok || v <= 0 {
|
|
return "", .PDF, "", 0, true, shared.validation_error("quick local all pages must be a positive integer")
|
|
}
|
|
pages = v
|
|
}
|
|
|
|
switch fmt_name {
|
|
case "pdf": return project_path, .PDF, export_path, pages, true, shared.ok()
|
|
case "png": return project_path, .PNG, export_path, pages, true, shared.ok()
|
|
case "cbz": return project_path, .CBZ, export_path, pages, true, shared.ok()
|
|
}
|
|
|
|
return "", .PDF, "", 0, true, shared.validation_error("unknown quick local all export format")
|
|
}
|
|
|
|
parse_auto_all_command :: proc(input: string) -> (core.Export_Format, string, bool, shared.App_Error) {
|
|
trimmed := strings.trim_space(input)
|
|
if !strings.has_prefix(trimmed, "auto all ") || strings.has_prefix(trimmed, "auto all local ") {
|
|
return .PDF, "", false, shared.ok()
|
|
}
|
|
|
|
rest := strings.trim_space(trimmed[len("auto all "):])
|
|
parts, ferr := strings.fields(rest)
|
|
if ferr != nil {
|
|
return .PDF, "", true, shared.new_error(.Validation, "auto all parse failed", true)
|
|
}
|
|
defer delete(parts)
|
|
|
|
if len(parts) != 2 {
|
|
return .PDF, "", true, shared.validation_error("usage: auto all <pdf|png|cbz> <path>")
|
|
}
|
|
|
|
switch parts[0] {
|
|
case "pdf": return .PDF, parts[1], true, shared.ok()
|
|
case "png": return .PNG, parts[1], true, shared.ok()
|
|
case "cbz": return .CBZ, parts[1], true, shared.ok()
|
|
}
|
|
return .PDF, "", true, shared.validation_error("unknown auto all export format")
|
|
}
|
|
|
|
parse_auto_all_local_command :: proc(input: string) -> (core.Export_Format, string, int, bool, shared.App_Error) {
|
|
trimmed := strings.trim_space(input)
|
|
if !strings.has_prefix(trimmed, "auto all local ") {
|
|
return .PDF, "", 0, false, shared.ok()
|
|
}
|
|
|
|
rest := strings.trim_space(trimmed[len("auto all local "):])
|
|
parts, ferr := strings.fields(rest)
|
|
if ferr != nil {
|
|
return .PDF, "", 0, true, shared.new_error(.Validation, "auto all local parse failed", true)
|
|
}
|
|
defer delete(parts)
|
|
|
|
if len(parts) < 2 || len(parts) > 3 {
|
|
return .PDF, "", 0, true, shared.validation_error("usage: auto all local <pdf|png|cbz> <path> [pages]")
|
|
}
|
|
|
|
pages := 2
|
|
if len(parts) == 3 {
|
|
v, ok := strconv.parse_int(parts[2])
|
|
if !ok || v <= 0 {
|
|
return .PDF, "", 0, true, shared.validation_error("auto all local pages must be a positive integer")
|
|
}
|
|
pages = v
|
|
}
|
|
|
|
switch parts[0] {
|
|
case "pdf": return .PDF, parts[1], pages, true, shared.ok()
|
|
case "png": return .PNG, parts[1], pages, true, shared.ok()
|
|
case "cbz": return .CBZ, parts[1], pages, true, shared.ok()
|
|
}
|
|
return .PDF, "", 0, true, shared.validation_error("unknown auto all local export format")
|
|
}
|
|
|
|
run_quick_local_pipeline :: proc(controller: ^ui.App_Controller, export_format: core.Export_Format, export_path: string, pages: int) -> shared.App_Error {
|
|
story := controller.state.story_idea
|
|
if len(story) == 0 {
|
|
story = "A local adventure"
|
|
}
|
|
|
|
script := build_local_script(story, pages)
|
|
core.dispose_script(&controller.state.script)
|
|
controller.state.script = script
|
|
controller.state.characters = controller.state.script.characters
|
|
|
|
panels := collect_script_panels(controller.state.script)
|
|
defer delete(panels)
|
|
if len(panels) == 0 {
|
|
return shared.validation_error("quick local failed: no panels")
|
|
}
|
|
|
|
images, lerr := build_local_panel_images(panels)
|
|
if !shared.is_ok(lerr) {
|
|
return lerr
|
|
}
|
|
for _, img in controller.state.panel_images {
|
|
delete(img.url)
|
|
delete(img.prompt)
|
|
}
|
|
delete(controller.state.panel_images)
|
|
controller.state.panel_images = images
|
|
|
|
core.dispose_page_layouts(&controller.state.page_layouts)
|
|
controller.state.page_layouts = core.auto_layout_pages(panels, controller.state.page_size, controller.state.story_genre, "")
|
|
|
|
opts := adapters.Export_Options{format = export_format, page_size = controller.state.page_size, dpi = 300, quality = 90}
|
|
err := adapters.export_comic_stub(export_path, controller.state.page_layouts, controller.state.panel_images, opts)
|
|
if !shared.is_ok(err) {
|
|
controller.state.workflow.error_message = err.message
|
|
return err
|
|
}
|
|
controller.state.export_format = export_format
|
|
controller.active_screen = .Export
|
|
controller.state.workflow.current_step = .Complete
|
|
controller.state.workflow.error_message = ""
|
|
return shared.ok()
|
|
}
|
|
|
|
clear_screen :: proc() {
|
|
fmt.print("\x1b[2J\x1b[H")
|
|
}
|
|
|
|
normalize_tui_command :: proc(input: string) -> string {
|
|
cmd := strings.trim_space(input)
|
|
switch cmd {
|
|
case "h": return "help"
|
|
case "s": return "status"
|
|
case "?": return "doctor"
|
|
case "r": return "ready"
|
|
case "n": return "next"
|
|
case "p": return "plan"
|
|
case "x": return "auto"
|
|
case "d": return "done"
|
|
case "c": return "cancel"
|
|
case "q": return "quit"
|
|
case "1": return "goto story"
|
|
case "2": return "goto script"
|
|
case "3": return "goto characters"
|
|
case "4": return "goto panels"
|
|
case "5": return "goto layout"
|
|
case "6": return "goto bubbles"
|
|
case "7": return "goto export"
|
|
case "8": return "goto community"
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
screen_from_name :: proc(s: string) -> (ui.App_Screen, bool) {
|
|
switch s {
|
|
case "story": return .Story, true
|
|
case "script": return .Script, true
|
|
case "characters": return .Characters, true
|
|
case "panels": return .Panels, true
|
|
case "layout": return .Layout, true
|
|
case "bubbles", "speech": return .Bubbles, true
|
|
case "export": return .Export, true
|
|
case "community": return .Community, true
|
|
}
|
|
return .Story, false
|
|
}
|
|
|
|
workflow_from_name :: proc(s: string) -> (core.Workflow_Step, bool) {
|
|
switch s {
|
|
case "story": return .Story_Input, true
|
|
case "generating-script": return .Generating_Script, true
|
|
case "script-review": return .Script_Review, true
|
|
case "character-setup": return .Character_Setup, true
|
|
case "generating-panels": return .Generating_Panels, true
|
|
case "layout": return .Layout, true
|
|
case "speech-bubbles": return .Speech_Bubbles, true
|
|
case "complete": return .Complete, true
|
|
}
|
|
return .Story_Input, false
|
|
}
|
|
|
|
job_type_from_name :: proc(s: string) -> (ui.Job_Type, bool) {
|
|
switch s {
|
|
case "script": return .Generate_Script, true
|
|
case "character": return .Generate_Character, true
|
|
case "panel": return .Generate_Panel, true
|
|
case "export": return .Export, true
|
|
}
|
|
return .Generate_Script, false
|
|
}
|
|
|
|
read_stdin_line :: proc() -> (line: string, ok: bool, err: shared.App_Error) {
|
|
buf: [dynamic]u8
|
|
one: [1]u8
|
|
|
|
for {
|
|
n, rerr := os.read(os.stdin, one[:])
|
|
if rerr != nil {
|
|
if rerr == .EOF {
|
|
if len(buf) == 0 {
|
|
return "", false, shared.ok()
|
|
}
|
|
break
|
|
}
|
|
delete(buf)
|
|
return "", false, shared.new_error(.Config, "stdin read error", true)
|
|
}
|
|
if n == 0 {
|
|
continue
|
|
}
|
|
if one[0] == '\n' {
|
|
break
|
|
}
|
|
if one[0] != '\r' {
|
|
append(&buf, one[0])
|
|
}
|
|
}
|
|
|
|
return string(buf[:]), true, shared.ok()
|
|
}
|
|
|
|
run_tui_command :: proc(controller: ^ui.App_Controller, input: string, last_job_id: ^int) -> (quit: bool, out: string, err: shared.App_Error) {
|
|
if input == "help" {
|
|
return false, fmt.aprintf("%s", tui_help_text()), shared.ok()
|
|
}
|
|
if input == "status" {
|
|
return false, ui.render_app_to_string(controller^), shared.ok()
|
|
}
|
|
if input == "doctor" {
|
|
return false, build_doctor_report(), shared.ok()
|
|
}
|
|
if input == "ready" {
|
|
return false, build_ready_report(controller^), shared.ok()
|
|
}
|
|
if input == "next" {
|
|
return false, fmt.aprintf("%s", next_action_hint(controller^)), shared.ok()
|
|
}
|
|
if input == "plan" {
|
|
return false, plan_report(controller^), shared.ok()
|
|
}
|
|
auto_all_local_format, auto_all_local_path, auto_all_local_pages, is_auto_all_local, aalerr := parse_auto_all_local_command(input)
|
|
if !shared.is_ok(aalerr) {
|
|
return false, "", aalerr
|
|
}
|
|
if is_auto_all_local {
|
|
if err := run_quick_local_pipeline(controller, auto_all_local_format, auto_all_local_path, auto_all_local_pages); !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
return false, fmt.aprintf("auto all local exported %s (%d pages)", auto_all_local_path, auto_all_local_pages), shared.ok()
|
|
}
|
|
|
|
auto_all_format, auto_all_path, is_auto_all, aaerr := parse_auto_all_command(input)
|
|
if !shared.is_ok(aaerr) {
|
|
return false, "", aaerr
|
|
}
|
|
if is_auto_all {
|
|
for i in 0..<6 {
|
|
hint := next_action_hint(controller^)
|
|
if strings.has_prefix(hint, "next: export ") {
|
|
export_cmd := fmt.aprintf("export %s %s", export_format_name(auto_all_format), auto_all_path)
|
|
_, out, eerr := run_tui_command(controller, export_cmd, last_job_id)
|
|
delete(export_cmd)
|
|
if !shared.is_ok(eerr) {
|
|
return false, "", eerr
|
|
}
|
|
if len(out) > 0 { delete(out) }
|
|
return false, fmt.aprintf("auto all exported %s", auto_all_path), shared.ok()
|
|
}
|
|
if !strings.has_prefix(hint, "next: ") {
|
|
return false, "", shared.validation_error("auto all failed: invalid next hint")
|
|
}
|
|
next_cmd := strings.trim_space(hint[len("next: "):])
|
|
_, out, aerr := run_tui_command(controller, next_cmd, last_job_id)
|
|
if len(out) > 0 { delete(out) }
|
|
if !shared.is_ok(aerr) {
|
|
return false, "", aerr
|
|
}
|
|
}
|
|
return false, "", shared.validation_error("auto all exceeded step limit")
|
|
}
|
|
|
|
if input == "auto" {
|
|
hint := next_action_hint(controller^)
|
|
if !strings.has_prefix(hint, "next: ") {
|
|
return false, "", shared.validation_error("auto failed: invalid next hint")
|
|
}
|
|
next_cmd := strings.trim_space(hint[len("next: "):])
|
|
_, out, aerr := run_tui_command(controller, next_cmd, last_job_id)
|
|
if !shared.is_ok(aerr) {
|
|
return false, "", aerr
|
|
}
|
|
if len(out) > 0 {
|
|
msg := fmt.aprintf("auto ran: %s\n%s", next_cmd, out)
|
|
delete(out)
|
|
return false, msg, shared.ok()
|
|
}
|
|
return false, fmt.aprintf("auto ran: %s", next_cmd), shared.ok()
|
|
}
|
|
if input == "done" {
|
|
if last_job_id^ <= 0 {
|
|
return false, "", shared.validation_error("no active job")
|
|
}
|
|
err = ui.finish_background_job(controller, last_job_id^, "")
|
|
if !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
return false, fmt.aprintf("job marked done"), shared.ok()
|
|
}
|
|
if input == "cancel" {
|
|
if last_job_id^ <= 0 {
|
|
return false, "", shared.validation_error("no active job")
|
|
}
|
|
err = ui.cancel_background_job(controller, last_job_id^)
|
|
if !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
return false, fmt.aprintf("job cancelled"), shared.ok()
|
|
}
|
|
if input == "quit" || input == "exit" {
|
|
return true, fmt.aprintf("exiting tui"), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "goto ") {
|
|
target_name := strings.trim_space(input[len("goto "):])
|
|
target, ok := screen_from_name(target_name)
|
|
if !ok {
|
|
return false, "", shared.validation_error("unknown screen")
|
|
}
|
|
err = ui.navigate_to_screen(controller, target)
|
|
if !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
return false, fmt.aprintf("screen: %s", ui.screen_name(target)), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "step ") {
|
|
step_name := strings.trim_space(input[len("step "):])
|
|
next, ok := workflow_from_name(step_name)
|
|
if !ok {
|
|
return false, "", shared.validation_error("unknown workflow step")
|
|
}
|
|
err = ui.set_workflow_step(controller, next)
|
|
if !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
return false, fmt.aprintf("step: %v", next), shared.ok()
|
|
}
|
|
|
|
if strings.trim_space(input) == "new" {
|
|
core.dispose_state(&controller.state)
|
|
controller.state = core.new_initial_state()
|
|
controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
|
|
last_job_id^ = 0
|
|
return false, fmt.aprintf("new project initialized"), shared.ok()
|
|
}
|
|
|
|
local_pages, is_generate_local_script, lserr := parse_generate_script_local_pages(input)
|
|
if !shared.is_ok(lserr) {
|
|
return false, "", lserr
|
|
}
|
|
if is_generate_local_script {
|
|
core.set_workflow_step(&controller.state, .Generating_Script)
|
|
controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
|
|
controller.state.workflow.is_generating = true
|
|
controller.state.workflow.generation_progress = 25
|
|
|
|
script := build_local_script(controller.state.story_idea, local_pages)
|
|
core.dispose_script(&controller.state.script)
|
|
controller.state.script = script
|
|
controller.state.characters = controller.state.script.characters
|
|
core.set_workflow_step(&controller.state, .Script_Review)
|
|
controller.active_screen = .Script
|
|
controller.state.workflow.is_generating = false
|
|
controller.state.workflow.generation_progress = 100
|
|
controller.state.workflow.error_message = ""
|
|
return false, fmt.aprintf("local script generated (%d pages)", local_pages), shared.ok()
|
|
}
|
|
|
|
script_pages, is_generate_script, pserr := parse_generate_script_pages(input)
|
|
if !shared.is_ok(pserr) {
|
|
return false, "", pserr
|
|
}
|
|
if is_generate_script {
|
|
cfg := shared.load_config()
|
|
core.set_workflow_step(&controller.state, .Generating_Script)
|
|
controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
|
|
controller.state.workflow.is_generating = true
|
|
controller.state.workflow.generation_progress = 25
|
|
|
|
opts := adapters.Generate_Script_Options{
|
|
story_idea = controller.state.story_idea,
|
|
genre = controller.state.story_genre,
|
|
art_style = controller.state.art_style,
|
|
num_pages = script_pages,
|
|
audience = controller.state.target_audience,
|
|
}
|
|
script, gerr := adapters.generate_comic_script_stub(cfg, opts)
|
|
if !shared.is_ok(gerr) {
|
|
controller.state.workflow.is_generating = false
|
|
controller.state.workflow.error_message = gerr.message
|
|
controller.state.workflow.current_step = .Story_Input
|
|
controller.active_screen = .Story
|
|
return false, "", gerr
|
|
}
|
|
|
|
core.dispose_script(&controller.state.script)
|
|
controller.state.script = script
|
|
controller.state.characters = controller.state.script.characters
|
|
core.set_workflow_step(&controller.state, .Script_Review)
|
|
controller.active_screen = .Script
|
|
controller.state.workflow.is_generating = false
|
|
controller.state.workflow.generation_progress = 100
|
|
controller.state.workflow.error_message = ""
|
|
return false, fmt.aprintf("script generated (%d pages)", script_pages), shared.ok()
|
|
}
|
|
|
|
panel_local_page, is_generate_panels_local, plerr := parse_generate_panels_local_page(input)
|
|
if !shared.is_ok(plerr) {
|
|
return false, "", plerr
|
|
}
|
|
if is_generate_panels_local {
|
|
panels: []core.Panel
|
|
if panel_local_page > 0 {
|
|
panels = collect_script_panels_for_page(controller.state.script, panel_local_page)
|
|
} else {
|
|
panels = collect_script_panels(controller.state.script)
|
|
}
|
|
defer delete(panels)
|
|
if len(panels) == 0 {
|
|
return false, "", shared.validation_error("no script panels available")
|
|
}
|
|
|
|
core.set_workflow_step(&controller.state, .Generating_Panels)
|
|
controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
|
|
controller.state.workflow.is_generating = true
|
|
controller.state.workflow.generation_progress = 35
|
|
|
|
images, gerr := build_local_panel_images(panels)
|
|
if !shared.is_ok(gerr) {
|
|
controller.state.workflow.is_generating = false
|
|
controller.state.workflow.error_message = gerr.message
|
|
return false, "", gerr
|
|
}
|
|
|
|
for _, img in controller.state.panel_images {
|
|
delete(img.url)
|
|
delete(img.prompt)
|
|
}
|
|
delete(controller.state.panel_images)
|
|
controller.state.panel_images = images
|
|
core.set_workflow_step(&controller.state, .Layout)
|
|
controller.active_screen = .Layout
|
|
controller.state.workflow.is_generating = false
|
|
controller.state.workflow.generation_progress = 100
|
|
controller.state.workflow.error_message = ""
|
|
if panel_local_page > 0 {
|
|
return false, fmt.aprintf("local panel images generated for page %d (%d)", panel_local_page, len(images)), shared.ok()
|
|
}
|
|
return false, fmt.aprintf("local panel images generated (%d)", len(images)), shared.ok()
|
|
}
|
|
|
|
panel_page, is_generate_panels, pperr := parse_generate_panels_page(input)
|
|
if !shared.is_ok(pperr) {
|
|
return false, "", pperr
|
|
}
|
|
if is_generate_panels {
|
|
cfg := shared.load_config()
|
|
panels: []core.Panel
|
|
if panel_page > 0 {
|
|
panels = collect_script_panels_for_page(controller.state.script, panel_page)
|
|
} else {
|
|
panels = collect_script_panels(controller.state.script)
|
|
}
|
|
defer delete(panels)
|
|
if len(panels) == 0 {
|
|
return false, "", shared.validation_error("no script panels available")
|
|
}
|
|
|
|
core.set_workflow_step(&controller.state, .Generating_Panels)
|
|
controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
|
|
controller.state.workflow.is_generating = true
|
|
controller.state.workflow.generation_progress = 35
|
|
|
|
q := adapters.new_fal_queue(2)
|
|
client := adapters.new_fal_client(&q)
|
|
images, gerr := adapters.generate_all_panels_batched(client, cfg, panels, controller.state.characters, controller.state.art_style, controller.state.project.project_id, controller.state.story_genre, controller.state.target_audience, nil)
|
|
if !shared.is_ok(gerr) {
|
|
controller.state.workflow.is_generating = false
|
|
controller.state.workflow.error_message = gerr.message
|
|
return false, "", gerr
|
|
}
|
|
|
|
for _, img in controller.state.panel_images {
|
|
delete(img.url)
|
|
delete(img.prompt)
|
|
}
|
|
delete(controller.state.panel_images)
|
|
controller.state.panel_images = images
|
|
core.set_workflow_step(&controller.state, .Layout)
|
|
controller.active_screen = .Layout
|
|
controller.state.workflow.is_generating = false
|
|
controller.state.workflow.generation_progress = 100
|
|
controller.state.workflow.error_message = ""
|
|
if panel_page > 0 {
|
|
return false, fmt.aprintf("panel images generated for page %d (%d)", panel_page, len(images)), shared.ok()
|
|
}
|
|
return false, fmt.aprintf("panel images generated (%d)", len(images)), shared.ok()
|
|
}
|
|
|
|
if strings.trim_space(input) == "layout auto" {
|
|
panels := collect_script_panels(controller.state.script)
|
|
defer delete(panels)
|
|
if len(panels) == 0 {
|
|
return false, "", shared.validation_error("no script panels available")
|
|
}
|
|
core.dispose_page_layouts(&controller.state.page_layouts)
|
|
controller.state.page_layouts = core.auto_layout_pages(panels, controller.state.page_size, controller.state.story_genre, "")
|
|
controller.state.workflow.current_step = .Layout
|
|
controller.active_screen = .Layout
|
|
return false, fmt.aprintf("layout generated (%d pages)", len(controller.state.page_layouts)), shared.ok()
|
|
}
|
|
|
|
project_path, quick_all_format, quick_all_export_path, quick_all_pages, is_quick_local_all, qaerr := parse_quick_local_all_command(input)
|
|
if !shared.is_ok(qaerr) {
|
|
return false, "", qaerr
|
|
}
|
|
if is_quick_local_all {
|
|
if err := run_quick_local_pipeline(controller, quick_all_format, quick_all_export_path, quick_all_pages); !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
err = adapters.save_project(project_path, controller.state)
|
|
if !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
return false, fmt.aprintf("quick local all saved %s and exported %s (%d pages)", project_path, quick_all_export_path, quick_all_pages), shared.ok()
|
|
}
|
|
|
|
quick_format, quick_path, quick_pages, is_quick_local, qerr := parse_quick_local_command(input)
|
|
if !shared.is_ok(qerr) {
|
|
return false, "", qerr
|
|
}
|
|
if is_quick_local {
|
|
if err := run_quick_local_pipeline(controller, quick_format, quick_path, quick_pages); !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
return false, fmt.aprintf("quick local exported %s (%d pages)", quick_path, quick_pages), shared.ok()
|
|
}
|
|
|
|
export_format, export_path, is_export, exerr := parse_export_command(input)
|
|
if !shared.is_ok(exerr) {
|
|
return false, "", exerr
|
|
}
|
|
if is_export {
|
|
opts := adapters.Export_Options{format = export_format, page_size = controller.state.page_size, dpi = 300, quality = 90}
|
|
err = adapters.export_comic_stub(export_path, controller.state.page_layouts, controller.state.panel_images, opts)
|
|
if !shared.is_ok(err) {
|
|
controller.state.workflow.error_message = err.message
|
|
return false, "", err
|
|
}
|
|
controller.state.export_format = export_format
|
|
controller.active_screen = .Export
|
|
controller.state.workflow.current_step = .Complete
|
|
controller.state.workflow.error_message = ""
|
|
return false, fmt.aprintf("exported %s", export_path), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "set idea ") {
|
|
v := strings.trim_space(input[len("set idea "):])
|
|
controller.state.story_idea = fmt.aprintf("%s", v)
|
|
return false, fmt.aprintf("story idea updated"), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "set genre ") {
|
|
v := strings.trim_space(input[len("set genre "):])
|
|
controller.state.story_genre = fmt.aprintf("%s", v)
|
|
return false, fmt.aprintf("story genre updated"), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "set audience ") {
|
|
v := strings.trim_space(input[len("set audience "):])
|
|
controller.state.target_audience = fmt.aprintf("%s", v)
|
|
return false, fmt.aprintf("target audience updated"), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "saveas ") {
|
|
path := strings.trim_space(input[len("saveas "):])
|
|
err = adapters.save_project(path, controller.state)
|
|
if !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
return false, fmt.aprintf("saved to %s", path), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "save ") {
|
|
path := strings.trim_space(input[len("save "):])
|
|
err = adapters.save_project(path, controller.state)
|
|
if !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
return false, fmt.aprintf("saved to %s", path), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "open ") {
|
|
path := strings.trim_space(input[len("open "):])
|
|
loaded, lerr := adapters.load_project(path)
|
|
if !shared.is_ok(lerr) {
|
|
return false, "", lerr
|
|
}
|
|
core.dispose_state(&controller.state)
|
|
controller.state = loaded
|
|
controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
|
|
return false, fmt.aprintf("loaded from %s", path), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "load ") {
|
|
path := strings.trim_space(input[len("load "):])
|
|
loaded, lerr := adapters.load_project(path)
|
|
if !shared.is_ok(lerr) {
|
|
return false, "", lerr
|
|
}
|
|
core.dispose_state(&controller.state)
|
|
controller.state = loaded
|
|
controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
|
|
return false, fmt.aprintf("loaded from %s", path), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "start ") {
|
|
name := strings.trim_space(input[len("start "):])
|
|
job_type, ok := job_type_from_name(name)
|
|
if !ok {
|
|
return false, "", shared.validation_error("unknown job type")
|
|
}
|
|
id := ui.start_background_job(controller, job_type, "job")
|
|
_ = ui.mark_job_running(&controller.jobs, id)
|
|
last_job_id^ = id
|
|
return false, fmt.aprintf("started job %d", id), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "progress ") {
|
|
v := strings.trim_space(input[len("progress "):])
|
|
n, ok := strconv.parse_int(v)
|
|
if !ok {
|
|
return false, "", shared.validation_error("invalid progress")
|
|
}
|
|
if n < 0 || n > 100 {
|
|
return false, "", shared.validation_error("progress must be 0..100")
|
|
}
|
|
ui.set_generation_progress(controller, f32(n))
|
|
controller.state.workflow.is_generating = true
|
|
return false, fmt.aprintf("progress set to %d", n), shared.ok()
|
|
}
|
|
|
|
if strings.has_prefix(input, "fail ") {
|
|
if last_job_id^ <= 0 {
|
|
return false, "", shared.validation_error("no active job")
|
|
}
|
|
msg := strings.trim_space(input[len("fail "):])
|
|
if len(msg) == 0 {
|
|
msg = "job failed"
|
|
}
|
|
err = ui.finish_background_job(controller, last_job_id^, msg)
|
|
if !shared.is_ok(err) {
|
|
return false, "", err
|
|
}
|
|
return false, fmt.aprintf("job marked failed"), shared.ok()
|
|
}
|
|
|
|
return false, "", shared.validation_error("unknown command")
|
|
}
|
|
|
|
run_tui_loop :: proc(state: ^core.Comic_State) -> (string, shared.App_Error) {
|
|
controller := ui.new_controller(state^)
|
|
defer ui.dispose_job_manager(&controller.jobs)
|
|
|
|
last_job_id := 0
|
|
|
|
for {
|
|
clear_screen()
|
|
fmt.println("comic-odin interactive tui")
|
|
fmt.println("screens: [1]Story [2]Script [3]Characters [4]Panels [5]Layout [6]Bubbles [7]Export [8]Community")
|
|
fmt.println("shortcuts: h help | s status | ? doctor | r ready | n next | p plan | x auto | d done | c cancel | q quit")
|
|
fmt.println()
|
|
view := ui.render_app_to_string(controller)
|
|
fmt.println(view)
|
|
delete(view)
|
|
fmt.print("\ntui> ")
|
|
|
|
line, ok, rerr := read_stdin_line()
|
|
if !shared.is_ok(rerr) {
|
|
return "", rerr
|
|
}
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
cmd := normalize_tui_command(line)
|
|
if len(cmd) == 0 {
|
|
delete(line)
|
|
continue
|
|
}
|
|
|
|
quit, out, cerr := run_tui_command(&controller, cmd, &last_job_id)
|
|
delete(line)
|
|
if !shared.is_ok(cerr) {
|
|
fmt.printf("error: %s\n", cerr.message)
|
|
} else if len(out) > 0 {
|
|
fmt.println(out)
|
|
delete(out)
|
|
}
|
|
if quit {
|
|
break
|
|
}
|
|
}
|
|
|
|
core.dispose_state(state)
|
|
state^ = controller.state
|
|
controller.state = core.Comic_State{}
|
|
return "", shared.ok()
|
|
}
|
|
|
|
run_cli_command :: proc(cmd: Parsed_CLI_Command, state: ^core.Comic_State) -> (string, shared.App_Error) {
|
|
switch cmd.kind {
|
|
case .Demo:
|
|
controller := ui.new_controller(state^)
|
|
defer ui.dispose_controller(&controller)
|
|
job_id := ui.start_background_job(&controller, .Generate_Script, "Generating script")
|
|
_ = ui.mark_job_running(&controller.jobs, job_id)
|
|
ui.set_generation_progress(&controller, 35)
|
|
_ = ui.finish_background_job(&controller, job_id, "")
|
|
return ui.render_app_to_string(controller), shared.ok()
|
|
case .Status:
|
|
controller := ui.new_controller(state^)
|
|
defer ui.dispose_controller(&controller)
|
|
return ui.render_app_to_string(controller), shared.ok()
|
|
case .Save:
|
|
if len(cmd.path) == 0 {
|
|
return usage_text(), shared.validation_error("missing save path")
|
|
}
|
|
err := adapters.save_project(cmd.path, state^)
|
|
if !shared.is_ok(err) {
|
|
return "", err
|
|
}
|
|
return fmt.aprintf("Saved project to %s", cmd.path), shared.ok()
|
|
case .Load:
|
|
if len(cmd.path) == 0 {
|
|
return usage_text(), shared.validation_error("missing load path")
|
|
}
|
|
loaded, err := adapters.load_project(cmd.path)
|
|
if !shared.is_ok(err) {
|
|
return "", err
|
|
}
|
|
core.dispose_state(state)
|
|
state^ = loaded
|
|
return fmt.aprintf("Loaded project from %s", cmd.path), shared.ok()
|
|
case .Tui:
|
|
return run_tui_loop(state)
|
|
case .Gui:
|
|
err := gui.run_gui_app(state)
|
|
if !shared.is_ok(err) {
|
|
return "", err
|
|
}
|
|
return fmt.aprintf("GUI session ended"), shared.ok()
|
|
case .Help:
|
|
fallthrough
|
|
case:
|
|
return usage_text(), shared.ok()
|
|
}
|
|
}
|
|
|
|
run_cli_from_process_args :: proc(state: ^core.Comic_State) -> (string, shared.App_Error) {
|
|
if len(os.args) <= 1 {
|
|
return run_cli_command(Parsed_CLI_Command{kind = .Demo}, state)
|
|
}
|
|
cmd := parse_cli_command(os.args[1:])
|
|
return run_cli_command(cmd, state)
|
|
}
|