package main import "core:fmt" import "core:os" import "core:os/os2" import "core:path/filepath" import "core:sys/windows" import "core:strings" /* TODO: Drag-n-drop window TODO: Testing with 8-series */ VERBOSE :: false PART_ONE :: #load("parts/start.html", string) PART_TWO :: #load("parts/start2.html", string) PART_END :: #load("parts/end.html", string) Device :: enum { UNSET, ZOOM, SD6, } Stages :: enum { TITLE, INFO, HEADER, BODY, } Info_Line :: struct { field : string, entry : string, } Report :: struct { title : string, info_lines : []Info_Line, header : []string, table : [][]string, column_count : int, row_count : int, info_line_count : int, tc_column_index : int, } main :: proc() { when ODIN_OS == .Windows { windows.SetConsoleOutputCP(windows.CODEPAGE.UTF8) } input_file_name : string if len(os.args) < 2 { fmt.println("ERROR: No file submitted.") return } else { fmt.printf("Input path: {}\n", os.args[1]) input_file_name = os.args[1] } path_info, error := os.stat(input_file_name) file_count := 1 files_done := 0 if error == os.ERROR_NONE { file_list : [dynamic]string if(path_info.is_dir) { fmt.printf("Directory submitted! Walking directory...\n\n") fmt.printf("šŸ“ {}\n", path_info.name) try_os2 := walk_directory(path_info.fullpath, &file_count, &file_list, 1) if len(file_list) == 0 && try_os2 { fmt.printf("\nNot_Dir error encountered. Trying os2 version...\n\n") fmt.printf("šŸ“ {}\n", path_info.name) walk_directory_os2(path_info.fullpath, &file_count, &file_list, 1) } } else { fmt.println("File submitted! Processing file...") append(&file_list, strings.clone(path_info.fullpath)) } for file, f in file_list { file_info, _ := os.stat(file) fmt.printf("\nšŸ“„ File {}: {}\n", f+1, file_info.name) parsed, ok_parse := parse(file_info.fullpath) if !ok_parse { fmt.printf("Parse failed: {}\n", file_info.fullpath) continue } output_name := fmt.aprintf("{}/{}_Knekt_rapport.html", filepath.dir(file_info.fullpath), parsed.title, allocator=context.temp_allocator) render(parsed, output_name) free_all(context.temp_allocator) files_done += 1 } fmt.printf("\nCompleted {}/{} files.\n\n", files_done, len(file_list)) } else { fmt.printf("ERROR could not get path info for: {}\n", input_file_name) } } parse :: proc(path : string, device : Device = .UNSET) -> (Report, bool) { device := device output : Report = {} data, ok := os.read_entire_file(path, context.temp_allocator) if !ok { fmt.printf("ERROR: Could not read file: {}\n", path) return {}, false } file_info, _ := os.lstat(path, context.temp_allocator) lines := strings.split_lines(string(data), allocator=context.temp_allocator) // STAGE 1 -------------------------------------------------------------- // First, we detect what kind of sound report this is for line, line_number in lines { if (device!=.UNSET) { break } if line == "\"SOUND REPORT\"," { device = .ZOOM if VERBOSE do fmt.printf("Detected ZOOM from quotes and comma on line index {}\n", line_number) } if line == "\"ZOOM F8\"," { device = .ZOOM if VERBOSE do fmt.printf("Detected ZOOM from \"ZOOM F8\" on line index {}\n", line_number) } if line == "SOUND REPORT" { device = .SD6 if VERBOSE do fmt.printf("Detected SOUND_DEVICES from unquoted SOUND REPORT line index {}\n", line_number) } } if device == .UNSET { fmt.printf("ERROR: Unable to detect sound report type!\n") return {}, false } // STAGE 2 -------------------------------------------------------------- // Measuring content for allocation if device == .ZOOM { output.column_count = 21 // Ugly magic number, could be fucked by firmware update // Padded for expanding info lines from unchanging columns output.info_lines = make([]Info_Line, 2+output.column_count, context.temp_allocator) output.info_line_count = 2 output.row_count = strings.count(string(data), "\n") - 7 // Ugly magic number, could be fucked by firmware update output.table = make([][]string, output.row_count, context.temp_allocator) output.header = make([]string, output.column_count, context.temp_allocator) for &row in output.table { row = make([]string, output.column_count, context.temp_allocator) } } else if device == .SD6 { second_to_last_line := lines[len(lines)-2] output.column_count = strings.count(second_to_last_line, ",") count_stage : Stages = .TITLE for line, l in lines { switch count_stage { case .TITLE: if l == 1 { // Ugly magic number, could be fucked by firmware update count_stage = .INFO } case .INFO: if line == "," { count_stage = .HEADER continue } else if len(line) > 2 { output.info_line_count += 1 } case .HEADER: if line == "" { count_stage = .BODY } case .BODY: if len(line)>2 { output.row_count += 1 } } } output.info_lines = make([]Info_Line, output.info_line_count+output.column_count, context.temp_allocator) output.header = make([]string, output.column_count, context.temp_allocator) output.table = make([][]string, output.row_count, context.temp_allocator) for &row in output.table { row = make([]string, output.column_count, context.temp_allocator) } } // STAGE 3 -------------------------------------------------------------- // Filling with data if VERBOSE do fmt.printf("Struct before main parse:\n%#v\n", output) first_channel_index := -1 last_channel_index := -1 stage : Stages = .TITLE #partial switch device { case .SD6: // .d8888b. 8888888b. .d8888b. // d88P Y88b 888 "Y88b d88P Y88b // Y88b. 888 888 888 // "Y888b. 888 888 888d888b. // "Y88b. 888 888 888P "Y88b // "888 888 888 888 888 // Y88b d88P 888 .d88P Y88b d88P // "Y8888P" 8888888P" "Y8888P" fmt.printf("Parsing [{}] as Sound Devices 6XX report, ", file_info.name) if file_info.name[len(file_info.name)-11:len(file_info.name)-3] == "_Report." { output.title = file_info.name[:len(file_info.name)-11] } else { output.title = file_info.name } fmt.printf("titled \"{}\".\n", output.title) info_line_index := 0 body_line_index := 0 for line, line_index in lines { switch stage { case .TITLE: if line_index == 1 { // Ugly magic number, could be fucked by firmware update stage = .INFO } case .INFO: if line == "," { stage = .HEADER continue } line_elements := strings.split(line, ",") if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) field := line_elements[0] entry_raw := line_elements[1] entry := line_elements[1][1:len(entry_raw)-1] output.info_lines[info_line_index].field = field output.info_lines[info_line_index].entry = entry info_line_index += 1 case .HEADER: if line == "," { // This is here because there are a bunch of lines that are just commas before the header } else if len(line)>3 { if VERBOSE do fmt.printf(".HEADER {}:", line_index) // No trailing comma in the header?? for element, e in strings.split(line, ",") { if VERBOSE do fmt.printf(" {}", element) output.header[e] = element if element[:4] == "Trk " { if first_channel_index == -1 do first_channel_index = e last_channel_index = e } if element == "Start TC" { output.tc_column_index = e } } if VERBOSE do fmt.printf("\n") } else if line == "" { stage = .BODY if VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) if VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) } case .BODY: if len(line) > 2 { if VERBOSE do fmt.printf(".BODY {}:", line_index) // to skip empty entry after trailing comma we do a silly slice for element, e in strings.split(line, ",")[:output.column_count] { if VERBOSE do fmt.printf(" {}", element) entry : string = element // Stripping quotes if after tracks begin if e >= first_channel_index && (len(element)>0) { entry = element[1:len(element)-1] } output.table[body_line_index][e] = entry } if VERBOSE do fmt.printf("\n") body_line_index += 1 } } } case .ZOOM: // 8888888888 .d8888b. // 888 d88P Y88b // 888 Y88b. d88P // 8888888 "Y88888" // 888 .d8P""Y8b. // 888 888 888 // 888 Y88b d88P // 888 "Y8888P" fmt.printf("Parsing [{}] as ZOOM report, ", file_info.name) // Getting title if file_info.name[:8] == "F8n Pro_" { output.title = file_info.name[8:len(file_info.name)-4] } else if file_info.name[:4] == "F8n_" { // I don't own one, so I don't know if this is what the F8n does output.title = file_info.name[4:len(file_info.name)-4] } else if file_info.name[:3] == "F8_" { // TODO: Verify this is what the original F8 does output.title = file_info.name[4:len(file_info.name)-4] } else { output.title = file_info.name } fmt.printf("titled \"{}\".\n", output.title) info_line_index := 0 body_line_index := 0 for line, line_index in lines { switch stage { case .TITLE: if line_index == 1 { // Ugly magic number, could be fucked by firmware update stage = .INFO } case .INFO: if line == "" { stage = .HEADER continue } line_elements := strings.split(line, ",") if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) field_raw := line_elements[0] entry_raw := line_elements[1] field := line_elements[0][1:len(field_raw)-1] entry := line_elements[1][1:len(entry_raw)-1] output.info_lines[info_line_index].field = field output.info_lines[info_line_index].entry = entry info_line_index += 1 case .HEADER: if VERBOSE do fmt.printf(".HEADER {}:", line_index) // to skip empty entry after trailing comma we do a silly slice for element, e in strings.split(line, ",")[:output.column_count] { if VERBOSE do fmt.printf(" {}", element) output.header[e] = element[1:len(element)-1] if element[:4] == "\"Tr " { if first_channel_index==-1 do first_channel_index = e last_channel_index = e output.header[e] = fmt.aprintf("Trk {}", e-first_channel_index+1, allocator=context.temp_allocator) } if element == "\"Start TC\"" { output.tc_column_index = e } } if VERBOSE do fmt.printf("\n") stage = .BODY if VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) if VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) case .BODY: if line == "" do break if VERBOSE do fmt.printf(".BODY {}:", line_index) // to skip empty entry after trailing comma we do a silly slice for element, e in strings.split(line, ",")[:output.column_count] { if VERBOSE do fmt.printf(" {}", element) output.table[body_line_index][e] = element[1:len(element)-1] } if VERBOSE do fmt.printf("\n") body_line_index += 1 } } } // STAGE 4 -------------------------------------------------------------- // Cleanup! if VERBOSE do fmt.printf("Struct before cleanup:\n%#v\n", output) // Stacking tracks to the left for &line, l in output.table { stacking_index := first_channel_index for &field, f in line[first_channel_index:last_channel_index+1] { if field != "" { line[stacking_index] = field stacking_index += 1 } } for &field, f in line[stacking_index:last_channel_index+1] { field = "" } } // Cleaning out unused columns touched := make([]bool, output.column_count, context.temp_allocator) // Finding them for line, l in output.table { for field, f in line { if touched[f] do continue if field != "" { touched[f] = true } } } // Turning unchanging columns into info line changed := make([]bool, output.column_count, context.temp_allocator) prev_line : []string = nil for line, l in output.table { if l>0 { prev_line = output.table[l - 1] for field, f in line { if (prev_line[f] != field) || (first_channel_index <= f && f <= last_channel_index) || (f == output.tc_column_index) { changed[f] = true } } } } for did_change, i in changed { if (!did_change) && touched[i] { field := fmt.aprintf("{}: ", output.header[i], allocator=context.temp_allocator) entry := prev_line[i] output.info_lines[output.info_line_count] = {field=field, entry=entry} output.info_line_count += 1 } } // Removing unused and static for &line, l in output.table { stacking_index := 0 for &field, f in line { if touched[f] && changed[f] { line[stacking_index] = field stacking_index += 1 } } for &field, f in line[stacking_index:] { field = "" } } stacking_index := 0 for &field, f in output.header { if touched[f] && changed[f] { output.header[stacking_index] = field stacking_index += 1 } } for &field, f in output.header[stacking_index:] { field = "" } output.column_count = stacking_index if VERBOSE do fmt.printf("Struct before output:\n%#v\n", output) return output, true } render :: proc(report : Report, path : string) { // Now we output the HTML. builder := strings.builder_make(context.temp_allocator) strings.write_string(&builder, PART_ONE) strings.write_string(&builder, report.title) strings.write_string(&builder, " - Lydrapport") strings.write_string(&builder, PART_TWO) for line, l in report.info_lines[:report.info_line_count] { strings.write_string(&builder, "

