diff --git a/src/musicplayer.gleam b/src/musicplayer.gleam index de7d965..6d7bfc3 100644 --- a/src/musicplayer.gleam +++ b/src/musicplayer.gleam @@ -1,22 +1,32 @@ import gleam/erlang/process.{type Name} -import musicplayer/input/input.{type Listener} +import musicplayer/input/input import musicplayer/input/key.{type Key} import musicplayer/mpv/mpv +import musicplayer/musicplayer pub fn main() -> Nil { - let exit = process.new_subject() + let input_keys_name: Name(Key) = process.new_name("input_keys") + let input_stream_name: Name(List(String)) = process.new_name("input_stream") - // `inject_input` is created by name to allow the `input` process that + // `input_inject` is created by name to allow the `input` process that // owns `read_input` to be able to register and receive from it, // while the any other processes can use the name reference to // inject input - let inject_input_name: Name(Key) = process.new_name("inject_input") + let input_inject_name: Name(Key) = process.new_name("input_inject_keys") - let assert Ok(mpv_listener) = mpv.new(exit) + input.new(input_keys_name, input_stream_name, input_inject_name) - let listeners: List(Listener) = [mpv_listener] + let assert Ok(mpv) = mpv.new() - input.new(listeners, inject_input_name) + let exit = process.new_subject() + let assert Ok(_) = + musicplayer.new( + mpv, + input_keys_name, + input_stream_name, + input_inject_name, + exit, + ) process.receive_forever(exit) } diff --git a/src/musicplayer/control.gleam b/src/musicplayer/control.gleam new file mode 100644 index 0000000..36de686 --- /dev/null +++ b/src/musicplayer/control.gleam @@ -0,0 +1,25 @@ +import musicplayer/input/key.{type Key} + +pub type Control { + TogglePlayPause + + Search + + Exit +} + +pub fn from_key(key: Key) -> Result(Control, Nil) { + case key { + key.Char(char) -> char_control(char) + _ -> Error(Nil) + } +} + +fn char_control(char: String) -> Result(Control, Nil) { + case char { + " " -> Ok(TogglePlayPause) + "q" -> Ok(Exit) + "/" -> Ok(Search) + _ -> Error(Nil) + } +} diff --git a/src/musicplayer/input/input.gleam b/src/musicplayer/input/input.gleam index d39fdd1..f590055 100644 --- a/src/musicplayer/input/input.gleam +++ b/src/musicplayer/input/input.gleam @@ -1,12 +1,8 @@ import gleam/erlang/process.{type Name, type Subject} -import gleam/list -import gleam/option.{type Option, None, Some} import musicplayer/input/key.{type Key} -pub type Listener { - InputListener(final: Subject(Key), tap: Option(Subject(List(String)))) -} +// TODO REWRITE below /// `new` accepts a list of listeners that are composed of two subjects; /// - one to get the final `Key` and @@ -14,46 +10,39 @@ pub type Listener { /// and /// - a subject name that is used to create a `Subject` that other processes /// know they can inject a `Key` into the input with -pub fn new(listeners: List(Listener), inject_input_name: Name(Key)) -> Nil { +pub fn new( + input_keys_name: Name(Key), + input_stream_name: Name(List(String)), + input_inject_name: Name(Key), +) -> Nil { let _ = process.spawn(fn() { - let inject_input: Subject(Key) = process.named_subject(inject_input_name) - let assert Ok(_) = process.register(process.self(), inject_input_name) + let input_keys = process.named_subject(input_keys_name) + let input_stream = process.named_subject(input_stream_name) + let input_inject = process.named_subject(input_inject_name) - // Extract all finals and taps (that are defined) - let #(finals, taps) = - list.fold(listeners, #([], []), fn(acc, listener) { - let #(finals, taps) = acc + let assert Ok(_) = process.register(process.self(), input_inject_name) - let finals = [listener.final, ..finals] - - let taps = case listener.tap { - Some(t) -> [t, ..taps] - None -> taps - } - - #(finals, taps) - }) - - read_input(finals, taps, inject_input) + read_input(input_keys, input_stream, input_inject) }) + echo "waiting for input" + key.start_raw_shell() Nil } fn read_input( - finals: List(Subject(Key)), - taps: List(Subject(List(String))), - inject_input: Subject(Key), + input_keys: Subject(Key), + input_stream: Subject(List(String)), + input_inject: Subject(Key), ) -> Nil { - let buffer = case process.receive(inject_input, 1) { + let buffer = case process.receive(input_inject, 1) { Ok(key.Continue(buffer)) -> buffer Ok(_) | Error(_) -> [] } - let _ = - key.read_input_until_key(buffer, taps) - |> fn(k) { list.each(finals, process.send(_, k)) } + key.read_input_until_key(buffer, input_stream) + |> process.send(input_keys, _) - read_input(finals, taps, inject_input) + read_input(input_keys, input_stream, input_inject) } diff --git a/src/musicplayer/input/key.gleam b/src/musicplayer/input/key.gleam index efe94b6..f07aa53 100644 --- a/src/musicplayer/input/key.gleam +++ b/src/musicplayer/input/key.gleam @@ -62,7 +62,7 @@ pub fn start_raw_shell() { pub fn read_input_until_key( l: List(String), - taps: List(Subject(List(String))), + input_stream: Subject(List(String)), ) -> Key { case internal_input.read_input() @@ -71,8 +71,8 @@ pub fn read_input_until_key( |> from_list { Continue(l) -> { - list.each(taps, process.send(_, l)) - read_input_until_key(l, taps) + process.send(input_stream, l) + read_input_until_key(l, input_stream) } k -> k } diff --git a/src/musicplayer/mpv/control.gleam b/src/musicplayer/mpv/control.gleam index 0e0b935..fde5fd7 100644 --- a/src/musicplayer/mpv/control.gleam +++ b/src/musicplayer/mpv/control.gleam @@ -1,8 +1,8 @@ +import gleam/erlang/process.{type Subject} import gleam/json import gleam/result import gleam/string -import musicplayer/input/key.{type Key} import musicplayer/mpv/internal as internal_control import musicplayer/tcp/reason.{type Reason} import musicplayer/tcp/tcp.{type Socket} @@ -10,28 +10,15 @@ import musicplayer/tcp/tcp.{type Socket} pub type Control { TogglePlayPause - Exit + GetPlaybackTime(reply_to: Subject(Result(PlaybackTime, ControlError))) + + Exit(reply_to: Subject(Nil)) } pub type ControlError { ControlError(details: String) } -pub fn from_key(key: Key) -> Result(Control, Nil) { - case key { - key.Char(char) -> char_control(char) - _ -> Error(Nil) - } -} - -fn char_control(char: String) -> Result(Control, Nil) { - case char { - " " -> Ok(TogglePlayPause) - "q" -> Ok(Exit) - _ -> Error(Nil) - } -} - pub fn toggle_play_pause(socket: Socket) -> Result(Nil, ControlError) { let command = json.object([#("command", json.array(["cycle", "pause"], of: json.string))]) diff --git a/src/musicplayer/mpv/mpv.gleam b/src/musicplayer/mpv/mpv.gleam index 6f4732f..c701b40 100644 --- a/src/musicplayer/mpv/mpv.gleam +++ b/src/musicplayer/mpv/mpv.gleam @@ -1,89 +1,62 @@ import gleam/erlang/process.{type Subject} -import gleam/float -import gleam/option.{None} import gleam/otp/actor import gleam/result import gleam/string -import musicplayer/input/input.{type Listener, InputListener} -import musicplayer/input/key.{type Key} -import musicplayer/mpv/control.{type Control} +import musicplayer/mpv/control import musicplayer/tcp/reason import musicplayer/tcp/tcp.{type Socket} -type State(socket, exit) { - State(socket: Socket, exit: Subject(Nil)) +type State(socket) { + State(socket: Socket) } -pub fn new(exit: Subject(Nil)) -> Result(Listener, String) { +pub fn new() -> Result(Subject(control.Control), 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) -> { - let final_input_name = process.new_name("mpv_final_input") - let final_input: Subject(Key) = process.named_subject(final_input_name) - case - actor.new(State(socket, exit)) + actor.new(State(socket)) |> 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" - key.start_raw_shell() - - process.spawn(fn() { - let assert Ok(_) = - process.register(process.self(), final_input_name) - - handle_key(final_input, data) - }) - - Ok(InputListener(final: final_input, tap: None)) - } + Ok(actor.Started(data: mpv, ..)) -> Ok(mpv) } } } } fn handle_message( - state: State(socket, exit), - control: Control, -) -> actor.Next(State(socket, exit), Control) { + state: State(socket), + control: control.Control, +) -> actor.Next(State(socket), control.Control) { case control { control.TogglePlayPause -> { - echo "toggling play/pause" + echo "mpv: toggling play/pause" let _ = result.map_error(control.toggle_play_pause(state.socket), fn(err) { echo "Could not toggle play/pause: " <> err.details }) - let _ = - result.map(control.get_playback_time(state.socket), fn(playback) { - echo "playback: " <> float.to_string(playback.data) - }) - actor.continue(state) } - control.Exit -> { - process.send(state.exit, Nil) + + control.GetPlaybackTime(reply_to) -> { + let res = control.get_playback_time(state.socket) + process.send(reply_to, res) + actor.continue(state) + } + + control.Exit(reply_to) -> { + tcp.close(state.socket) + process.send(reply_to, Nil) actor.stop() } } } - -/// `handle_key` listens to a subject onto which `input` will send messages with -/// parsed `Key`s which will be mapped to `Control`s (if possible) -fn handle_key(final_input: Subject(Key), subject: Subject(Control)) -> Nil { - let _ = - process.receive_forever(final_input) - |> control.from_key - |> result.map(process.send(subject, _)) - - handle_key(final_input, subject) -} diff --git a/src/musicplayer/musicplayer.gleam b/src/musicplayer/musicplayer.gleam new file mode 100644 index 0000000..3c55175 --- /dev/null +++ b/src/musicplayer/musicplayer.gleam @@ -0,0 +1,106 @@ +import gleam/erlang/process.{type Name, type Subject} +import gleam/float +import gleam/otp/actor +import gleam/result +import gleam/string + +import musicplayer/control.{type Control} +import musicplayer/input/key.{type Key} +import musicplayer/mpv/control as mpv_control + +type State(mpv, input_inject, exit) { + State( + mpv: Subject(mpv_control.Control), + input_inject: Subject(Key), + exit: Subject(Nil), + ) +} + +pub fn new( + mpv: Subject(mpv_control.Control), + input_keys_name: Name(Key), + input_stream_name: Name(List(String)), + input_inject_name: Name(Key), + exit: Subject(Nil), +) -> Result(Nil, String) { + let input_keys = process.named_subject(input_keys_name) + let input_stream = process.named_subject(input_stream_name) + let input_inject = process.named_subject(input_inject_name) + + case + actor.new(State(mpv, input_inject, exit)) + |> actor.on_message(handle_message) + |> actor.start + { + Error(start_error) -> + Error("Could not start actor: " <> string.inspect(start_error)) + Ok(actor.Started(data: musicplayer, ..)) -> { + process.spawn(fn() { + let assert Ok(_) = process.register(process.self(), input_keys_name) + handle_key(musicplayer, input_keys) + }) + + process.spawn(fn() { + let assert Ok(_) = process.register(process.self(), input_stream_name) + temp_input_stream(input_stream) + }) + + Ok(Nil) + } + } +} + +fn handle_message( + state: State(mpv, input_inject, exit), + control: Control, +) -> actor.Next(State(mpv, input_inject, exit), Control) { + case control { + control.Search -> { + process.send(state.input_inject, key.Continue([key.input_introducer])) + + actor.continue(state) + } + control.TogglePlayPause -> { + echo "toggling play/pause" + + process.send(state.mpv, mpv_control.TogglePlayPause) + + case + process.call(state.mpv, 1000, fn(reply_to) { + mpv_control.GetPlaybackTime(reply_to) + }) + { + Error(err) -> echo "! could not get playbackTime: " <> err.details + Ok(mpv_control.PlaybackTime(data: playback_time)) -> + echo "playbacktime from mpv: " <> float.to_string(playback_time) + } + + actor.continue(state) + } + control.Exit -> { + // Close socket to `mpv` + process.call(state.mpv, 1000, fn(reply_to) { mpv_control.Exit(reply_to) }) + + // End main process + process.send(state.exit, Nil) + actor.stop() + } + } +} + +/// `handle_key` listens to a subject onto which `input` will send messages with +/// parsed `Key`s which will be mapped to `Control`s (if possible) +fn handle_key(musicplayer: Subject(Control), input_keys: Subject(Key)) -> Nil { + let _ = + process.receive_forever(input_keys) + |> control.from_key + |> result.map(process.send(musicplayer, _)) + + handle_key(musicplayer, input_keys) +} + +fn temp_input_stream(input_stream: Subject(List(String))) -> Nil { + let stream = process.receive_forever(input_stream) + echo "input stream: " <> string.inspect(stream) + temp_input_stream(input_stream) +} diff --git a/test/musicplayer/mpv/control_test.gleam b/test/musicplayer/control_test.gleam similarity index 57% rename from test/musicplayer/mpv/control_test.gleam rename to test/musicplayer/control_test.gleam index d46f403..ab75436 100644 --- a/test/musicplayer/mpv/control_test.gleam +++ b/test/musicplayer/control_test.gleam @@ -1,9 +1,8 @@ import gleam/list import gleeunit +import musicplayer/control.{type Control} import musicplayer/input/key.{type Key, Char} -import musicplayer/mpv/control.{type Control} -import musicplayer/mpv/internal as control_internal pub fn main() -> Nil { gleeunit.main() @@ -23,11 +22,3 @@ pub fn control_from_key_test() { assert tc.expected == control.from_key(tc.key) }) } - -pub fn parse_playback_time_test() { - let json_string = - "{\"data\":\"123.456789\",\"request_id\":0,\"error\":\"success\"}\n" - - let assert Ok(data) = control_internal.parse_playback_time(json_string) - assert data == 123.456789 -} diff --git a/test/musicplayer/mpv/mpv_test.gleam b/test/musicplayer/mpv/mpv_test.gleam new file mode 100644 index 0000000..e0fb0c2 --- /dev/null +++ b/test/musicplayer/mpv/mpv_test.gleam @@ -0,0 +1,15 @@ +import gleeunit + +import musicplayer/mpv/internal as control_internal + +pub fn main() -> Nil { + gleeunit.main() +} + +pub fn parse_playback_time_test() { + let json_string = + "{\"data\":\"123.456789\",\"request_id\":0,\"error\":\"success\"}\n" + + let assert Ok(data) = control_internal.parse_playback_time(json_string) + assert data == 123.456789 +}