commit edaffdb868355ad3f667f4edea32aab5f32f36f7 Author: Marc-André Lureau Date: Sat Jan 23 20:03:56 2021 +0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cc18dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +Cargo.lock +.DS_Store +.idea +*.log diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e12251b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +members = ["qemu-display-listener", "qemu-gtk4"] + +#[patch.crates-io] +#zbus = { path = '/home/elmarco/src/zbus/zbus' } + diff --git a/qemu-display-listener/.gitignore b/qemu-display-listener/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/qemu-display-listener/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/qemu-display-listener/Cargo.toml b/qemu-display-listener/Cargo.toml new file mode 100644 index 0000000..cd1e31a --- /dev/null +++ b/qemu-display-listener/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "qemu-display-listener" +version = "0.1.0" +authors = ["Marc-André Lureau "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +zbus = "2.0.0-beta" +derivative = "2.1.3" +zvariant = "2.4.0" +libc = "0.2.86" +glib = { git = "https://github.com/gtk-rs/gtk-rs", optional = true } diff --git a/qemu-display-listener/src/console.rs b/qemu-display-listener/src/console.rs new file mode 100644 index 0000000..fe3964c --- /dev/null +++ b/qemu-display-listener/src/console.rs @@ -0,0 +1,121 @@ +use std::cell::RefCell; +use std::os::unix::net::UnixStream; +use std::rc::Rc; +use std::sync::mpsc::{self, Receiver}; +use std::{os::unix::io::AsRawFd, thread}; + +use zbus::{dbus_proxy, export::zvariant::Fd}; + +use crate::Result; +use crate::{Event, Listener}; + +#[dbus_proxy(default_service = "org.qemu", interface = "org.qemu.Display1.Console")] +pub trait Console { + /// RegisterListener method + fn register_listener(&self, listener: Fd) -> zbus::Result<()>; + + #[dbus_proxy(property)] + fn label(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn head(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn type_(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn width(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn height(&self) -> zbus::Result; +} + +#[derive(derivative::Derivative)] +#[derivative(Debug)] +pub struct Console<'c> { + #[derivative(Debug = "ignore")] + proxy: ConsoleProxy<'c>, +} + +impl<'c> Console<'c> { + pub fn new(conn: &zbus::Connection, idx: u32) -> Result { + let proxy = ConsoleProxy::new_for_owned_path( + conn.clone(), + format!("/org/qemu/Display1/Console_{}", idx), + )?; + Ok(Self { proxy }) + } + + pub fn label(&self) -> Result { + Ok(self.proxy.label()?) + } + + pub fn width(&self) -> Result { + Ok(self.proxy.width()?) + } + + pub fn height(&self) -> Result { + Ok(self.proxy.height()?) + } + + pub fn listen(&self) -> Result> { + let (p0, p1) = UnixStream::pair()?; + let (tx, rx) = mpsc::channel(); + self.proxy.register_listener(p0.as_raw_fd().into())?; + + let _thread = thread::spawn(move || { + let c = zbus::Connection::new_unix_client(p1, false).unwrap(); + let mut s = zbus::ObjectServer::new(&c); + let err = Rc::new(RefCell::new(None)); + s.at( + &zvariant::ObjectPath::from_str_unchecked("/org/qemu/Display1/Listener"), + Listener::new(tx, err.clone()), + ) + .unwrap(); + loop { + if let Err(e) = s.try_handle_next() { + eprintln!("Listener DBus error: {}", e); + return; + } + if let Some(e) = &*err.borrow() { + eprintln!("Listener channel error: {}", e); + return; + } + } + }); + + Ok(rx) + } +} + +#[cfg(feature = "glib")] +impl<'c> Console<'c> { + pub fn glib_listen(&self) -> Result> { + let (p0, p1) = UnixStream::pair()?; + let (tx, rx) = glib::MainContext::channel(glib::source::Priority::default()); + self.proxy.register_listener(p0.as_raw_fd().into())?; + + let _thread = thread::spawn(move || { + let c = zbus::Connection::new_unix_client(p1, false).unwrap(); + let mut s = zbus::ObjectServer::new(&c); + let err = Rc::new(RefCell::new(None)); + s.at( + &zvariant::ObjectPath::from_str_unchecked("/org/qemu/Display1/Listener"), + Listener::new(tx, err.clone()), + ) + .unwrap(); + loop { + if let Err(e) = s.try_handle_next() { + eprintln!("Listener DBus error: {}", e); + return; + } + if let Some(e) = &*err.borrow() { + eprintln!("Listener channel error: {}", e); + return; + } + } + }); + + Ok(rx) + } +} diff --git a/qemu-display-listener/src/error.rs b/qemu-display-listener/src/error.rs new file mode 100644 index 0000000..f5246bf --- /dev/null +++ b/qemu-display-listener/src/error.rs @@ -0,0 +1,50 @@ +use std::error; +use std::fmt; +use std::io; + +#[derive(Debug)] +pub enum Error { + Io(io::Error), + Zbus(zbus::Error), + Zvariant(zvariant::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Io(e) => write!(f, "{}", e), + Error::Zbus(e) => write!(f, "{}", e), + Error::Zvariant(e) => write!(f, "{}", e), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Error::Io(e) => Some(e), + Error::Zbus(e) => Some(e), + Error::Zvariant(e) => Some(e), + } + } +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: zbus::Error) -> Self { + Error::Zbus(e) + } +} + +impl From for Error { + fn from(e: zvariant::Error) -> Self { + Error::Zvariant(e) + } +} + +pub type Result = std::result::Result; diff --git a/qemu-display-listener/src/lib.rs b/qemu-display-listener/src/lib.rs new file mode 100644 index 0000000..3a3e5d5 --- /dev/null +++ b/qemu-display-listener/src/lib.rs @@ -0,0 +1,21 @@ +#![allow(clippy::too_many_arguments)] + +mod error; +pub use error::*; + +mod vm; +pub use vm::*; + +mod console; +pub use console::*; + +mod listener; +pub use listener::*; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/qemu-display-listener/src/listener.rs b/qemu-display-listener/src/listener.rs new file mode 100644 index 0000000..b356ab3 --- /dev/null +++ b/qemu-display-listener/src/listener.rs @@ -0,0 +1,124 @@ +use std::cell::RefCell; +use std::os::unix::io::{AsRawFd, RawFd}; +use std::rc::Rc; +use std::sync::mpsc::{SendError, Sender}; + +use zbus::{dbus_interface, export::zvariant::Fd}; + +// TODO: replace events mpsc with async traits +#[derive(Debug)] +pub enum Event { + Switch { + width: i32, + height: i32, + }, + Update { + x: i32, + y: i32, + w: i32, + h: i32, + }, + Scanout { + fd: RawFd, + width: u32, + height: u32, + stride: u32, + fourcc: u32, + modifier: u64, + y0_top: bool, + }, + MouseSet { + x: i32, + y: i32, + on: i32, + }, + CursorDefine { + width: i32, + height: i32, + hot_x: i32, + hot_y: i32, + data: Vec, + }, +} + +pub(crate) trait EventSender { + fn send_event(&self, t: Event) -> Result<(), SendError>; +} + +impl EventSender for Sender { + fn send_event(&self, t: Event) -> Result<(), SendError> { + self.send(t) + } +} + +#[cfg(feature = "glib")] +impl EventSender for glib::Sender { + fn send_event(&self, t: Event) -> Result<(), SendError> { + self.send(t) + } +} + +#[derive(Debug)] +pub(crate) struct Listener { + tx: E, + err: Rc>>>, +} + +#[dbus_interface(name = "org.qemu.Display1.Listener")] +impl Listener { + fn switch(&mut self, width: i32, height: i32) { + self.send(Event::Switch { width, height }) + } + + fn update(&mut self, x: i32, y: i32, w: i32, h: i32) { + self.send(Event::Update { x, y, w, h }) + } + + fn scanout( + &mut self, + fd: Fd, + width: u32, + height: u32, + stride: u32, + fourcc: u32, + modifier: u64, + y0_top: bool, + ) { + let fd = unsafe { libc::dup(fd.as_raw_fd()) }; + self.send(Event::Scanout { + fd, + width, + height, + stride, + fourcc, + modifier, + y0_top, + }) + } + + fn mouse_set(&mut self, x: i32, y: i32, on: i32) { + self.send(Event::MouseSet { x, y, on }) + } + + fn cursor_define(&mut self, width: i32, height: i32, hot_x: i32, hot_y: i32, data: Vec) { + self.send(Event::CursorDefine { + width, + height, + hot_x, + hot_y, + data, + }) + } +} + +impl Listener { + pub(crate) fn new(tx: E, err: Rc>>>) -> Self { + Listener { tx, err } + } + + fn send(&mut self, event: Event) { + if let Err(e) = self.tx.send_event(event) { + *self.err.borrow_mut() = Some(e); + } + } +} diff --git a/qemu-display-listener/src/vm.rs b/qemu-display-listener/src/vm.rs new file mode 100644 index 0000000..5d301c4 --- /dev/null +++ b/qemu-display-listener/src/vm.rs @@ -0,0 +1,16 @@ +use zbus::dbus_proxy; + +#[dbus_proxy( + default_service = "org.qemu", + interface = "org.qemu.Display1.VM", + default_path = "/org/qemu/Display1/VM" +)] +pub trait VM { + /// Name property + #[dbus_proxy(property)] + fn name(&self) -> zbus::Result; + + /// UUID property + #[dbus_proxy(property)] + fn uuid(&self) -> zbus::Result; +} diff --git a/qemu-gtk4/.editorconfig b/qemu-gtk4/.editorconfig new file mode 100644 index 0000000..e23188f --- /dev/null +++ b/qemu-gtk4/.editorconfig @@ -0,0 +1,13 @@ +root = true +[*] +indent_style = space +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 + +[*.{build,yml,ui,yaml}] +indent_size = 2 + +[*.{json,py}] +indent_size = 4 + diff --git a/qemu-gtk4/.github/workflows/ci.yml b/qemu-gtk4/.github/workflows/ci.yml new file mode 100644 index 0000000..677efed --- /dev/null +++ b/qemu-gtk4/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +on: + push: + branches: [master] + pull_request: +name: CI +jobs: + flatpak: + name: "Flatpak" + runs-on: ubuntu-20.04 + container: + image: bilelmoussaoui/flatpak-github-actions:gnome-3.38 + options: --privileged + steps: + - uses: actions/checkout@v2 + - uses: bilelmoussaoui/flatpak-github-actions@v2 + with: + bundle: "qemu-gtk4.flatpak" + manifest-path: "build-aux/org.qemu.gtk4.Devel.json" + run-tests: "true" diff --git a/qemu-gtk4/.gitignore b/qemu-gtk4/.gitignore new file mode 100644 index 0000000..ab60d21 --- /dev/null +++ b/qemu-gtk4/.gitignore @@ -0,0 +1,10 @@ +target/ +build/ +_build/ +builddir/ +build-aux/app +build-aux/.flatpak-builder/ +src/config.rs +*.ui.in~ +*.ui~ +.flatpak/ diff --git a/qemu-gtk4/.gitlab-ci.yml b/qemu-gtk4/.gitlab-ci.yml new file mode 100644 index 0000000..6f59d57 --- /dev/null +++ b/qemu-gtk4/.gitlab-ci.yml @@ -0,0 +1,39 @@ +stages: + - check + - test + +flatpak: + image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/rust_bundle:3.38' + stage: test + tags: + - flatpak + variables: + BUNDLE: "qemu-gtk4-nightly.flatpak" + MANIFEST_PATH: "build-aux/org.qemu.gtk4.Devel.json" + FLATPAK_MODULE: "qemu-gtk4" + APP_ID: "org.qemu.gtk4.Devel" + RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo" + script: + - > + xvfb-run -a -s "-screen 0 1024x768x24" + flatpak-builder --keep-build-dirs --user --disable-rofiles-fuse flatpak_app --repo=repo ${BRANCH:+--default-branch=$BRANCH} ${MANIFEST_PATH} + - flatpak build-bundle repo ${BUNDLE} --runtime-repo=${RUNTIME_REPO} ${APP_ID} ${BRANCH} + artifacts: + name: 'Flatpak artifacts' + expose_as: 'Get Flatpak bundle here' + when: 'always' + paths: + - "${BUNDLE}" + - '.flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-logs/meson-log.txt' + - '.flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-logs/testlog.txt' + expire_in: 14 days + +# Configure and run rustfmt +# Exits and builds fails if on bad format +rustfmt: + image: "rust:slim" + script: + - rustup component add rustfmt + - rustc -Vv && cargo -Vv + - cargo fmt --version + - cargo fmt --all -- --color=always --check diff --git a/qemu-gtk4/Cargo.toml b/qemu-gtk4/Cargo.toml new file mode 100644 index 0000000..f23eb69 --- /dev/null +++ b/qemu-gtk4/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "qemu-gtk4" +version = "0.1.0" +authors = ["QEMU developpers "] +edition = "2018" + +[dependencies] +qemu-display-listener = { path = "../qemu-display-listener", features = ["glib"] } +zbus = { version = "2.0.0-beta" } +log = "0.4" +pretty_env_logger = "0.4" +gettext-rs = { version = "0.5", features = ["gettext-system"] } +gtk-macros = "0.2" +once_cell = "1.5" + +[dependencies.gtk] +package = "gtk4" +git = "https://github.com/gtk-rs/gtk4-rs" +rev = "abea0c9980bc083494eceb30dfab5eeb99a73118" diff --git a/qemu-gtk4/LICENSE.md b/qemu-gtk4/LICENSE.md new file mode 100644 index 0000000..3844e57 --- /dev/null +++ b/qemu-gtk4/LICENSE.md @@ -0,0 +1,7 @@ +Copyright © 2019, QEMU developpers +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders X be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software. +Except as contained in this notice, the name of the QEMU developpers shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the QEMU developpers. \ No newline at end of file diff --git a/qemu-gtk4/README.md b/qemu-gtk4/README.md new file mode 100644 index 0000000..514499d --- /dev/null +++ b/qemu-gtk4/README.md @@ -0,0 +1,5 @@ +# QEMU Gtk4 + +## Credits + +Based on [GTK Rust template](https://gitlab.gnome.org/bilelmoussaoui/gtk-rust-template) diff --git a/qemu-gtk4/build-aux/cargo.sh b/qemu-gtk4/build-aux/cargo.sh new file mode 100644 index 0000000..42bfb1e --- /dev/null +++ b/qemu-gtk4/build-aux/cargo.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +export MESON_BUILD_ROOT="$1" +export MESON_SOURCE_ROOT="$2" +export CARGO_TARGET_DIR="$MESON_BUILD_ROOT"/target +export CARGO_HOME="$CARGO_TARGET_DIR"/cargo-home + +if [[ $4 = "Devel" ]] +then + echo "DEBUG MODE" + cargo build --manifest-path \ + "$MESON_SOURCE_ROOT"/Cargo.toml && \ + cp "$CARGO_TARGET_DIR"/debug/$5 $3 +else + echo "RELEASE MODE" + cargo build --manifest-path \ + "$MESON_SOURCE_ROOT"/Cargo.toml --release && \ + cp "$CARGO_TARGET_DIR"/release/$5 $3 +fi + diff --git a/qemu-gtk4/build-aux/dist-vendor.sh b/qemu-gtk4/build-aux/dist-vendor.sh new file mode 100644 index 0000000..be73278 --- /dev/null +++ b/qemu-gtk4/build-aux/dist-vendor.sh @@ -0,0 +1,10 @@ +#!/bin/bash +export DIST="$1" +export SOURCE_ROOT="$2" + +cd "$SOURCE_ROOT" +mkdir "$DIST"/.cargo +cargo vendor | sed 's/^directory = ".*"/directory = "vendor"/g' > $DIST/.cargo/config +# Move vendor into dist tarball directory +mv vendor "$DIST" + diff --git a/qemu-gtk4/build-aux/meson_post_install.py b/qemu-gtk4/build-aux/meson_post_install.py new file mode 100755 index 0000000..9028aec --- /dev/null +++ b/qemu-gtk4/build-aux/meson_post_install.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +from os import environ, path +from subprocess import call + +if not environ.get('DESTDIR', ''): + PREFIX = environ.get('MESON_INSTALL_PREFIX', '/usr/local') + DATA_DIR = path.join(PREFIX, 'share') + print('Updating icon cache...') + call(['gtk-update-icon-cache', '-qtf', path.join(DATA_DIR, 'icons/hicolor')]) + print("Compiling new schemas...") + call(["glib-compile-schemas", path.join(DATA_DIR, 'glib-2.0/schemas')]) + print("Updating desktop database...") + call(["update-desktop-database", path.join(DATA_DIR, 'applications')]) diff --git a/qemu-gtk4/build-aux/org.qemu.gtk4.Devel.json b/qemu-gtk4/build-aux/org.qemu.gtk4.Devel.json new file mode 100644 index 0000000..d68ab26 --- /dev/null +++ b/qemu-gtk4/build-aux/org.qemu.gtk4.Devel.json @@ -0,0 +1,104 @@ +{ + "app-id": "org.qemu.gtk4.Devel", + "runtime": "org.gnome.Platform", + "runtime-version": "3.38", + "sdk": "org.gnome.Sdk", + "sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"], + "command": "qemu-gtk4", + "finish-args" : [ + "--socket=fallback-x11", + "--socket=wayland", + "--device=dri", + "--talk-name=org.a11y.Bus", + "--env=RUST_LOG=qemu-gtk4=debug", + "--env=G_MESSAGES_DEBUG=none" + ], + "build-options" : { + "append-path" : "/usr/lib/sdk/rust-stable/bin", + "build-args" : [ + "--share=network" + ], + "test-args": [ + "--socket=x11", + "--share=network" + ], + "env" : { + "CARGO_HOME" : "/run/build/qemu-gtk4/cargo", + "RUST_BACKTRACE": "1", + "RUSTFLAGS": "-L=/app/lib" + } + }, + "modules": [ + { + "name": "gtk4", + "buildsystem": "meson", + "config-opts": [ + "-Ddemos=false", + "-Dbuild-examples=false", + "-Dbuild-tests=false" + ], + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/gtk/4.0/gtk-4.0.3.tar.xz", + "sha256": "d7c9893725790b50bd9a3bb278856d9d543b44b6b9b951d7b60e7bdecc131890" + } + ], + "modules": [ + { + "name": "pango", + "buildsystem": "meson", + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/pango/1.48/pango-1.48.1.tar.xz", + "sha256": "08c2d550a96559f15fb317d7167b96df57ef743fef946f4e274bd8b6f2918058" + } + ] + }, + { + "name": "libsass", + "sources": [ + { + "type": "archive", + "url": "https://github.com/sass/libsass/archive/3.6.4.tar.gz", + "sha256": "f9484d9a6df60576e791566eab2f757a97fd414fce01dd41fc0a693ea5db2889" + }, + { + "type": "script", + "dest-filename": "autogen.sh", + "commands": ["autoreconf -si"] + } + ] + }, + { + "name": "sassc", + "sources": [ + { + "type": "archive", + "url": "https://github.com/sass/sassc/archive/3.6.1.tar.gz", + "sha256": "8cee391c49a102b4464f86fc40c4ceac3a2ada52a89c4c933d8348e3e4542a60" + }, + { + "type": "script", + "dest-filename": "autogen.sh", + "commands": ["autoreconf -si"] + } + ] + } + ] + }, + { + "name": "qemu-gtk4", + "buildsystem": "meson", + "run-tests": true, + "config-opts": ["-Dprofile=development"], + "sources": [ + { + "type": "dir", + "path": "../" + } + ] + } + ] +} diff --git a/qemu-gtk4/data/icons/meson.build b/qemu-gtk4/data/icons/meson.build new file mode 100644 index 0000000..2ab86e9 --- /dev/null +++ b/qemu-gtk4/data/icons/meson.build @@ -0,0 +1,10 @@ +install_data( + '@0@.svg'.format(application_id), + install_dir: iconsdir / 'hicolor' / 'scalable' / 'apps' +) + +install_data( + '@0@-symbolic.svg'.format(base_id), + install_dir: iconsdir / 'hicolor' / 'symbolic' / 'apps', + rename: '@0@-symbolic.svg'.format(application_id) +) diff --git a/qemu-gtk4/data/icons/org.qemu.gtk4-symbolic.svg b/qemu-gtk4/data/icons/org.qemu.gtk4-symbolic.svg new file mode 100644 index 0000000..fc4d934 --- /dev/null +++ b/qemu-gtk4/data/icons/org.qemu.gtk4-symbolic.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qemu-gtk4/data/icons/org.qemu.gtk4.Devel.svg b/qemu-gtk4/data/icons/org.qemu.gtk4.Devel.svg new file mode 100644 index 0000000..92533ae --- /dev/null +++ b/qemu-gtk4/data/icons/org.qemu.gtk4.Devel.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qemu-gtk4/data/icons/org.qemu.gtk4.svg b/qemu-gtk4/data/icons/org.qemu.gtk4.svg new file mode 100644 index 0000000..c2bd5b1 --- /dev/null +++ b/qemu-gtk4/data/icons/org.qemu.gtk4.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qemu-gtk4/data/meson.build b/qemu-gtk4/data/meson.build new file mode 100644 index 0000000..cc26f98 --- /dev/null +++ b/qemu-gtk4/data/meson.build @@ -0,0 +1,83 @@ +subdir('icons') +# Desktop file +desktop_conf = configuration_data() +desktop_conf.set('icon', application_id) +desktop_file = i18n.merge_file( + type: 'desktop', + input: configure_file( + input: '@0@.desktop.in.in'.format(base_id), + output: '@BASENAME@', + configuration: desktop_conf + ), + output: '@0@.desktop'.format(application_id), + po_dir: podir, + install: true, + install_dir: datadir / 'applications' +) +# Validate Desktop file +if desktop_file_validate.found() + test( + 'validate-desktop', + desktop_file_validate, + args: [ + desktop_file.full_path() + ] + ) +endif + +# Appdata +appdata_conf = configuration_data() +appdata_conf.set('app-id', application_id) +appdata_conf.set('gettext-package', gettext_package) +appdata_file = i18n.merge_file( + input: configure_file( + input: '@0@.metainfo.xml.in.in'.format(base_id), + output: '@BASENAME@', + configuration: appdata_conf + ), + output: '@0@.metainfo.xml'.format(application_id), + po_dir: podir, + install: true, + install_dir: datadir / 'metainfo' +) +# Validate Appdata +if appstream_util.found() + test( + 'validate-appdata', appstream_util, + args: [ + 'validate', '--nonet', appdata_file.full_path() + ] + ) +endif + +# GSchema +gschema_conf = configuration_data() +gschema_conf.set('app-id', application_id) +gschema_conf.set('gettext-package', gettext_package) +configure_file( + input: '@0@.gschema.xml.in'.format(base_id), + output: '@0@.gschema.xml'.format(application_id), + configuration: gschema_conf, + install: true, + install_dir: datadir / 'glib-2.0' / 'schemas' +) + +# Validata GSchema +if glib_compile_schemas.found() + test( + 'validate-gschema', glib_compile_schemas, + args: [ + '--strict', '--dry-run', meson.current_source_dir() + ] + ) +endif + +# Resources +resources = gnome.compile_resources( + 'resources', + 'resources.gresource.xml', + gresource_bundle: true, + source_dir: meson.current_build_dir(), + install: true, + install_dir: pkgdatadir, +) diff --git a/qemu-gtk4/data/org.qemu.gtk4.desktop.in.in b/qemu-gtk4/data/org.qemu.gtk4.desktop.in.in new file mode 100644 index 0000000..b17ae0b --- /dev/null +++ b/qemu-gtk4/data/org.qemu.gtk4.desktop.in.in @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=QEMU Gtk +Comment=A GTK + Rust application boilerplate template +Type=Application +Exec=qemu-gtk4 +Terminal=false +Categories=GNOME;GTK; +Keywords=Gnome;GTK; +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=@icon@ +StartupNotify=true diff --git a/qemu-gtk4/data/org.qemu.gtk4.gschema.xml.in b/qemu-gtk4/data/org.qemu.gtk4.gschema.xml.in new file mode 100644 index 0000000..a1fbd27 --- /dev/null +++ b/qemu-gtk4/data/org.qemu.gtk4.gschema.xml.in @@ -0,0 +1,20 @@ + + + + + -1 + Default window width + Default window width + + + -1 + Default window height + Default window height + + + false + Default window maximized behaviour + + + + diff --git a/qemu-gtk4/data/org.qemu.gtk4.metainfo.xml.in.in b/qemu-gtk4/data/org.qemu.gtk4.metainfo.xml.in.in new file mode 100644 index 0000000..c53c4f8 --- /dev/null +++ b/qemu-gtk4/data/org.qemu.gtk4.metainfo.xml.in.in @@ -0,0 +1,30 @@ + + + + @app-id@ + CC0 + GPL-3.0+ + QEMU Gtk + A GTK QEMU display. + +