") strings.write_string(&builder, line.field) strings.write_string(&builder, " ") strings.write_string(&builder, line.entry) strings.write_string(&builder, "

\n") } strings.write_string(&builder, " \n \n \n \n \n") for field, f in report.header[:report.column_count] { if f != report.tc_column_index { strings.write_string(&builder, " \n") } strings.write_string(&builder, " \n \n \n") for line, l in report.table { strings.write_string(&builder, " \n") for field, f in line[:report.column_count] { strings.write_string(&builder, " \n") } strings.write_string(&builder, " \n") } strings.write_string(&builder, PART_END) output_text := strings.to_string(builder) os.write_entire_file(path, transmute([]u8)output_text) fmt.printf("Output: {}\n", path) } indent_by :: proc(i : int) { for x in 0.. bool { handle, ok := os.open(path) if ok != os.ERROR_NONE { indent_by(depth) fmt.printf("ERROR opening dir: %s\n", path) return false } defer os.close(handle) files, okr := os.read_dir(handle, -1) if okr != os.ERROR_NONE { indent_by(depth) fmt.printf("ERROR [{}] reading dir: %s\n", okr, path) if okr == os.ERROR_FILE_IS_NOT_DIR do return true return true } defer delete(files) for file in files { full_path := file.fullpath if file.is_dir { indent_by(depth) fmt.printf("šŸ“ %s\n", file.name) return walk_directory(full_path, file_number, file_list, depth+1) // Recurse } else { // If file is actually a file extension := strings.to_lower(filepath.ext(file.name)) defer delete(extension) if(extension == ".csv"){ indent_by(depth) fmt.printf("šŸ“„ %d %s\n", file_number^, file.name) append(file_list, strings.clone(file.fullpath)) file_number^ += 1 } } } return false } walk_directory_os2 :: proc(path : string, file_number : ^int, file_list : ^[dynamic]string, depth : int = 0) { handle, ok := os2.open(path) if ok != os2.ERROR_NONE { indent_by(depth) fmt.printf("ERROR opening dir: %s\n", path) return } defer os2.close(handle) files, okr := os2.read_dir(handle, -1, context.temp_allocator) if okr != os2.ERROR_NONE { indent_by(depth) fmt.printf("ERROR [{}] reading dir: %s\n", okr, path) return } for file in files { full_path := file.fullpath if os.is_dir(full_path) { indent_by(depth) fmt.printf("šŸ“ %s\n", file.name) walk_directory_os2(full_path, file_number, file_list, depth+1) // Recurse } else { // If file is actually a file extension := strings.to_lower(filepath.ext(file.name)) defer delete(extension) if(extension == ".csv"){ indent_by(depth) fmt.printf("šŸ“„ %d %s\n", file_number^, file.name) append(file_list, strings.clone(file.fullpath)) file_number^ += 1 } } } }
") } else { strings.write_string(&builder, " ") } strings.write_string(&builder, field) strings.write_string(&builder, "
") strings.write_string(&builder, field) strings.write_string(&builder, "