Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PyTauri #119

Open
AriBermeki opened this issue Jun 17, 2024 · 5 comments
Open

PyTauri #119

AriBermeki opened this issue Jun 17, 2024 · 5 comments

Comments

@AriBermeki
Copy link

Please contact me via email at [ malek.ali@yellow-sic.com ]. I plan to develop a Python version of the Tauri framework. The attached screenshot shows the use of PyWry, Next.js and Vite. There are urgent tasks that need to be handled by PyWry.

Screenshot 2024-06-17 052411


Screenshot 2024-06-17 054028

@andrewkenreich
Copy link
Collaborator

Can you explain here what you are requesting to happen? @AriBermeki

@AriBermeki
Copy link
Author

AriBermeki commented Jun 18, 2024

Init Static Directory

without fastapi server

Screenshot 2024-06-19 154852

with headless = True

Screenshot 2024-06-19 161453


I strongly assume that the problem lies in the headless.rs file. To solve the problem, one needs to implement a code, like the fastapi code shown underneath,

It could be that this functionality is already implemented, but I'm not sure how to achieve it. I would like pywry to have a HTTP GET method that returns static files as a file response. This GET method should look for the requested file in a python directory. If the file exists, it should be returned as a file response; otherwise a 404 error code (not found) should be returned.

This should be done as part of the default router example.
The endpoint @app.get("/") should return an HTML document as HTML response,
otherwise a 404 error code should be returned. Similarly, @app.get("/{asset_path}")
hould return the corresponding file or a 404 error.

Request example:


<script src="/_next/static/main.js"></script>
<link rel="stylesheet" href="/_next/static/min.css">

Or

<script src="/assets/main.js"></script>
<link rel="stylesheet" href="/assets/min.css">

Python example:


from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pathlib import Path

app = FastAPI()

index_html_path = Path("path/to/index.html")
directory_ = Path("path/to/static/files")
app.mount("/static", StaticFiles(directory=directory_ , html=True), name="static")
@app.get("/")
async def get_index():
    if not index_html_path.is_file():
        raise HTTPException(status_code=404, detail="File not found")
    return FileResponse(index_html_path, media_type='text/html')

@app.get("/{asset_path:path}")
async def get_assets(asset_path: str):
    asset_file_path = directory_ / asset_path
    if not asset_file_path.is_file():
        raise HTTPException(status_code=404, detail="File not found")
    return FileResponse(asset_file_path)

maybe this code will be helpful for you


tauri assets system

https://github.com/tauri-apps/tauri/blob/dev/core/tauri-utils/src/assets.rs

tauri html system


https://github.com/tauri-apps/tauri/blob/dev/core/tauri-utils/src/html.rs


tauri mime_type system


https://github.com/tauri-apps/tauri/blob/dev/core/tauri-utils/src/mime_type.rs


IPC communication between Rust and Python:


It would be very helpful if json_data could forward events sent via IPC
from the frontend back to the Python domain, e.g. via a socket. That means we need a Rust method or function that can
be called from the Python side to set communication parameters such as the communication
port or host, so that Wry on the Rust side knows which channels to use for communication with Python.

IPC example in Rust:


use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;

    loop {
        let (mut socket, _) = listener.accept().await?;

        tokio::spawn(async move {
            let mut buf = [0; 1024];

            // In a loop, read data from the socket and write the data back.
            loop {
                let n = match socket.read(&mut buf).await {
                    // socket closed
                    Ok(n) if n == 0 => return,
                    Ok(n) => n,
                    Err(e) => {
                        eprintln!("failed to read from socket; err = {:?}", e);
                        return;
                    }
                };

                // Write the data back
                if let Err(e) = socket.write_all(&buf[0..n]).await {
                    eprintln!("failed to write to socket; err = {:?}", e);
                    return;
                }
            }
        });
    }
}

IPC example in Python:


import socket

def communicate_with_rust_server(message: str, server_ip: str = '127.0.0.1', server_port: int = 8080):
    # Create a TCP/IP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Connect the socket to the server's port
    server_address = (server_ip, server_port)
    print(f'Connecting to {server_ip}:{server_port}')
    sock.connect(server_address)

    try:
        # Send data
        print(f'Sending: {message}')
        sock.sendall(message.encode('utf-8'))

        # Look for the response (we'll receive the same data back from the server)
        amount_received = 0
        amount_expected = len(message)

        received_data = []

        while amount_received < amount_expected:
            data = sock.recv(1024)
            amount_received += len(data)
            received_data.append(data)

        print(f'Received: {"".join([chunk.decode("utf-8") for chunk in received_data])}')

    finally:
        print('Closing connection')
        sock.close()