GTK application to interact with QEMU display.

+
+ https://gitlab.com/qemu-project/qemu + https://gitlab.com/qemu-project/qemu/issues + + + + + + + ModernToolkit + HiDpiIcon + + QEMU developpers + qemu-devel@nongnu.org + @gettext-package@ + @app-id@.desktop +
diff --git a/qemu-gtk4/data/resources.gresource.xml b/qemu-gtk4/data/resources.gresource.xml new file mode 100644 index 0000000..b8abc9a --- /dev/null +++ b/qemu-gtk4/data/resources.gresource.xml @@ -0,0 +1,9 @@ + + + + resources/ui/shortcuts.ui + resources/ui/window.ui + + resources/style.css + + diff --git a/qemu-gtk4/data/resources/style.css b/qemu-gtk4/data/resources/style.css new file mode 100644 index 0000000..3c4bd47 --- /dev/null +++ b/qemu-gtk4/data/resources/style.css @@ -0,0 +1,4 @@ +.title-header{ + font-size: 36px; + font-weight: bold; +} diff --git a/qemu-gtk4/data/resources/ui/shortcuts.ui b/qemu-gtk4/data/resources/ui/shortcuts.ui new file mode 100644 index 0000000..d5dff5e --- /dev/null +++ b/qemu-gtk4/data/resources/ui/shortcuts.ui @@ -0,0 +1,30 @@ + + + + True + + + shortcuts + 10 + + + General + + + Show Shortcuts + <Primary>question + + + + + Quit + <Primary>Q + + + + + + + + + diff --git a/qemu-gtk4/data/resources/ui/window.ui b/qemu-gtk4/data/resources/ui/window.ui new file mode 100644 index 0000000..be3fb3c --- /dev/null +++ b/qemu-gtk4/data/resources/ui/window.ui @@ -0,0 +1,40 @@ + + +
+ + _Preferences + app.preferences + + + _Keyboard Shortcuts + win.show-help-overlay + + + _About GTK QEMU + app.about + +
+
+ +
diff --git a/qemu-gtk4/hooks/pre-commit.hook b/qemu-gtk4/hooks/pre-commit.hook new file mode 100755 index 0000000..363b8d5 --- /dev/null +++ b/qemu-gtk4/hooks/pre-commit.hook @@ -0,0 +1,54 @@ +#!/bin/sh +# Source: https://gitlab.gnome.org/GNOME/fractal/blob/master/hooks/pre-commit.hook + +install_rustfmt() { + if ! which rustup &> /dev/null; then + curl https://sh.rustup.rs -sSf | sh -s -- -y + export PATH=$PATH:$HOME/.cargo/bin + if ! which rustup &> /dev/null; then + echo "Failed to install rustup. Performing the commit without style checking." + exit 0 + fi + fi + + if ! rustup component list|grep rustfmt &> /dev/null; then + echo "Installing rustfmt…" + rustup component add rustfmt + fi +} + +if ! which cargo &> /dev/null || ! cargo fmt --help &> /dev/null; then + echo "Unable to check Fractal’s code style, because rustfmt could not be run." + + if [ ! -t 1 ]; then + # No input is possible + echo "Performing commit." + exit 0 + fi + + echo "" + echo "y: Install rustfmt via rustup" + echo "n: Don't install rustfmt and perform the commit" + echo "Q: Don't install rustfmt and abort the commit" + + while true; do + read -p "Install rustfmt via rustup? [y/n/Q]: " yn + case $yn in + [Yy]* ) install_rustfmt; break;; + [Nn]* ) echo "Performing commit."; exit 0;; + [Qq]* | "" ) echo "Aborting commit."; exit -1;; + * ) echo "Invalid input";; + esac + done +fi + +echo "--Checking style--" +cargo fmt --all -- --check +if test $? != 0; then + echo "--Checking style fail--" + echo "Please fix the above issues, either manually or by running: cargo fmt --all" + + exit -1 +else + echo "--Checking style pass--" +fi diff --git a/qemu-gtk4/meson.build b/qemu-gtk4/meson.build new file mode 100644 index 0000000..0474896 --- /dev/null +++ b/qemu-gtk4/meson.build @@ -0,0 +1,70 @@ +project('qemu-gtk4', + 'rust', + version: '0.0.1', + license: 'MIT', + meson_version: '>= 0.50') + +i18n = import('i18n') +gnome = import('gnome') + +base_id = 'org.qemu.gtk4' + +dependency('glib-2.0', version: '>= 2.66') +dependency('gio-2.0', version: '>= 2.66') +dependency('gtk4', version: '>= 4.0.0') + +glib_compile_resources = find_program('glib-compile-resources', required: true) +glib_compile_schemas = find_program('glib-compile-schemas', required: true) +desktop_file_validate = find_program('desktop-file-validate', required: false) +appstream_util = find_program('appstream-util', required: false) +cargo = find_program('cargo', required: true) +cargo_script = find_program('build-aux/cargo.sh') + +version = meson.project_version() +version_array = version.split('.') +major_version = version_array[0].to_int() +minor_version = version_array[1].to_int() +version_micro = version_array[2].to_int() + +prefix = get_option('prefix') +bindir = prefix / get_option('bindir') +localedir = prefix / get_option('localedir') + +datadir = prefix / get_option('datadir') +pkgdatadir = datadir / meson.project_name() +iconsdir = datadir / 'icons' +podir = meson.source_root() / 'po' +gettext_package = meson.project_name() + +if get_option('profile') == 'development' + profile = 'Devel' + vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip() + if vcs_tag == '' + version_suffix = '-devel' + else + version_suffix = '-@0@'.format(vcs_tag) + endif + application_id = '@0@.@1@'.format(base_id, profile) +else + profile = '' + version_suffix = '' + application_id = base_id +endif + +meson.add_dist_script( + 'build-aux/dist-vendor.sh', + meson.build_root() / 'meson-dist' / meson.project_name() + '-' + version, + meson.source_root() +) + +if get_option('profile') == 'development' + # Setup pre-commit hook for ensuring coding style is always consistent + message('Setting up git pre-commit hook..') + run_command('cp', '-f', 'hooks/pre-commit.hook', '.git/hooks/pre-commit') +endif + +subdir('data') +subdir('po') +subdir('src') + +meson.add_install_script('build-aux/meson_post_install.py') diff --git a/qemu-gtk4/meson_options.txt b/qemu-gtk4/meson_options.txt new file mode 100644 index 0000000..abf708f --- /dev/null +++ b/qemu-gtk4/meson_options.txt @@ -0,0 +1,10 @@ +option( + 'profile', + type: 'combo', + choices: [ + 'default', + 'development' + ], + value: 'default', + description: 'The build profile for QEMU Gtk. One of "default" or "development".' +) diff --git a/qemu-gtk4/po/LINGUAS b/qemu-gtk4/po/LINGUAS new file mode 100644 index 0000000..e69de29 diff --git a/qemu-gtk4/po/POTFILES.in b/qemu-gtk4/po/POTFILES.in new file mode 100644 index 0000000..8ead562 --- /dev/null +++ b/qemu-gtk4/po/POTFILES.in @@ -0,0 +1,5 @@ +data/resources/ui/shortcuts.ui +data/resources/ui/window.ui.in +data/org.qemu.gtk4.desktop.in.in +data/org.qemu.gtk4.gschema.xml.in +data/org.qemu.gtk4.metainfo.xml.in.in diff --git a/qemu-gtk4/po/meson.build b/qemu-gtk4/po/meson.build new file mode 100644 index 0000000..57d1266 --- /dev/null +++ b/qemu-gtk4/po/meson.build @@ -0,0 +1 @@ +i18n.gettext(gettext_package, preset: 'glib') diff --git a/qemu-gtk4/src/application.rs b/qemu-gtk4/src/application.rs new file mode 100644 index 0000000..2963ef4 --- /dev/null +++ b/qemu-gtk4/src/application.rs @@ -0,0 +1,170 @@ +use crate::config; +use crate::window::QemuApplicationWindow; +use gio::ApplicationFlags; +use glib::clone; +use glib::WeakRef; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{gdk, gio, glib}; +use gtk_macros::action; +use log::{debug, info}; +use once_cell::sync::OnceCell; +use std::env; + +use qemu_display_listener::Console; +use zbus::Connection; + +mod imp { + use super::*; + use glib::subclass; + + #[derive(Debug)] + pub struct QemuApplication { + pub window: OnceCell>, + pub conn: OnceCell, + } + + impl ObjectSubclass for QemuApplication { + const NAME: &'static str = "QemuApplication"; + type Type = super::QemuApplication; + type ParentType = gtk::Application; + type Interfaces = (); + type Instance = subclass::simple::InstanceStruct; + type Class = subclass::simple::ClassStruct; + + glib::object_subclass!(); + + fn new() -> Self { + Self { + window: OnceCell::new(), + conn: OnceCell::new(), + } + } + } + + impl ObjectImpl for QemuApplication {} + + impl gio::subclass::prelude::ApplicationImpl for QemuApplication { + fn activate(&self, app: &Self::Type) { + debug!("GtkApplication::activate"); + + let priv_ = QemuApplication::from_instance(app); + if let Some(window) = priv_.window.get() { + let window = window.upgrade().unwrap(); + window.show(); + window.present(); + return; + } + + app.set_resource_base_path(Some("/org/qemu/gtk4/")); + app.setup_css(); + + let conn = Connection::new_session().expect("Failed to connect"); + let console = Console::new(&conn, 0).expect("Failed to get the console"); + self.conn.set(conn).expect("Connection already set."); + + let window = QemuApplicationWindow::new(app, console); + self.window + .set(window.downgrade()) + .expect("Window already set."); + + app.setup_gactions(); + app.setup_accels(); + + app.get_main_window().present(); + } + + fn startup(&self, app: &Self::Type) { + debug!("GtkApplication::startup"); + self.parent_startup(app); + } + } + + impl GtkApplicationImpl for QemuApplication {} +} + +glib::wrapper! { + pub struct QemuApplication(ObjectSubclass) + @extends gio::Application, gtk::Application, @implements gio::ActionMap, gio::ActionGroup; +} + +impl QemuApplication { + pub fn new() -> Self { + glib::Object::new(&[ + ("application-id", &Some(config::APP_ID)), + ("flags", &ApplicationFlags::empty()), + ]) + .expect("Application initialization failed...") + } + + fn get_main_window(&self) -> QemuApplicationWindow { + let priv_ = imp::QemuApplication::from_instance(self); + priv_.window.get().unwrap().upgrade().unwrap() + } + + fn setup_gactions(&self) { + // Quit + action!( + self, + "quit", + clone!(@weak self as app => move |_, _| { + // This is needed to trigger the delete event + // and saving the window state + app.get_main_window().close(); + app.quit(); + }) + ); + + // About + action!( + self, + "about", + clone!(@weak self as app => move |_, _| { + app.show_about_dialog(); + }) + ); + } + + // Sets up keyboard shortcuts + fn setup_accels(&self) { + self.set_accels_for_action("app.quit", &["q"]); + self.set_accels_for_action("win.show-help-overlay", &["question"]); + } + + fn setup_css(&self) { + let provider = gtk::CssProvider::new(); + provider.load_from_resource("/org/qemu/gtk4/style.css"); + if let Some(display) = gdk::Display::get_default() { + gtk::StyleContext::add_provider_for_display( + &display, + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + } + } + + fn show_about_dialog(&self) { + let dialog = gtk::AboutDialogBuilder::new() + .program_name("QEMU Gtk") + .logo_icon_name(config::APP_ID) + .license_type(gtk::License::MitX11) + .website("https://gitlab.com/qemu-project/qemu/") + .version(config::VERSION) + .transient_for(&self.get_main_window()) + .modal(true) + .authors(vec!["QEMU developpers".into()]) + .artists(vec!["QEMU developpers".into()]) + .build(); + + dialog.show(); + } + + pub fn run(&self) { + info!("QEMU Gtk ({})", config::APP_ID); + info!("Version: {} ({})", config::VERSION, config::PROFILE); + info!("Datadir: {}", config::PKGDATADIR); + + let args: Vec = env::args().collect(); + ApplicationExtManual::run(self, &args); + } +} diff --git a/qemu-gtk4/src/config.rs.in b/qemu-gtk4/src/config.rs.in new file mode 100644 index 0000000..699897f --- /dev/null +++ b/qemu-gtk4/src/config.rs.in @@ -0,0 +1,7 @@ +pub const APP_ID: &str = @APP_ID@; +pub const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@; +pub const LOCALEDIR: &str = @LOCALEDIR@; +pub const PKGDATADIR: &str = @PKGDATADIR@; +pub const PROFILE: &str = @PROFILE@; +pub const RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/resources.gresource"); +pub const VERSION: &str = @VERSION@; diff --git a/qemu-gtk4/src/main.rs b/qemu-gtk4/src/main.rs new file mode 100644 index 0000000..5fdbe5f --- /dev/null +++ b/qemu-gtk4/src/main.rs @@ -0,0 +1,32 @@ +#[allow(clippy::new_without_default)] + +mod application; +#[rustfmt::skip] +mod config; +mod window; + +use application::QemuApplication; +use config::{GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE}; +use gettextrs::*; +use gtk::gio; + +fn main() { + // Initialize logger, debug is carried out via debug!, info!, and warn!. + pretty_env_logger::init(); + + // Prepare i18n + setlocale(LocaleCategory::LcAll, ""); + bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR); + textdomain(GETTEXT_PACKAGE); + + gtk::glib::set_application_name("QEMU Gtk"); + gtk::glib::set_prgname(Some("qemu-gtk4")); + + gtk::init().expect("Unable to start GTK4"); + + let res = gio::Resource::load(RESOURCES_FILE).expect("Could not load gresource file"); + gio::resources_register(&res); + + let app = QemuApplication::new(); + app.run(); +} diff --git a/qemu-gtk4/src/meson.build b/qemu-gtk4/src/meson.build new file mode 100644 index 0000000..bc737b2 --- /dev/null +++ b/qemu-gtk4/src/meson.build @@ -0,0 +1,45 @@ +global_conf = configuration_data() +global_conf.set_quoted('APP_ID', application_id) +global_conf.set_quoted('PKGDATADIR', pkgdatadir) +global_conf.set_quoted('PROFILE', profile) +global_conf.set_quoted('VERSION', version + version_suffix) +global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package) +global_conf.set_quoted('LOCALEDIR', localedir) +config = configure_file( + input: 'config.rs.in', + output: 'config.rs', + configuration: global_conf +) +# Copy the config.rs output to the source directory. +run_command( + 'cp', + meson.build_root() / 'src' / 'config.rs', + meson.source_root() / 'src' / 'config.rs', + check: true +) + +sources = files( + 'application.rs', + 'config.rs', + 'main.rs', + 'window.rs', +) + +custom_target( + 'cargo-build', + build_by_default: true, + input: sources, + output: meson.project_name(), + console: true, + install: true, + install_dir: bindir, + depends: resources, + command: [ + cargo_script, + meson.build_root(), + meson.source_root(), + '@OUTPUT@', + profile, + meson.project_name(), + ] +) diff --git a/qemu-gtk4/src/window.rs b/qemu-gtk4/src/window.rs new file mode 100644 index 0000000..e06f365 --- /dev/null +++ b/qemu-gtk4/src/window.rs @@ -0,0 +1,140 @@ +use crate::application::QemuApplication; +use crate::config::{APP_ID, PROFILE}; +use glib::clone; +use glib::signal::Inhibit; +use gtk::subclass::prelude::*; +use gtk::{self, prelude::*}; +use gtk::{gio, glib, CompositeTemplate}; +use log::warn; + +use qemu_display_listener::Console; + +mod imp { + use super::*; + use glib::subclass; + + #[derive(Debug, CompositeTemplate)] + #[template(resource = "/org/qemu/gtk4/window.ui")] + pub struct QemuApplicationWindow { + #[template_child] + pub headerbar: TemplateChild, + #[template_child] + pub label: TemplateChild, + pub settings: gio::Settings, + } + + impl ObjectSubclass for QemuApplicationWindow { + const NAME: &'static str = "QemuApplicationWindow"; + type Type = super::QemuApplicationWindow; + type ParentType = gtk::ApplicationWindow; + type Interfaces = (); + type Instance = subclass::simple::InstanceStruct; + type Class = subclass::simple::ClassStruct; + + glib::object_subclass!(); + + fn new() -> Self { + Self { + headerbar: TemplateChild::default(), + label: TemplateChild::default(), + settings: gio::Settings::new(APP_ID), + } + } + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + // You must call `Widget`'s `init_template()` within `instance_init()`. + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for QemuApplicationWindow { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + + let builder = gtk::Builder::from_resource("/org/qemu/gtk4/shortcuts.ui"); + let shortcuts = builder.get_object("shortcuts").unwrap(); + obj.set_help_overlay(Some(&shortcuts)); + + // Devel Profile + if PROFILE == "Devel" { + obj.get_style_context().add_class("devel"); + } + + // load latest window state + obj.load_window_size(); + } + } + + impl WindowImpl for QemuApplicationWindow { + // save window state on delete event + fn close_request(&self, obj: &Self::Type) -> Inhibit { + if let Err(err) = obj.save_window_size() { + warn!("Failed to save window state, {}", &err); + } + Inhibit(false) + } + } + + impl WidgetImpl for QemuApplicationWindow {} + impl ApplicationWindowImpl for QemuApplicationWindow {} +} + +glib::wrapper! { + pub struct QemuApplicationWindow(ObjectSubclass) + @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, @implements gio::ActionMap, gio::ActionGroup; +} + +impl QemuApplicationWindow { + pub fn new(app: &QemuApplication, console: Console) -> Self { + let window: Self = glib::Object::new(&[]).expect("Failed to create QemuApplicationWindow"); + window.set_application(Some(app)); + + // Set icons for shell + gtk::Window::set_default_icon_name(APP_ID); + + let rx = console + .glib_listen() + .expect("Failed to listen to the console"); + rx.attach( + None, + clone!(@weak window as win => move |t| { + let label = &imp::QemuApplicationWindow::from_instance(&win).label; + label.set_text(&format!("{:?}", t)); + Continue(true) + }), + ); + + window + } + + pub fn save_window_size(&self) -> Result<(), glib::BoolError> { + let settings = &imp::QemuApplicationWindow::from_instance(self).settings; + + let size = self.get_default_size(); + + settings.set_int("window-width", size.0)?; + settings.set_int("window-height", size.1)?; + + settings.set_boolean("is-maximized", self.is_maximized())?; + + Ok(()) + } + + fn load_window_size(&self) { + let settings = &imp::QemuApplicationWindow::from_instance(self).settings; + + let width = settings.get_int("window-width"); + let height = settings.get_int("window-height"); + let is_maximized = settings.get_boolean("is-maximized"); + + self.set_default_size(width, height); + + if is_maximized { + self.maximize(); + } + } +}