// SPDX-FileCopyrightText: Sam Nystrom // SPDX-License-Identifier: GPL-3.0-only use bufio; use encoding::utf8; use getopt; use io; use fmt; use fs; use os; use strconv; use strings; use time; use unix::poll; use unix::tty; type segment = struct { index: uint, start: time::duration, end: time::duration, text: str, }; export fn main() void = { let help: []getopt::help = [ "Play a .srt subtitle file", "", ]; let cmd = getopt::parse(os::args, help...); defer getopt::finish(&cmd); if (len(cmd.args) != 1) { getopt::printusage(os::stderr, os::args[0], help)!; os::exit(1); }; let path = cmd.args[0]; let mode = os::stat(path)!.mode; if (mode & fs::mode::DIR != 0) { fmt::fatalf("Error: '{}' is a directory\n", path); }; let file = match (os::open(path)) { case let f: io::file => yield f; case let err: fs::error => fmt::fatalf("Error reading '{}': {}\n", path, fs::strerror(err)); }; let segments = parse_srt(file); defer free(segments); defer for (let i = 0z; i < len(segments); i += 1) { free(segments[i].text); }; fmt::fprint(os::stdout_file, "\x1b[?25l")!; defer fmt::fprint(os::stdout_file, "\x1b[?25h")!; let termios = tty::termios_query(os::stdin_file)!; defer tty::termios_restore(&termios); tty::makeraw(&termios)!; let pollfds = [ poll::pollfd { fd = os::stdin_file, events = poll::event::POLLIN, ... }, ]; let end: time::duration = 0; for (let i = 0z; i < len(segments); i += 1) { if (segments[i].end > end) { end = segments[i].end; }; }; let start = time::now(time::clock::REALTIME); let elapsed: time::duration = 0; let pause_start: (time::instant | void) = void; for (true) { let now = time::now(time::clock::REALTIME); match (pause_start) { case void => elapsed = time::diff(start, now); case let inst: time::instant => elapsed = time::diff(start, inst); }; if (elapsed > end) break; fmt::printfln("\x1b[2J\x1b[;H{:02}:{:02}:{:02}{}\r", elapsed / time::HOUR, elapsed / time::MINUTE % 60, elapsed / time::SECOND % 60, if (pause_start is time::instant) " (PAUSED)" else "", )!; let timeout = end; for (let i = 0z; i < len(segments); i += 1) { let seg = segments[i]; if (seg.start > elapsed && seg.start < timeout) { timeout = seg.start; }; if (seg.end > elapsed && seg.end < timeout) { timeout = seg.end; }; if (seg.start <= elapsed && elapsed <= seg.end) { fmt::println(seg.text)!; }; }; timeout -= elapsed; let next_second = time::SECOND - elapsed % time::SECOND; if (timeout > next_second) { timeout = next_second; }; if (pause_start is time::instant) { timeout = poll::INDEF; }; match (poll::poll(pollfds, timeout)) { case uint => void; case let err: poll::error => fmt::errorln("Error polling for user input:", poll::strerror(err))!; }; if (pollfds[0].revents & poll::event::POLLIN == 0) { continue; }; static let buf: [os::BUFSIZ]u8 = [0...]; let input = match (io::read(os::stdin_file, buf)) { case let x: size => yield buf[..x]; case io::EOF => break; case let err: io::error => fmt::errorln("Error reading user input:", io::strerror(err))!; continue; }; let quit = false; for (let i = 0z; i < len(input); i += 1) { switch (input[i]) { case 'q' => quit = true; case 'j' => start = time::add(start, time::SECOND * 10); case 'l' => start = time::add(start, -time::SECOND * 10); case 'k' => match (pause_start) { case void => pause_start = now; case let inst: time::instant => pause_start = void; start = time::add(start, time::diff(inst, now)); }; case => void; }; }; if (quit) break; }; }; type state = enum { INDEX, TIMECODE, TEXT, }; fn parse_srt(file: io::handle) []segment = { let segments: []segment = []; let current = segment { ... }; let state = state::INDEX; for (let nr = 0; true; nr += 1) { let line = match (bufio::scanline(file)!) { case let line: []u8 => yield match (strings::fromutf8(line)) { case let line: str => yield line; case utf8::invalid => fmt::fatal("Error: invalid UTF-8"); }; case io::EOF => break; }; defer free(line); switch (state) { case state::INDEX => match (strconv::stou(line)) { case let index: uint => current.index = index; case => fmt::fatalf("Error on line {}: expected uint, found '{}'\n", nr, line); }; state = state::TIMECODE; case state::TIMECODE => match (parse_timecode(line)) { case let times: (time::duration, time::duration) => current.start = times.0; current.end = times.1; case => fmt::fatalf("Error on line {}: invalid timecode syntax\n", nr); }; state = state::TEXT; case state::TEXT => if (len(line) == 0) { state = state::INDEX; append(segments, current); current = segment { ... }; continue; }; let text = strings::concat(current.text, line, "\r\n"); free(current.text); current.text = text; }; }; return segments; }; fn parse_timecode(line: str) ((time::duration, time::duration) | strconv::invalid | strconv::overflow) = { let (start, end) = strings::cut(line, " --> "); let start = parse_part(start)?; let end = parse_part(end)?; return (start, end); }; fn parse_part(text: str) (time::duration | strconv::invalid | strconv::overflow) = { let dur: time::duration = 0; let (text, ms) = strings::cut(text, ","); dur += strconv::stoi64(ms)? * time::MILLISECOND; let (hrs, text) = strings::cut(text, ":"); dur += strconv::stoi64(hrs)? * time::HOUR; let (mins, secs) = strings::cut(text, ":"); dur += strconv::stoi64(mins)? * time::MINUTE; dur += strconv::stoi64(secs)? * time::SECOND; return dur; };