diff options
| author | Sam Nystrom <sam@samnystrom.dev> | 2023-08-16 16:26:05 -0400 |
|---|---|---|
| committer | Sam Nystrom <sam@samnystrom.dev> | 2023-08-16 16:27:29 -0400 |
| commit | 277fa45fd3d6bd80842660460ce978a01d286ced (patch) | |
| tree | 601504ff93df4fe067cb94d67127e1f86ba09f1a /main.ha | |
| parent | 6b0441319f2ace9b473c75eecd9b809fb7cf57c6 (diff) | |
Rewrite to Perl for maintainability
Hare is experimental, unstable, and low-level, and I want to spend as
little effort as possible maintaining this program.
Diffstat (limited to 'main.ha')
| -rw-r--r-- | main.ha | 403 |
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; -}; |
