diff options
| author | Sam Nystrom <sam@samnystrom.dev> | 2023-06-22 13:39:07 -0400 |
|---|---|---|
| committer | Sam Nystrom <sam@samnystrom.dev> | 2023-06-22 13:39:07 -0400 |
| commit | 245c4d9839182fbbf6a84a7ca2c678426bb020f4 (patch) | |
| tree | 9ed6526f5d8f945e59c06624cf0e2de6a33b54ff /main.ha | |
init
Diffstat (limited to 'main.ha')
| -rw-r--r-- | main.ha | 241 |
1 files changed, 241 insertions, 0 deletions
@@ -0,0 +1,241 @@ +// SPDX-FileCopyrightText: Sam Nystrom <sam@samnystrom.dev> +// 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", + "<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; +}; |
