Skip to content

Commit

Permalink
Add scratch dir button
Browse files Browse the repository at this point in the history
  • Loading branch information
ales-tsurko committed Oct 27, 2024
1 parent 5cf195d commit 5c6acc2
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 31 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
74 changes: 59 additions & 15 deletions src/app/app.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,6 +29,7 @@ pub struct State {
output: Vec<Output>,
question: Option<String>,
midi_player_state: GlobalMidiPlayerState,
scratch_dir: String,
}

impl Default for State {
Expand All @@ -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(),
}
}
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -191,6 +217,7 @@ pub fn view(state: &State) -> Element<Message> {
);

container(column![
view_top_panel(state),
scrollable(output.padding(20.0))
.style(|theme: &iced::Theme, status: Status| {
let mut style = theme.style(&<iced::Theme as Catalog>::default(), status);
Expand All @@ -207,14 +234,32 @@ pub fn view(state: &State) -> Element<Message> {
.height(iced::Length::Fill)
.anchor_bottom(),
view_prompt(state),
view_bottom_panel(state),
])
.padding(40)
.width(TERM_WIDTH * FONT_WIDTH)
.height(iced::Length::Fill)
.into()
}

fn view_top_panel(state: &State) -> Element<Message> {
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<Message> {
match output {
Output::Normal(msg) => container(text(msg)),
Expand Down Expand Up @@ -259,7 +304,7 @@ fn view_prompt(state: &State) -> Element<Message> {
};

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())),
Expand All @@ -269,19 +314,17 @@ fn view_prompt(state: &State) -> Element<Message> {
.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<Message> {
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<String> {
// 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.
Expand All @@ -297,6 +340,7 @@ pub enum Message {
ChangePlayingPosition(usize, f64),
Tick(time::Instant),
SetTempo(u16),
SetScratchDir,
}

impl From<interpreter::Message> for Message {
Expand Down
90 changes: 75 additions & 15 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod xml_tools_ext;

/// Global interpreter representation.
pub static INTERPRETER_WORKER: LazyLock<InterpreterWorker> = LazyLock::new(InterpreterWorker::run);
pub(crate) type InterpreterResult<T> = Result<T, Error>;

/// A worker which keeps the interpreter on a dedicated thread and provides communication with it
/// via channels.
Expand Down Expand Up @@ -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::<Message>::into)
.unwrap();

s.send_blocking(msg).expect("cannot send message to gui");
}
}
});
Expand Down Expand Up @@ -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<T> = Result<T, Error>;
impl From<Error> 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<Self> {
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<PyObjectRef> {
interpreter.enter(|vm| -> InterpreterResult<PyObjectRef> {
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
Expand All @@ -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))
})
}

Expand All @@ -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)
Expand All @@ -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<String> {
self.py_interpreter
.enter(|vm| -> InterpreterResult<String> {
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).
Expand All @@ -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::<PyTuple>() {
let elements = tuple.as_slice();
Expand Down Expand Up @@ -187,6 +240,13 @@ fn extract_tuple(vm: &VirtualMachine, result: PyObjectRef) -> PyResult<(bool, St
}
}

fn extract_string(vm: &VirtualMachine, result: PyObjectRef) -> PyResult<String> {
result
.payload::<PyStr>()
.ok_or_else(|| vm.new_type_error("Expected a string".to_owned()))
.map(ToString::to_string)
}

trait TryPy {
type Output;

Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -20,5 +19,6 @@ fn main() -> iced::Result {
..Default::default()
})
.font(iced_fonts::REQUIRED_FONT_BYTES)
.font(iced_fonts::NERD_FONT_BYTES)
.run()
}

0 comments on commit 5c6acc2

Please sign in to comment.