summaryrefslogtreecommitdiff
path: root/main.ha
diff options
context:
space:
mode:
Diffstat (limited to 'main.ha')
-rw-r--r--main.ha403
1 files changed, 0 insertions, 403 deletions
diff --git a/main.ha b/main.ha
deleted file mode 100644
index 0994b14..0000000
--- a/main.ha
+++ /dev/null
@@ -1,403 +0,0 @@
-// srtplay - play .srt subtitle files in a TUI
-// Copyright (c) 2023 Sam Nystrom <sam@samnystrom.dev>
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-use bufio;
-use encoding::utf8;
-use getopt;
-use io;
-use format::xml;
-use fmt;
-use fs;
-use os;
-use strconv;
-use strings;
-use strio;
-use time;
-use unix::poll;
-use unix::tty;
-use vt;
-
-type state = struct {
- subtitles: []subtitle,
-
- start: time::instant,
- now: time::instant,
- length: time::duration,
- elapsed: time::duration,
- paused: bool,
-
- term: *vt::term,
-};
-
-type subtitle = struct {
- index: uint,
- start: time::duration,
- end: time::duration,
- text: vt::styled,
-};
-
-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 subtitles = parse_srt(file);
- io::close(file)!;
- defer free(subtitles);
- defer for (let i = 0z; i < len(subtitles); i += 1) {
- let text = subtitles[i].text;
- for (let j = 0z; j < len(text.args); j += 1) {
- free((text.args[j] as vt::styled).args[0] as str);
- };
- free(text.args);
- };
-
- let now = time::now(time::clock::REALTIME);
- let state = state {
- start = now,
- now = now,
- term = vt::open(),
- subtitles = subtitles,
- ...
- };
- defer vt::close(state.term);
- vt::disablecur(state.term)!;
-
- for (let i = 0z; i < len(subtitles); i += 1) {
- if (subtitles[i].end > state.length) {
- state.length = subtitles[i].end;
- };
- };
-
- match (run(&state)) {
- case void => void;
- case let err: vt::error =>
- fmt::errorln("Error:", vt::strerror(err))!;
- };
-};
-
-fn run(state: *state) (void | vt::error) = {
- for (true) {
- if (!state.paused) {
- state.now = time::now(time::clock::REALTIME);
- };
- state.elapsed = time::diff(state.start, state.now);
- if (state.elapsed > state.length) break;
-
- vt::clear(state.term)?;
- let time_text = fmt::asprintf(
- "{:02}:{:02}:{:02} / {:02}:{:02}:{:02}{}\r\n",
- state.elapsed / time::HOUR,
- state.elapsed / time::MINUTE % 60,
- state.elapsed / time::SECOND % 60,
- state.length / time::HOUR,
- state.length / time::MINUTE % 60,
- state.length / time::SECOND % 60,
- if (state.paused) " (PAUSED)" else "",
- );
- defer free(time_text);
- vt::print(state.term, time_text)?;
-
- let timeout = state.length;
-
- for (let i = 0z; i < len(state.subtitles); i += 1) {
- let sub = state.subtitles[i];
- if (sub.start > state.elapsed && sub.start < timeout) {
- timeout = sub.start;
- };
- if (sub.end > state.elapsed && sub.end < timeout) {
- timeout = sub.end;
- };
- if (sub.start <= state.elapsed && state.elapsed <= sub.end) {
- vt::print(state.term, sub.text)?;
- };
- };
-
- timeout -= state.elapsed;
- let next_second = time::SECOND - state.elapsed % time::SECOND;
- if (timeout > next_second) {
- timeout = next_second;
- };
- if (state.paused) {
- timeout = poll::INDEF;
- };
-
- let pollfds = [poll::pollfd {
- fd = os::stdin_file,
- events = poll::event::POLLIN | poll::event::POLLHUP,
- ...
- }];
- if (poll::poll(pollfds, timeout)? == 0) continue;
-
- let ev = match (vt::pollevent(state.term)?) {
- case void =>
- continue;
- case let ev: vt::event =>
- yield ev;
- case io::EOF =>
- break;
- };
-
- match (ev.value) {
- case let key: rune =>
- switch (key) {
- case 'q' =>
- break;
- case 'c' =>
- if (ev.mods & vt::modflag::CTRL != 0) break;
- case 'j' =>
- fast_forward(state, -time::SECOND * 10);
- case 'l' =>
- fast_forward(state, time::SECOND * 10);
- case 'k' =>
- pause(state);
- case ' ' =>
- pause(state);
- case => void;
- };
- case let key: vt::specialkey =>
- switch (key) {
- case vt::specialkey::LEFT =>
- fast_forward(state, -time::SECOND * 5);
- case vt::specialkey::RIGHT =>
- fast_forward(state, time::SECOND * 5);
- };
- case vt::functionkey => void;
- };
- };
-};
-
-fn fast_forward(state: *state, dur: time::duration) void = {
- state.start = time::add(state.start, -dur);
- if (time::diff(state.start, state.now) < 0) {
- state.start = state.now;
- };
-};
-
-fn pause(state: *state) void = {
- if (state.paused) {
- let now = time::now(time::clock::REALTIME);
- state.start = time::add(state.start, time::diff(state.now, now));
- state.now = now;
- };
- state.paused = !state.paused;
-};
-
-type parser_state = enum {
- INDEX,
- TIMECODE,
- TEXT,
-};
-
-def XML_PROLOG: str = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n";
-
-fn parse_srt(file: io::handle) []subtitle = {
- let subtitles: []subtitle = [];
- let current = subtitle {
- text = vt::styled {
- pen = vt::defaultpen,
- ...
- },
- ...
- };
- let content = strio::dynamic();
- defer io::close(&content)!;
- strio::concat(&content, XML_PROLOG)!;
- let state = parser_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 parser_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 = parser_state::TIMECODE;
- case parser_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 = parser_state::TEXT;
- case parser_state::TEXT =>
- if (len(line) > 0) {
- strio::concat(&content, line, "\n")!;
- continue;
- };
-
- strio::concat(&content, "</root>")!;
- let content_str = strio::string(&content);
- let buf = bufio::fixed(strings::toutf8(content_str), io::mode::READ);
- current.text = match (parse_text(&buf)) {
- case let text: vt::styled =>
- yield text;
- case =>
- let content = strings::trimprefix(content_str, XML_PROLOG);
- let content = strings::trimsuffix(content, "</root>");
- let text = vt::styled {
- pen = vt::defaultpen,
- args = [strings::replace(content, "\n", "\r\n")],
- };
- yield text;
- };
-
- append(subtitles, current);
- current = subtitle {
- text = vt::styled {
- pen = vt::defaultpen,
- ...
- },
- ...
- };
-
- strio::reset(&content);
- strio::concat(&content, XML_PROLOG)!;
-
- state = parser_state::INDEX;
- };
- };
-
- return subtitles;
-};
-
-fn parse_timecode(timecode: str) ((time::duration, time::duration) | strconv::invalid | strconv::overflow) = {
- let (start, end) = strings::cut(timecode, " --> ");
- let start = parse_time(start)?;
- let end = parse_time(end)?;
- return (start, end);
-};
-
-fn parse_time(time: str) (time::duration | strconv::invalid | strconv::overflow) = {
- let dur: time::duration = 0;
-
- let (time, ms) = strings::cut(time, ",");
- dur += strconv::stoi64(ms)? * time::MILLISECOND;
- let (hrs, time) = strings::cut(time, ":");
- dur += strconv::stoi64(hrs)? * time::HOUR;
- let (mins, secs) = strings::cut(time, ":");
- dur += strconv::stoi64(mins)? * time::MINUTE;
- dur += strconv::stoi64(secs)? * time::SECOND;
-
- return dur;
-};
-
-fn parse_text(in: io::handle) (vt::styled | io::error | xml::error) = {
- let parser = xml::parse(in)?;
- defer xml::parser_free(parser);
-
- let text = vt::styled {
- pen = vt::defaultpen,
- ...
- };
- let pen = vt::defaultpen;
- let bold = 0;
- let italic = 0;
- let underline = 0;
- for (true) {
- let tok = match (xml::scan(parser)?) {
- case let tok: xml::token =>
- yield tok;
- case void =>
- break;
- };
-
- match (tok) {
- case let start: xml::elementstart =>
- switch (start) {
- case "b" =>
- if (bold == 0) {
- pen.style |= vt::style::BOLD;
- };
- bold += 1;
- case "i" =>
- if (italic == 0) {
- pen.style |= vt::style::ITALIC;
- };
- italic += 1;
- case "u" =>
- if (underline == 0) {
- pen.style |= vt::style::ULINE;
- };
- underline += 1;
- case => void;
- };
- case let end: xml::elementend =>
- switch (end) {
- case "b" =>
- if (bold == 1) {
- pen.style &= ~vt::style::BOLD;
- };
- bold -= 1;
- case "i" =>
- if (italic == 1) {
- pen.style &= ~vt::style::ITALIC;
- };
- italic -= 1;
- case "u" =>
- if (underline == 1) {
- pen.style &= ~vt::style::ULINE;
- };
- underline -= 1;
- case => void;
- };
- case xml::attribute => void;
- case let t: xml::text =>
- let styled = vt::styled {
- pen = pen,
- args = alloc([strings::replace(t, "\n", "\r\n")]),
- };
- append(text.args, styled);
- };
- };
- return text;
-};