From bc5297196ec36c27c4d084b9394eba869409b4be Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Fri, 14 Nov 2025 19:15:31 +0100 Subject: [PATCH] Add ability to listen to input --- src/mpv/internal.gleam | 67 +++++++++++++++++++++++++++++++++++++++++ src/mpv/mpv.gleam | 66 ++++++++++++++++++++++++++++++++++++++++ src/musicplayer.gleam | 7 +++-- test/mpv/mpv_test.gleam | 29 ++++++++++++++++++ 4 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/mpv/internal.gleam create mode 100644 src/mpv/mpv.gleam create mode 100644 test/mpv/mpv_test.gleam diff --git a/src/mpv/internal.gleam b/src/mpv/internal.gleam new file mode 100644 index 0000000..4fe0e49 --- /dev/null +++ b/src/mpv/internal.gleam @@ -0,0 +1,67 @@ +import gleam/erlang/atom +import gleam/list + +pub type Key { + Char(String) + + Left + Right + Up + Down + + Continue + Empty + Unknown +} + +pub const esc = "\u{001B}" + +// control sequence introducer +pub const csi = "[" + +pub fn from_list(l: List(String)) -> Key { + case l { + [e, c, "D"] if e == esc && c == csi -> Left + [e, c, "C"] if e == esc && c == csi -> Right + [e, c, "A"] if e == esc && c == csi -> Up + [e, c, "B"] if e == esc && c == csi -> Down + + [e, c] if e == esc && c == csi -> Continue + + [e] if e == esc -> Continue + [char] -> Char(char) + + [] -> Continue + _ -> Unknown + } +} + +pub fn start_raw_shell() { + let no_shell = atom.create("noshell") + let raw = atom.create("raw") + shell_start_interactive(#(no_shell, raw)) +} + +// TODO map key to something like Control, to not leak `Continue` etc. +pub fn read_input_until_key(l: List(String)) -> Key { + let l = read_input() |> list.wrap |> list.append(l, _) + + case from_list(l) { + Continue -> read_input_until_key(l) + k -> k + } +} + +fn read_input() -> String { + io_get_chars("", 1) +} + +pub type NotUsed + +// https://www.erlang.org/doc/apps/stdlib/shell.html#start_interactive/1 +@external(erlang, "shell", "start_interactive") +fn shell_start_interactive(options: #(atom.Atom, atom.Atom)) -> NotUsed + +// https://www.erlang.org/doc/apps/stdlib/io.html#get_line/1 +@external(erlang, "io", "get_chars") +fn io_get_chars(prompt: String, count: Int) -> String diff --git a/src/mpv/mpv.gleam b/src/mpv/mpv.gleam new file mode 100644 index 0000000..403f195 --- /dev/null +++ b/src/mpv/mpv.gleam @@ -0,0 +1,66 @@ +import gleam/erlang/process.{type Subject} +import gleam/otp/actor +import gleam/string + +import mpv/internal.{type Key, Char} +import tcp/reason.{type Reason} +import tcp/tcp.{type Socket} + +pub type Message { + KeyPress(Key) +} + +type State(socket, exit) { + State(socket: Socket, exit: Subject(Nil)) +} + +pub fn new(exit: Subject(Nil)) -> Result(Nil, String) { + // TODO start up mvp here, currently hi-jacking `naviterm`s socket + let socket_path = "/tmp/naviterm_mpv" + + case tcp.connect(socket_path) { + Error(r) -> Error("Could not connect to mpv: " <> reason.to_string(r)) + Ok(socket) -> { + case + actor.new(State(socket, exit)) + |> actor.on_message(handle_message) + |> actor.start + { + Error(start_error) -> + Error("Could not start actor: " <> string.inspect(start_error)) + Ok(actor.Started(data:, ..)) -> { + echo "waiting for input" + internal.start_raw_shell() + process.spawn(fn() { read_input(data) }) + Ok(Nil) + } + } + } + } +} + +pub fn toggle_play_pause(socket: Socket) -> Result(Nil, Reason) { + tcp.send(socket, "{\"command\":[\"cycle\",\"pause\"]}\n") +} + +fn handle_message( + state: State(socket, exit), + message: Message, +) -> actor.Next(State(socket, exit), Message) { + case message { + KeyPress(Char("q")) -> { + process.send(state.exit, Nil) + actor.stop() + } + KeyPress(key) -> { + echo "key: " <> string.inspect(key) + actor.continue(state) + } + } +} + +fn read_input(subject: Subject(Message)) -> Nil { + internal.read_input_until_key([]) |> KeyPress |> process.send(subject, _) + + read_input(subject) +} diff --git a/src/musicplayer.gleam b/src/musicplayer.gleam index c98a0e3..717142d 100644 --- a/src/musicplayer.gleam +++ b/src/musicplayer.gleam @@ -1,5 +1,8 @@ -import gleam/io +import gleam/erlang/process +import mpv/mpv pub fn main() -> Nil { - io.println("musicplayer") + let exit = process.new_subject() + let assert Ok(_) = mpv.new(exit) + process.receive_forever(exit) } diff --git a/test/mpv/mpv_test.gleam b/test/mpv/mpv_test.gleam new file mode 100644 index 0000000..c46fbf6 --- /dev/null +++ b/test/mpv/mpv_test.gleam @@ -0,0 +1,29 @@ +import gleam/list +import gleeunit + +import mpv/internal.{Char, csi, esc} + +pub fn main() -> Nil { + gleeunit.main() +} + +type TestCase { + TestCase(input: List(String), expected: internal.Key) +} + +pub fn mpv_key_from_list_test() { + let test_cases = [ + TestCase(["c"], Char("c")), + TestCase([esc, csi, "D"], internal.Left), + TestCase([esc, csi, "C"], internal.Right), + TestCase([esc, csi, "A"], internal.Up), + TestCase([esc, csi, "B"], internal.Down), + TestCase([esc, csi], internal.Continue), + TestCase([esc], internal.Continue), + TestCase([], internal.Continue), + ] + + list.each(test_cases, fn(tc) { + assert tc.expected == internal.from_list(tc.input) + }) +} -- 2.51.0