From 5c6acc2af6c45038c4d0d6bb1df26fce02f64793 Mon Sep 17 00:00:00 2001 From: Ales Tsurko Date: Sun, 27 Oct 2024 18:39:23 +0100 Subject: [PATCH] Add scratch dir button --- Cargo.lock | 1 + Cargo.toml | 1 + src/app/app.rs | 74 +++++++++++++++++++++++++++------- src/interpreter/mod.rs | 90 +++++++++++++++++++++++++++++++++++------- src/main.rs | 2 +- 5 files changed, 137 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 535d42d..3e8ced1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,6 +442,7 @@ dependencies = [ "env_logger", "iced", "iced_aw", + "iced_fonts", "log", "midi-player", "quick-xml 0.31.0", diff --git a/Cargo.toml b/Cargo.toml index e9be838..1bc5b4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ cpal = "0.15.3" env_logger = "0.11" iced = { version = "0.13.1", features = ["svg", "tokio", "wgpu"] } iced_aw = "0.11" +iced_fonts = { version = "0.1.1", features = ["nerd"]} log = "0.4" midi-player = "0.2.1" quick-xml = "0.31.0" diff --git a/src/app/app.rs b/src/app/app.rs index 86829e3..5c5b13c 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -1,10 +1,16 @@ //! Application's GUI. +use std::env; + use iced::futures::sink::SinkExt; use iced::stream; -use iced::widget::{column, container, scrollable, text, text::Style as TextStyle, text_input}; -use iced::{time, widget::row, Element, Font, Subscription}; +use iced::widget::{ + button, column, container, horizontal_space, row, scrollable, text, text::Style as TextStyle, + text_input, +}; +use iced::{time, Element, Font, Subscription}; use iced_aw::widget::number_input; +use rfd::FileDialog; use super::midi_player::{self, GlobalState as GlobalMidiPlayerState, State as MidiPlayerState}; use crate::interpreter; @@ -23,6 +29,7 @@ pub struct State { output: Vec, question: Option, midi_player_state: GlobalMidiPlayerState, + scratch_dir: String, } impl Default for State { @@ -41,11 +48,18 @@ impl Default for State { "# .to_owned(), )]; + + interpreter::INTERPRETER_WORKER + .interp_sender + .send_blocking(interpreter::Message::GetScratchDir) + .expect("the channel is unbound"); + Self { midi_player_state, answer: String::new(), output, question: None, + scratch_dir: String::new(), } } } @@ -64,6 +78,14 @@ pub fn update(state: &mut State, message: Message) { Message::InputChanged(val) => { state.answer = val; } + Message::SetScratchDir => { + if let Some(value) = pick_directory("Choose scratch folder") { + interpreter::INTERPRETER_WORKER + .interp_sender + .send_blocking(interpreter::Message::SetScratchDir(value)) + .expect("the channel is unbound"); + } + } Message::PlayMidi(id) => { if let Some(playing_id) = state.midi_player_state.playing_id { if let Some(Output::MidiPlayer(player_state)) = state.output.get_mut(playing_id) { @@ -162,6 +184,10 @@ pub fn update(state: &mut State, message: Message) { position: 0.0, })); } + interpreter::Message::ScratchDir(value) => { + state.scratch_dir = value; + } + _ => (), }, Message::Answer(question, value) => { state.question = None; @@ -191,6 +217,7 @@ pub fn view(state: &State) -> Element { ); container(column![ + view_top_panel(state), scrollable(output.padding(20.0)) .style(|theme: &iced::Theme, status: Status| { let mut style = theme.style(&::default(), status); @@ -207,7 +234,6 @@ pub fn view(state: &State) -> Element { .height(iced::Length::Fill) .anchor_bottom(), view_prompt(state), - view_bottom_panel(state), ]) .padding(40) .width(TERM_WIDTH * FONT_WIDTH) @@ -215,6 +241,25 @@ pub fn view(state: &State) -> Element { .into() } +fn view_top_panel(state: &State) -> Element { + row![ + button(text("").font(iced_fonts::NERD_FONT).size(16.0)) + .style(button::text) + .on_press(Message::SetScratchDir), + text(&state.scratch_dir), + horizontal_space(), + text("Tempo:"), + number_input(state.midi_player_state.tempo(), 20..=600, Message::SetTempo,) + .step(1) + .width(60.0), + text("BPM"), + ] + .spacing(10.0) + .align_y(iced::Alignment::Center) + .height(40.0) + .into() +} + fn view_output(output: &Output) -> Element { match output { Output::Normal(msg) => container(text(msg)), @@ -259,7 +304,7 @@ fn view_prompt(state: &State) -> Element { }; let text_input = match &state.question { - Some(question) => text_input(&question, &state.answer) + Some(question) => text_input(question, &state.answer) .style(question_style) .on_input(Message::InputChanged) .on_submit(Message::Answer(question.to_owned(), state.answer.clone())), @@ -269,19 +314,17 @@ fn view_prompt(state: &State) -> Element { .on_submit(interpreter::Message::SendCmd(state.answer.clone()).into()), }; - container(text_input).height(40).into() + container(text_input.size(16.0)).height(40).into() } -fn view_bottom_panel(state: &State) -> Element { - row![ - text("Tempo:"), - number_input(state.midi_player_state.tempo(), 20..=600, Message::SetTempo,).step(1).width(60.0), - text("BPM") - ] - .spacing(10.0) - .align_y(iced::Alignment::Center) - .height(70.0) - .into() +fn pick_directory(title: &str) -> Option { + // let initial_dir = env::current_dir().unwrap_or_default(); + FileDialog::new() + .set_title(title) + // .set_directory(initial_dir) + .set_can_create_directories(true) + .pick_folder() + .map(|pb| pb.to_string_lossy().to_string()) } /// The iced message type. @@ -297,6 +340,7 @@ pub enum Message { ChangePlayingPosition(usize, f64), Tick(time::Instant), SetTempo(u16), + SetScratchDir, } impl From for Message { diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 2f39731..f04b586 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -16,6 +16,7 @@ mod xml_tools_ext; /// Global interpreter representation. pub static INTERPRETER_WORKER: LazyLock = LazyLock::new(InterpreterWorker::run); +pub(crate) type InterpreterResult = Result; /// A worker which keeps the interpreter on a dedicated thread and provides communication with it /// via channels. @@ -50,15 +51,20 @@ impl InterpreterWorker { loop { if let Ok(message) = r.recv_blocking() { - if let Message::SendCmd(cmd) = message { - let msg = match interpreter.run_cmd(&cmd) { - Ok(msg) => Message::Post(msg), - Err(Error::Command(_, cmd_err)) => Message::Error(cmd_err), - Err(Error::PythonError(err)) => Message::PythonError(err), - }; - - s.send_blocking(msg).expect("cannot send message to gui"); + let msg = match message { + Message::SendCmd(cmd) => interpreter.run_cmd(&cmd).map(Message::Post), + Message::GetScratchDir => { + interpreter.scratch_dir().map(Message::ScratchDir) + } + Message::SetScratchDir(value) => interpreter + .set_scratch_dir(&value) + .map(|_| Message::ScratchDir(value)), + _ => continue, } + .map_err(Into::::into) + .unwrap(); + + s.send_blocking(msg).expect("cannot send message to gui"); } } }); @@ -92,27 +98,44 @@ pub enum Message { /// /// The value is the path to the file. LoadMidi(String), + /// Get scratch dir. + GetScratchDir, + /// Set scratch dir. + SetScratchDir(String), + /// The result of `Self::GetScratchDir`. + ScratchDir(String), } -pub(crate) type InterpreterResult = Result; +impl From for Message { + fn from(value: Error) -> Self { + match value { + Error::Command(_, cmd_err) => Message::Error(cmd_err), + Error::PythonError(err) => Message::PythonError(err), + } + } +} struct Interpreter { py_interpreter: PyInterpreter, ath_interpreter: PyObjectRef, + ath_object: PyObjectRef, } impl Interpreter { fn new() -> InterpreterResult { let py_interpreter = init_py_interpreter(); - let ath_interpreter = Self::init_ath_interpreter(&py_interpreter)?; + let (ath_interpreter, ath_object) = Self::init_ath_interpreter(&py_interpreter)?; Ok(Self { py_interpreter: init_py_interpreter(), ath_interpreter, + ath_object, }) } - fn init_ath_interpreter(interpreter: &PyInterpreter) -> InterpreterResult { - interpreter.enter(|vm| -> InterpreterResult { + fn init_ath_interpreter( + interpreter: &PyInterpreter, + ) -> InterpreterResult<(PyObjectRef, PyObjectRef)> { + interpreter.enter(|vm| -> InterpreterResult<(PyObjectRef, PyObjectRef)> { let scope = vm.new_scope_with_builtins(); let module = vm::py_compile!( source = r#"from athenaCL.libATH import athenaObj @@ -122,7 +145,10 @@ interp"# let _ = vm .run_code_obj(vm.ctx.new_code(module), scope.clone()) .try_py()?; - scope.globals.get_item("interp", vm).try_py() + let interp = scope.globals.get_item("interp", vm).try_py()?; + let ath_object = interp.get_attr("ao", vm).try_py()?; + + Ok((interp, ath_object)) }) } @@ -132,7 +158,7 @@ interp"# let result = vm .call_method(&self.ath_interpreter, "cmd", (cmd.to_string(),)) .try_py()?; - let (is_ok, msg) = extract_tuple(vm, result).try_py()?; + let (is_ok, msg) = extract_result_tuple(vm, result).try_py()?; if is_ok { Ok(msg) @@ -141,6 +167,33 @@ interp"# } }) } + + fn set_scratch_dir(&self, path: &str) -> InterpreterResult<()> { + self.py_interpreter.enter(|vm| -> InterpreterResult<()> { + let external = vm + .get_attribute_opt(self.ath_object.clone(), "external") + .try_py()? + .expect("external attribute is always available on AthenaObject"); + vm.call_method(&external, "writePref", ("athena", "fpScratchDir", path)) + .try_py()?; + Ok(()) + }) + } + + fn scratch_dir(&self) -> InterpreterResult { + self.py_interpreter + .enter(|vm| -> InterpreterResult { + let external = vm + .get_attribute_opt(self.ath_object.clone(), "external") + .try_py()? + .expect("external attribute is always available on AthenaObject"); + let result = vm + .call_method(&external, "getPref", ("athena", "fpScratchDir")) + .try_py()?; + + extract_string(vm, result).try_py() + }) + } } /// Initialize the python interpreter with precompiled stdlib and athenaCL (python modules). @@ -156,7 +209,7 @@ pub fn init_py_interpreter() -> PyInterpreter { }) } -fn extract_tuple(vm: &VirtualMachine, result: PyObjectRef) -> PyResult<(bool, String)> { +fn extract_result_tuple(vm: &VirtualMachine, result: PyObjectRef) -> PyResult<(bool, String)> { // Ensure the result is a tuple if let Some(tuple) = result.payload::() { let elements = tuple.as_slice(); @@ -187,6 +240,13 @@ fn extract_tuple(vm: &VirtualMachine, result: PyObjectRef) -> PyResult<(bool, St } } +fn extract_string(vm: &VirtualMachine, result: PyObjectRef) -> PyResult { + result + .payload::() + .ok_or_else(|| vm.new_type_error("Expected a string".to_owned())) + .map(ToString::to_string) +} + trait TryPy { type Output; diff --git a/src/main.rs b/src/main.rs index e7b0376..6225a17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ //! The executable. use athenacl::app; -use iced_aw::iced_fonts; fn main() -> iced::Result { iced::application("athenaCL", app::update, app::view) @@ -20,5 +19,6 @@ fn main() -> iced::Result { ..Default::default() }) .font(iced_fonts::REQUIRED_FONT_BYTES) + .font(iced_fonts::NERD_FONT_BYTES) .run() }