Simplify input capture

Instead of "injecting" characters into the input stream, the input
stream is now forwarded to the `musicplayer`. It has will have to
decide what to do with the stream, e.g. by setting the "mode" to
something that captures the input stream and acts upon it
This commit is contained in:
Alexander Heldt
2025-11-28 23:32:45 +01:00
parent 35d331a753
commit dd9468938d
7 changed files with 98 additions and 105 deletions

View File

@@ -8,15 +8,13 @@ import musicplayer/ui/ui
pub fn main() -> Nil {
let input_keys_name: Name(Key) = process.new_name("input_keys")
let input_inject_name: Name(Key) = process.new_name("input_inject_keys")
input.new(input_keys_name, input_inject_name)
input.new(input_keys_name)
let assert Ok(ui) = ui.new()
let assert Ok(mpv) = mpv.new()
let exit = process.new_subject()
let assert Ok(_) =
musicplayer.new(ui, mpv, input_keys_name, input_inject_name, exit)
let assert Ok(_) = musicplayer.new(ui, mpv, input_keys_name, exit)
process.receive_forever(exit)
}

View File

@@ -2,26 +2,29 @@ import musicplayer/input/key.{type Key}
pub type Control {
TogglePlayPause
Search
Input(String)
Search(List(String))
Raw(String)
Return
Backspace
Exit
}
pub fn from_key(key: Key) -> Result(Control, Nil) {
case key {
key.Input(content) -> Ok(Input(content))
key.Char(char) -> char_control(char)
key.Return -> Ok(Return)
key.Backspace -> Ok(Backspace)
key.Char(char) -> Ok(char_control(char))
_ -> Error(Nil)
}
}
fn char_control(char: String) -> Result(Control, Nil) {
fn char_control(char: String) -> Control {
case char {
" " -> Ok(TogglePlayPause)
"/" -> Ok(Search([]))
"q" -> Ok(Exit)
_ -> Error(Nil)
" " -> TogglePlayPause
"/" -> Search
"q" -> Exit
_ -> Raw(char)
}
}

View File

@@ -2,22 +2,12 @@ import gleam/erlang/process.{type Name}
import musicplayer/input/key.{type Key}
/// `new` accepts two named subjects:
/// - one to send the input to, and
/// - one to receive injected input from
///
/// The subject for input injection is used 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
pub fn new(input_keys_name: Name(Key), input_inject_name: Name(Key)) -> Nil {
/// `new` accepts a subject that all input will be sent to
pub fn new(input_keys_name: Name(Key)) -> Nil {
let _ =
process.spawn(fn() {
let input_keys = process.named_subject(input_keys_name)
let input_inject = process.named_subject(input_inject_name)
let assert Ok(_) = process.register(process.self(), input_inject_name)
key.read_input_until_key(input_keys, input_inject)
key.read_input_until_key(input_keys)
})
key.start_raw_shell()

View File

@@ -1,20 +1,21 @@
import gleam/erlang/atom
import gleam/erlang/process.{type Subject}
import gleam/list
import gleam/string
import musicplayer/input/internal as internal_input
pub type Key {
Continue(buffer: List(String))
Char(String)
Input(String)
Left
Right
Up
Down
Continue(buffer: List(String))
Return
Backspace
Unknown
}
@@ -23,8 +24,9 @@ pub const esc = "\u{001B}"
// control sequence introducer
pub const csi = "["
// input introducer
pub const input_introducer = "::"
pub const return = "\r"
pub const backspace = "\u{007F}"
pub fn from_list(l: List(String)) -> Key {
case l {
@@ -35,18 +37,9 @@ pub fn from_list(l: List(String)) -> Key {
[e, c] if e == esc && c == csi -> Continue(l)
[ci] | [ci, _] if ci == input_introducer -> Continue(l)
[ii, cmd, tail] if ii == input_introducer -> {
case tail {
// Return
"\r" -> Input(cmd)
// Backspace
"\u{007F}" -> Continue([ii, string.drop_end(cmd, 1)])
_ -> Continue([ii, cmd <> tail])
}
}
[e] if e == esc -> Continue(l)
[bs] if bs == backspace -> Backspace
[rtn] if rtn == return -> Return
[char] -> Char(char)
[] -> Continue([])
@@ -60,35 +53,24 @@ pub fn start_raw_shell() {
internal_input.shell_start_interactive(#(no_shell, raw))
}
pub fn read_input_until_key(
input_keys: Subject(Key),
input_inject: Subject(Key),
) -> Nil {
case process.receive(input_inject, 1) {
Ok(Continue(buffer)) -> buffer
Ok(_) | Error(_) -> []
}
|> forward_input_to(input_keys)
read_input_until_key(input_keys, input_inject)
pub fn read_input_until_key(input_keys: Subject(Key)) -> Nil {
read_input_until_key_loop([], input_keys)
}
pub fn forward_input_to(l: List(String), input_keys: Subject(Key)) -> Nil {
pub fn read_input_until_key_loop(
l: List(String),
input_keys: Subject(Key),
) -> Nil {
case
internal_input.read_input()
|> list.wrap
|> list.append(l, _)
|> from_list
{
Continue(l) -> {
case l {
[ii, cmd] if ii == input_introducer -> {
process.send(input_keys, Continue([cmd]))
forward_input_to(l, input_keys)
}
_ -> forward_input_to(l, input_keys)
}
Continue(l) -> read_input_until_key_loop(l, input_keys)
k -> {
process.send(input_keys, k)
read_input_until_key_loop([], input_keys)
}
k -> process.send(input_keys, k)
}
}

View File

@@ -37,8 +37,6 @@ fn handle_message(
) -> actor.Next(State(socket), Control) {
case control {
control.TogglePlayPause -> {
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

View File

@@ -10,11 +10,21 @@ import musicplayer/mpv/control as mpv_control
import musicplayer/ui/control as ui_control
import musicplayer/ui/layout
type State(ui, mpv, input_inject, exit) {
type Mode {
Idle
Searching
}
type Input {
Input(capturing: Bool, content: String)
}
type State {
State(
mode: Mode,
input: Input,
ui: Subject(ui_control.Control),
mpv: Subject(mpv_control.Control),
input_inject: Subject(Key),
exit: Subject(Nil),
)
}
@@ -23,14 +33,14 @@ pub fn new(
ui: Subject(ui_control.Control),
mpv: Subject(mpv_control.Control),
input_keys_name: Name(Key),
input_inject_name: Name(Key),
exit: Subject(Nil),
) -> Result(Nil, String) {
let input_keys = process.named_subject(input_keys_name)
let input_inject = process.named_subject(input_inject_name)
let input = Input(False, "")
case
actor.new(State(ui, mpv, input_inject, exit))
actor.new(State(Idle, input, ui, mpv, exit))
|> actor.on_message(handle_message)
|> actor.start
{
@@ -49,28 +59,53 @@ pub fn new(
}
}
fn handle_message(
state: State(ui, mpv, input_inject, exit),
control: Control,
) -> actor.Next(State(ui, mpv, input_inject, exit), Control) {
fn handle_message(state: State, control: Control) -> actor.Next(State, Control) {
case control {
control.Search([]) -> {
process.send(state.input_inject, key.Continue([key.input_introducer]))
control.Search -> {
update_search(state.ui, "searching: ")
actor.continue(state)
actor.continue(
State(
..state,
mode: Searching,
input: Input(..state.input, capturing: True),
),
)
}
control.Search(content) -> {
update_search(state.ui, "searching: " <> string.join(content, ""))
actor.continue(state)
control.Raw(content) -> {
let content = case state.mode {
Idle -> state.input.content
Searching -> {
let updated = state.input.content <> content
update_search(state.ui, "searching: " <> updated)
updated
}
}
actor.continue(State(..state, input: Input(..state.input, content:)))
}
control.Input(_) -> {
update_search(state.ui, "")
actor.continue(state)
control.Backspace -> {
let content = case state.mode {
Idle -> state.input.content
Searching -> string.drop_end(state.input.content, 1)
}
actor.continue(State(..state, input: Input(..state.input, content:)))
}
control.Return -> {
// Note: state.input.content is now the final input, use it
// before it is reset
case state.mode {
Idle -> Nil
Searching -> update_search(state.ui, "")
}
actor.continue(
State(..state, mode: Idle, input: Input(capturing: False, content: "")),
)
}
control.TogglePlayPause -> {
echo "toggling play/pause"
process.send(state.mpv, mpv_control.TogglePlayPause)
update_playback_time(state.mpv, state.ui)
actor.continue(state)
@@ -133,24 +168,14 @@ fn update_search(ui: Subject(ui_control.Control), content: String) -> Nil {
process.send(ui, ui_control.UpdateState(layout.Search, content))
}
/// `handle_key` listens to a subject onto which `input` will send messages with
/// `Key`s. The keys can be either be
/// - instances of `Continue` which are "streamed"input, or
/// - instances of "final" keys that are the result of "end of input"
/// `handle_key` listens to a subject onto which `input` will send messages with `Key`s
fn handle_key(musicplayer: Subject(Control), input_keys: Subject(Key)) -> Nil {
let _ = case
let _ =
process.new_selector()
|> process.select(input_keys)
|> process.selector_receive_forever
{
key.Continue(buffer:) ->
Ok(process.send(musicplayer, control.Search(buffer)))
key ->
key
|> control.from_key
|> result.map(process.send(musicplayer, _))
}
|> control.from_key
|> result.map(process.send(musicplayer, _))
handle_key(musicplayer, input_keys)
}