# Example usage:
if __name__ == '__main__':
    communicate_with_rust_server("Hello, Rust server!")

Event control between frontend and backend:


When the frontend wants to trigger a Tao event, it sends the event name via IPC to Wry with a Pywry frontend command.
Wry then sends it via IPC to Python, which receives the message and sends a Tao event as a dictionary back to Wry,
which then triggers it with the iterateJsonObjectAndAttachTaoEventHandler function.

this is an example of how to implement iterateJsonObjectAndAttachTaoEventHandler function in javascript but it must be done in rust


const synthesizeTaoEventHandler = (event, e) => {
  switch(event) {
    case "onclick":
      console.log("Clicked!", e);
      return;
    // Weitere Fälle hier hinzufügen
  }
};

const iterateJsonObjectAndAttachTaoEventHandler = (json_object, eventList) => {
  const event_object = { ...json_object };
  for (const propName in event_object) {
    if (eventList.includes(propName)) {
      const eventTypeValue = event_object[propName];
      event_object[propName] = (e) => synthesizeTaoEventHandler(eventTypeValue, e);
    }
  }
  return event_object;
};


const jsonObject = {
  event_type: "onclick",
  event_props: {},
  window_id: "value"
};

const eventList = ["onclick", "onmouseover"];

const taoevent = iterateJsonObjectAndAttachTaoEventHandler(jsonObject, eventList);

Rust example


i don't know if they have the same eefecte


use serde_json::Value;

fn synthesize_tao_event_handler(event: &str, e: &Value) {
    match event {
        "onclick" => {
            println!("Clicked! {:?}", e);
        }
        // Add more cases here as needed
        _ => {}
    }
}

fn iterate_json_object_and_attach_tao_event_handler(
    json_object: &[Value],
    event_list: &[&str],
) -> Vec<Value> {
    let mut event_object = json_object.to_vec(); // Make a mutable copy of the input

    for obj in &mut event_object {
        if let Some(event_type_value) = obj.get_mut("event_type") {
            if let Some(event_type_str) = event_type_value.as_str() {
                if event_list.contains(&event_type_str) {
                    let handler_value = serde_json::json!({
                        "handler": event_type_str
                    });
                    *event_type_value = handler_value;
                }
            }
        }
    }

    event_object
}

fn main() {
    let json_data = r#"
    [
        {
            "event_type": "onclick",
            "event_props": {
                "widith": 800,
                "height": 600
            },
            "window_id": 1
        },
        {
            "event_type": "onclick",
            "event_props": {
                "widith": 800,
                "height": 600
            },
            "window_id": 1
        },
        {
            "event_type": "onclick",
            "event_props": {
                "widith": 800,
                "height": 600
            },
            "window_id": 1
        },
        {
            "event_type": "onclick",
            "event_props": {
                "widith": 800,
                "height": 600
            },
            "window_id": 1
        }
    ]
    "#;

    // Parse JSON data into a Vec<Value>
    let json_object: Vec<Value> = serde_json::from_str(json_data).unwrap();

    let event_list = vec!["onclick", "onmouseover"];

    let taoevent = iterate_json_object_and_attach_tao_event_handler(&json_object, &event_list);

    // Example usage of the handlers
    for handler_obj in &taoevent {
        if let Some(handler) = handler_obj.get("handler") {
            if let Value::String(event_type_value) = handler {
                synthesize_tao_event_handler(event_type_value, &Value::Null);
            }
        }
    }

    println!("{:?}", taoevent);
}



Rust functions for executing Python functions:


We need two Rust functions that can call Python functions (e.g. startup, shutdown)
when starting or stopping pywry.


These functions are useful and important to take
advantage of Python without starting a separate Python web server that requires additional effort and runtime.


Additional Note:


I assume you have developed pywry for a specific purpose. In my view the purpose is to use Python features for desktop applications without having to use Rust. I am trying to link javascript files via pywry. This does not work for me. I get a file not found error. I believe there will be developers who will get a similar problem. Please help me to solve the problem and extend the functionality of IPC. I would like to send real-time data (via IPC) back and forth.

