summaryrefslogtreecommitdiff
path: root/main.ha
diff options
context:
space:
mode:
authorSam Nystrom <sam@samnystrom.dev>2023-06-22 13:39:07 -0400
committerSam Nystrom <sam@samnystrom.dev>2023-06-22 13:39:07 -0400
commit245c4d9839182fbbf6a84a7ca2c678426bb020f4 (patch)
tree9ed6526f5d8f945e59c06624cf0e2de6a33b54ff /main.ha
init
Diffstat (limited to 'main.ha')
-rw-r--r--main.ha241
1 files changed, 241 insertions, 0 deletions
diff --git a/main.ha b/main.ha
new file mode 100644
index 0000000..8de4287
--- /dev/null
+++ b/main.ha
@@ -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;
+};