Skip to content

Commit

Permalink
Satisfy warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
ales-tsurko committed Oct 28, 2024
1 parent d7b326e commit edba035
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 273 deletions.
2 changes: 1 addition & 1 deletion src/app/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use iced::futures::sink::SinkExt;
use iced::stream;
use iced::widget::{
button, column, container, container::Style as ContainerStyle, row, scrollable, stack, text,
button, column, container, container::Style as ContainerStyle, row, scrollable, text,
text::Style as TextStyle, text_input,
};
use iced::{time, Element, Font, Subscription, Task};
Expand Down
7 changes: 2 additions & 5 deletions src/app/midi_player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@ use iced::{
};
use midi_player::{Player, PlayerController, Settings as PlayerSettings};

const PLAY_SVG_ICON: &str = include_str!("../../resources/img/play.svg");
const PAUSE_SVG_ICON: &str = include_str!("../../resources/img/pause.svg");

pub(crate) struct GlobalState {
pub(crate) controller: PlayerController,
audio_stream: AudioStream,
_audio_stream: AudioStream,
pub(crate) playing_id: Option<usize>,
pub(crate) tempo: u16,
}
Expand All @@ -30,7 +27,7 @@ impl GlobalState {

Self {
controller,
audio_stream,
_audio_stream: audio_stream,
playing_id: None,
tempo: 120,
}
Expand Down
2 changes: 2 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! This module keeps GUI-related stuff.
pub use app::*;

mod app;
Expand Down
270 changes: 270 additions & 0 deletions src/interpreter/interpreter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
//! athenaCL interpreter.
use std::sync::LazyLock;
use std::thread;

use async_channel::{unbounded, Receiver, Sender};
use rustpython_vm as vm;
use thiserror::Error;
use vm::{
builtins::{PyBaseExceptionRef, PyInt, PyStr, PyTuple},
Interpreter as PyInterpreter, PyObjectRef, PyResult, VirtualMachine,
};

use super::{xml_tools_ext, dialog_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.
#[derive(Debug)]
pub struct InterpreterWorker {
pub interp_sender: Sender<Message>,
pub gui_sender: Sender<Message>,
pub gui_receiver: Receiver<Message>,
/// Response sender/receiver is a special channel dedicated for sending user's answer to
/// `Message::Ask`.
pub response_sender: Sender<String>,
/// Response sender/receiver is a special channel dedicated for sending user's answer to
/// `Message::Ask`.
pub response_receiver: Receiver<String>,
}

impl InterpreterWorker {
/// Run the interpereter loop.
fn run() -> Self {
let (interp_sender, r) = unbounded::<Message>();
let (gui_sender, gui_receiver) = unbounded::<Message>();
let s = gui_sender.clone();

let _ = thread::spawn(move || {
let interpreter = Interpreter::new().unwrap_or_else(|err| {
s.send_blocking(Message::PythonError(err.to_string()))
.expect("can't send message to channel");
panic!("error initializating interpreter");
});

loop {
if let Ok(message) = r.recv_blocking() {
let msg = match message {
Message::SendCmd(cmd) => interpreter.run_cmd(&cmd).map(Message::Post),
Message::GetScratchDir => {
interpreter.scratch_dir().map(Message::ScratchDir)
}
_ => continue,
}
.into();

s.send_blocking(msg).expect("cannot send message to gui");
}
}
});

let (response_sender, response_receiver) = unbounded();

Self {
interp_sender,
gui_sender,
gui_receiver,
response_sender,
response_receiver,
}
}
}

#[derive(Debug, Clone)]
pub enum Message {
/// Output from the interpreter (stdout).
Post(String),
/// Request input from the user (stdin).
Ask(String),
/// Send command to the interpreter.
SendCmd(String),
/// Error from the interpreter (stderr).
Error(String),
/// Python's interpreter- level errors.
PythonError(String),
/// Play a MIDI file (in the output area).
///
/// The value is the path to the file.
LoadMidi(String),
/// Get scratch dir.
GetScratchDir,
/// The result of `Self::GetScratchDir`.
ScratchDir(String),
}

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),
}
}
}

impl From<InterpreterResult<Message>> for Message {
fn from(value: InterpreterResult<Message>) -> Self {
match value {
Ok(msg) => msg,
Err(e) => Self::from(e),
}
}
}

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, 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, 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
interp = athenaObj.Interpreter()
interp"#
);
let _ = vm
.run_code_obj(vm.ctx.new_code(module), scope.clone())
.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))
})
}

fn run_cmd(&self, cmd: &str) -> InterpreterResult<String> {
self.py_interpreter
.enter(|vm| -> InterpreterResult<String> {
let result = vm
.call_method(&self.ath_interpreter, "cmd", (cmd.to_string(),))
.try_py()?;
let (is_ok, msg) = extract_result_tuple(vm, result).try_py()?;

if is_ok {
Ok(msg)
} else {
Err(Error::Command(cmd.to_owned(), msg))
}
})
}

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).
pub fn init_py_interpreter() -> PyInterpreter {
let mut settings = vm::Settings::default();
settings.optimize = 2;
PyInterpreter::with_init(settings, |vm| {
vm.add_native_modules(rustpython_stdlib::get_module_inits());
vm.add_frozen(rustpython_pylib::FROZEN_STDLIB);
vm.add_frozen(vm::py_freeze!(dir = "pysrc"));
xml_tools_ext::make_module(vm);
dialog_ext::make_module(vm);
})
}

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();

// Ensure the tuple has exactly 2 elements (Integer, String)
if elements.len() == 2 {
// Extract and convert the first element to i32
let int_part = elements[0]
.payload::<PyInt>()
.ok_or_else(|| vm.new_type_error("Expected an integer".to_owned()))?
.as_bigint();

let bool_part = *int_part != 0.into();

// Extract and convert the second element to String
let str_part = elements[1]
.payload::<PyStr>()
.ok_or_else(|| vm.new_type_error("Expected a string".to_owned()))?
.as_str()
.to_owned();

Ok((bool_part, str_part))
} else {
Err(vm.new_value_error("Expected a tuple of length 2".to_owned()))
}
} else {
Err(vm.new_type_error("Expected a tuple".to_owned()))
}
}

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;

fn try_py(self) -> InterpreterResult<Self::Output>;
}

impl<T> TryPy for PyResult<T> {
type Output = T;

fn try_py(self) -> InterpreterResult<T> {
self.map_err(|e| Error::from_py_err(e))
}
}

#[derive(Debug, Error, Clone)]
pub enum Error {
#[error("{0}")]
PythonError(String),
#[error("Error running command `{0}`: {1}")]
Command(String, String),
}

impl Error {
fn from_py_err(err: PyBaseExceptionRef) -> Self {
let message = err
.get_arg(0)
.as_ref()
.and_then(|arg| arg.downcast_ref::<PyStr>())
.map(|s| s.as_str().to_owned())
.unwrap_or_else(|| "Unknown (silent) error".to_string());

Self::PythonError(message)
}
}
Loading

0 comments on commit edba035

Please sign in to comment.