My knowledge of Rust is almost zero, but I have experience in full-stack development and I know how web technology works in Python. I have experience with FastAPI, Flask, and Django. I believe Pywry has this ability without developing a Python web server, that requires a lot of work and runtime. I wrote these things so that we don't have to create an additional Python web server that runs in the background.

@AriBermeki
Copy link
Author

@andrewkenreich

@AriBermeki
Copy link
Author

AriBermeki commented Jun 22, 2024

@andrewkenreich

I have solved the first problem (Init Static Directory)


Your headless.rs file must have this response system. This system currently works for me without FastAPI only rust


neu_response


[dependencies]
mime_guess = "2.0.4"
tao = "0.28.1"
wry = "0.41.0"

cargo add mime_guess
use std::path::PathBuf;

use tao::{
    event::{Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    window::WindowBuilder,
};
use wry::{
    http::{header::CONTENT_TYPE, Request, Response},
    WebViewBuilder,
};
use mime_guess::from_path;

fn main() -> wry::Result<()> {
    let event_loop = EventLoop::new();
    let window = WindowBuilder::new()
        .build(&event_loop).unwrap();

    #[cfg(any(
        target_os = "windows",
        target_os = "macos",
        target_os = "ios",
        target_os = "android"
    ))]
    let builder = WebViewBuilder::new(&window);

    #[cfg(not(any(
        target_os = "windows",
        target_os = "macos",
        target_os = "ios",
        target_os = "android"
    )))]
    let builder = {
        use tao::platform::unix::WindowExtUnix;
        use wry::WebViewBuilderExtUnix;
        let vbox = window.default_vbox().unwrap();
        WebViewBuilder::new_gtk(vbox)
    };

    // the variables must be accessible from the python side and initialization script must also be able to interact with this url
    // with pywry.setup() function
    let root_path = PathBuf::from("../out");
    let frame_port = 3000;
    let development = true;
    let development_url = format!("http://localhost:{}", frame_port);
    let index_page = "index.html";

    let builder = builder
        .with_devtools(true)
        .with_hotkeys_zoom(true)
        .with_custom_protocol(
            "wry".into(), 
            move |request| {
                match get_pywry_response_protocol(request, index_page, &root_path) {
                    Ok(r) => r.map(Into::into),
                    Err(e) => Response::builder()
                        .header(CONTENT_TYPE, "text/plain")
                        .status(500)
                        .body(e.to_string().as_bytes().to_vec())
                        .unwrap()
                        .map(Into::into),
                }
            },
        );

    let builder = if development {
        builder.with_url(&development_url)
    } else {
        builder.with_url("wry://localhost")
    };

    let _webview = builder.build()?;

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

        if let Event::WindowEvent {
            event: WindowEvent::CloseRequested,
            ..
        } = event
        {
            *control_flow = ControlFlow::Exit
        }
    });
}

fn get_pywry_response_protocol(
    request: Request<Vec<u8>>,
    index_page: &str,
    root_path: &PathBuf
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
    let path = request.uri().path();
    // Read the file content from file path

    let path = if path == "/" {
        index_page
    } else {
        // Removing leading slash
        &path[1..]
    };
    let full_path = std::fs::canonicalize(root_path.join(path))?;
    let content = std::fs::read(&full_path)?;
    #[cfg(target_os = "windows")]
    let headers = "https://wry.localhost".to_string();
    #[cfg(not(target_os = "windows"))]
    let headers = "wry://localhost".to_string();
    // Determine MIME type using `mime_guess`
    let mime_type = from_path(&full_path).first_or_octet_stream();

    Response::builder()
        .header(CONTENT_TYPE, mime_type.as_ref())
        .header("Access-Control-Allow-Origin", headers)
        .header("Cache-Control", "no-cache, no-store, must-revalidate")
        .header("Pragma", "no-cache")
        .header("Expires", "0")
        .header("Accept-Encoding", "gzip, compress, br, deflate")
        .body(content)
        .map_err(Into::into)
}

@camux
Copy link

camux commented Sep 28, 2024

Please contact me via email at [ malek.ali@yellow-sic.com ]. I plan to develop a Python version of the Tauri framework. The attached screenshot shows the use of PyWry, Next.js and Vite. There are urgent tasks that need to be handled by PyWry.

Screenshot 2024-06-17 052411 Screenshot 2024-06-17 054028

Why do not it in Mojo (faster language and closer to python) instead Python?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants