From 610967b7beec11e212ed4602954f7bf3c7db7822 Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Sat, 29 Nov 2025 15:06:15 +0100 Subject: [PATCH 1/2] Add `logging` module --- gleam.toml | 1 + manifest.toml | 2 + src/musicplayer/logging/control.gleam | 7 ++++ src/musicplayer/logging/logging.gleam | 58 +++++++++++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/musicplayer/logging/control.gleam create mode 100644 src/musicplayer/logging/logging.gleam diff --git a/gleam.toml b/gleam.toml index 70b914e..775ec6b 100644 --- a/gleam.toml +++ b/gleam.toml @@ -18,6 +18,7 @@ gleam_otp = ">= 1.2.0 and < 2.0.0" gleam_erlang = ">= 1.3.0 and < 2.0.0" simplifile = ">= 2.3.1 and < 3.0.0" gleam_json = ">= 3.1.0 and < 4.0.0" +gleam_time = ">= 1.6.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index a94ff87..af21009 100644 --- a/manifest.toml +++ b/manifest.toml @@ -7,6 +7,7 @@ packages = [ { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, + { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, { name = "simplifile", version = "2.3.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "957E0E5B75927659F1D2A1B7B75D7B9BA96FAA8D0C53EA71C4AD9CD0C6B848F6" }, ] @@ -16,5 +17,6 @@ gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } gleam_json = { version = ">= 3.1.0 and < 4.0.0" } gleam_otp = { version = ">= 1.2.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleam_time = { version = ">= 1.6.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } simplifile = { version = ">= 2.3.1 and < 3.0.0" } diff --git a/src/musicplayer/logging/control.gleam b/src/musicplayer/logging/control.gleam new file mode 100644 index 0000000..00a1220 --- /dev/null +++ b/src/musicplayer/logging/control.gleam @@ -0,0 +1,7 @@ +import gleam/erlang/process.{type Subject} + +pub type Control { + Write(String) + + Exit(reply_to: Subject(Nil)) +} diff --git a/src/musicplayer/logging/logging.gleam b/src/musicplayer/logging/logging.gleam new file mode 100644 index 0000000..26687c8 --- /dev/null +++ b/src/musicplayer/logging/logging.gleam @@ -0,0 +1,58 @@ +import gleam/erlang/process.{type Subject} +import gleam/otp/actor +import gleam/result +import gleam/string +import gleam/time/calendar +import gleam/time/timestamp +import simplifile + +import musicplayer/logging/control.{type Control} + +type State { + State(filepath: String) +} + +pub fn new(filepath: String) -> Result(Subject(Control), String) { + use _ <- result.try( + case simplifile.is_file(filepath) { + Ok(True) -> Ok(Nil) + _ -> simplifile.create_file(filepath) + } + |> result.map_error(fn(e) { + "Could not access or create log file: " <> string.inspect(e) + }), + ) + + actor.new(State(filepath:)) + |> actor.on_message(handle_message) + |> actor.start + |> result.map_error(fn(start_error) { + "Could not start logger: " <> string.inspect(start_error) + }) + |> result.map(fn(started) { started.data }) +} + +fn handle_message(state: State, control: Control) -> actor.Next(State, Control) { + case control { + control.Write(content) -> { + let log_line = + timestamp.system_time() + |> timestamp.to_rfc3339(calendar.utc_offset) + <> ": " + <> content + <> "\n" + + // Ignore any logging errors + let _ = simplifile.append(state.filepath, log_line) + actor.continue(state) + } + control.Exit(reply_to) -> { + process.send(reply_to, Nil) + actor.stop() + } + } +} + +pub fn log(logger: Subject(Control), content: String) -> Nil { + process.send(logger, control.Write(content)) +} -- 2.52.0 From 1d12f46d2cbea664450fcec13bf47829992993ee Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Sat, 29 Nov 2025 15:06:24 +0100 Subject: [PATCH 2/2] Use `logger` --- src/musicplayer.gleam | 6 +++++- src/musicplayer/mpv/mpv.gleam | 2 +- src/musicplayer/musicplayer.gleam | 33 +++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/musicplayer.gleam b/src/musicplayer.gleam index 69d9bf5..3fd6635 100644 --- a/src/musicplayer.gleam +++ b/src/musicplayer.gleam @@ -2,17 +2,21 @@ import gleam/erlang/process.{type Name} import musicplayer/input/input import musicplayer/input/key.{type Key} +import musicplayer/logging/logging import musicplayer/mpv/mpv import musicplayer/musicplayer import musicplayer/ui/ui pub fn main() -> Nil { + let assert Ok(logger) = logging.new("/tmp/musicplayer.log") + let input_keys_name: Name(Key) = process.new_name("input_keys") input.new(input_keys_name) let assert Ok(ui) = ui.new() let assert Ok(mpv) = mpv.new() - let assert Ok(musicplayer_pid) = musicplayer.new(ui, mpv, input_keys_name) + let assert Ok(musicplayer_pid) = + musicplayer.new(logger, ui, mpv, input_keys_name) let monitor = process.monitor(musicplayer_pid) process.new_selector() diff --git a/src/musicplayer/mpv/mpv.gleam b/src/musicplayer/mpv/mpv.gleam index 73f4e27..7e38ad6 100644 --- a/src/musicplayer/mpv/mpv.gleam +++ b/src/musicplayer/mpv/mpv.gleam @@ -24,7 +24,7 @@ pub fn new() -> Result(Subject(Control), String) { |> actor.start { Error(start_error) -> - Error("Could not start actor: " <> string.inspect(start_error)) + Error("Could not start mpv: " <> string.inspect(start_error)) Ok(actor.Started(data: mpv, ..)) -> Ok(mpv) } } diff --git a/src/musicplayer/musicplayer.gleam b/src/musicplayer/musicplayer.gleam index f81e75f..65c8bfa 100644 --- a/src/musicplayer/musicplayer.gleam +++ b/src/musicplayer/musicplayer.gleam @@ -2,11 +2,13 @@ import gleam/erlang/process.{type Name, type Pid, type Subject} import gleam/otp/actor import gleam/result import gleam/string -import musicplayer/time/time import musicplayer/control.{type Control} import musicplayer/input/key.{type Key} +import musicplayer/logging/control as logging_control +import musicplayer/logging/logging import musicplayer/mpv/control as mpv_control +import musicplayer/time/time import musicplayer/ui/control as ui_control import musicplayer/ui/layout @@ -23,12 +25,14 @@ type State { State( mode: Mode, input: Input, + logger: Subject(logging_control.Control), ui: Subject(ui_control.Control), mpv: Subject(mpv_control.Control), ) } pub fn new( + logger: Subject(logging_control.Control), ui: Subject(ui_control.Control), mpv: Subject(mpv_control.Control), input_keys_name: Name(Key), @@ -38,13 +42,14 @@ pub fn new( let input = Input(False, "") case - actor.new(State(Idle, input, ui, mpv)) + actor.new(State(Idle, input, logger, ui, mpv)) |> actor.on_message(handle_message) |> actor.start { Error(start_error) -> Error("Could not start actor: " <> string.inspect(start_error)) Ok(actor.Started(pid:, data: musicplayer)) -> { + logging.log(logger, "musicplayer - started") process.spawn(fn() { let assert Ok(_) = process.register(process.self(), input_keys_name) handle_key(musicplayer, input_keys) @@ -60,6 +65,8 @@ pub fn new( fn handle_message(state: State, control: Control) -> actor.Next(State, Control) { case control { control.Search -> { + logging.log(state.logger, "musicplayer - initiating search") + update_search(state.ui, "searching: ") actor.continue( @@ -72,6 +79,8 @@ fn handle_message(state: State, control: Control) -> actor.Next(State, Control) } control.Raw(content) -> { + logging.log(state.logger, "musicplayer - recieved raw input: " <> content) + let content = case state.mode { Idle -> state.input.content Searching -> { @@ -84,6 +93,8 @@ fn handle_message(state: State, control: Control) -> actor.Next(State, Control) actor.continue(State(..state, input: Input(..state.input, content:))) } control.Backspace -> { + logging.log(state.logger, "musicplayer - recieved backspace") + let content = case state.mode { Idle -> state.input.content Searching -> string.drop_end(state.input.content, 1) @@ -91,6 +102,14 @@ fn handle_message(state: State, control: Control) -> actor.Next(State, Control) actor.continue(State(..state, input: Input(..state.input, content:))) } control.Return -> { + logging.log( + state.logger, + "musicplayer - recieved return. `input.capture`: " + <> "'" + <> state.input.content + <> "'", + ) + // Note: state.input.content is now the final input, use it // before it is reset case state.mode { @@ -104,17 +123,27 @@ fn handle_message(state: State, control: Control) -> actor.Next(State, Control) } control.TogglePlayPause -> { + logging.log(state.logger, "musicplayer - toggling play/pause") + process.send(state.mpv, mpv_control.TogglePlayPause) update_playback_time(state.mpv, state.ui) actor.continue(state) } control.Exit -> { + logging.log(state.logger, "musicplayer - initiating musicplayer shutdown") // Close `mpv` socket process.call(state.mpv, 1000, fn(reply_to) { mpv_control.Exit(reply_to) }) // Reset terminal state (show cursor etc.) process.call(state.ui, 1000, fn(reply_to) { ui_control.Exit(reply_to) }) + logging.log(state.logger, "musicplayer - stopped") + + // Close logger (NOOP) + process.call(state.logger, 1000, fn(reply_to) { + logging_control.Exit(reply_to) + }) + actor.stop() } } -- 2.52.0