Trying to finally implement GUI for desktop
All checks were successful
gitea/Frida/pipeline/head This commit looks good

modified:   Cargo.lock
	modified:   frida_core/Cargo.toml
	modified:   frida_gui/Cargo.toml
	renamed:    frida_core/build.rs -> frida_gui/build.rs
	renamed:    frida_core/icons/off.ico -> frida_gui/icons/off.ico
	renamed:    frida_core/icons/on.ico -> frida_gui/icons/on.ico
	new file:   frida_gui/icons/on.raw
	deleted:    frida_gui/src/gui/mod.rs
	deleted:    frida_gui/src/gui/tab/mod.rs
	deleted:    frida_gui/src/gui/tab_button.rs
	deleted:    frida_gui/src/gui/tab_panel.rs
	modified:   frida_gui/src/main.rs
	new file:   frida_gui/src/toggle_switch.rs
	renamed:    frida_core/tray.rc -> frida_gui/tray.rc
This commit is contained in:
Michael Wain 2025-01-22 23:08:24 +03:00
parent b8c9d1250f
commit 39d9e14ddb
14 changed files with 1622 additions and 1341 deletions

2429
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -23,14 +23,8 @@ log = { workspace = true }
tokio = { workspace = true }
[target.'cfg(target_os="windows")'.dependencies]
iced = { version = "0.13.1", features = ["tokio"] }
dirs = "6.0.0"
tray-item = "0.10.0"
wintun = "0.5.0"
[target.'cfg(target_os="windows")'.build-dependencies]
embed-resource = "3.0.1"
[target.'cfg(target_os="macos")'.dependencies]
nix = { version = "0.29.0", features = ["socket"] }

View File

@ -14,3 +14,17 @@ name = "frida-gui"
path = "src/gui/mod.rs"
[dependencies]
frida_core = { path = "../frida_core", package = "frida_core" }
frida_client = { path = "../frida_client", package = "frida_client" }
tokio = { workspace = true }
log = { workspace = true }
env_logger = { workspace = true }
serde_yaml = { workspace = true }
dirs = "6.0.0"
egui_logger = "0.6.1"
eframe = { version = "0.30.0", features = ["wgpu"] }
egui_extras = { version = "0.30.0", features = ["all_loaders"] }
egui_file = "0.21.0"
[target.'cfg(target_os="windows")'.build-dependencies]
embed-resource = "3.0.1"

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
frida_gui/icons/on.raw Normal file

Binary file not shown.

View File

@ -1,88 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub mod tab;
pub mod tab_button;
pub mod tab_panel;
use std::{
ffi::OsStr,
path::{Path, PathBuf}, sync::mpsc::SyncSender,
borrow::Cow
};
use std::sync::mpsc;
use log::{info, error};
use log::LevelFilter;
use env_logger::Builder;
use tray_item::{IconSource, TrayItem};
use iced::{widget::container, window, Element, Settings, Task as Command};
use iced::widget::{button, column, pick_list, radio, text, Column, Container, scrollable};
use crate::tab_button::TabButton;
use crate::tab_panel::TabPanel;
use crate::tab::Tab;
fn get_configs_dir() -> PathBuf {
let mut p = dirs::home_dir().unwrap();
p.push(".frida");
p
}
#[derive(Debug, Clone)]
pub enum Message {
ButtonPressed(u8),
ChangeUI
}
struct State {
tab_panel: TabPanel,
}
impl State {
fn new() -> Self {
Self { tab_panel: TabPanel::new() }
}
}
enum App {
Preloaded,
Loaded(State)
}
impl App {
pub fn new() -> (Self, Command<Message>) {
(Self::Preloaded, Command::done(Message::ChangeUI))
}
pub fn view(&self) -> Element<Message> {
match self {
App::Preloaded => {
return container(text("Loading")).into();
}
App::Loaded(state) => {
return state.tab_panel.view();
}
}
}
pub fn update(&mut self, message: Message) -> Command<Message> {
match self {
App::Preloaded => {
let mut panel = TabPanel::new();
panel.push_tab(TabButton::new("First", 0), Tab::new());
*self = App::Loaded(State { tab_panel: panel });
return Command::done(Message::ChangeUI);
}
App::Loaded(state) => {
state.tab_panel.update(message);
}
}
Command::none()
}
}
fn main() -> iced::Result {
iced::application("title", App::update, App::view)
.window_size((640.0, 480.0))
.run_with(App::new)
}

View File

@ -1,13 +0,0 @@
use crate::Message;
use iced::{Task as Command, Element};
use iced::widget::{button, column, pick_list, radio, text, Column, Container, scrollable};
pub struct Tab {
}
impl Tab {
pub fn new() -> Self {
Self{}
}
}

View File

@ -1,27 +0,0 @@
use crate::Message;
use iced::{Task as Command, Element};
use iced::widget::{button, column, pick_list, radio, text, Column, Container, scrollable};
#[derive(Debug, Clone)]
pub struct TabButton {
label: String,
pub id: u8,
}
impl TabButton {
pub fn new<S: AsRef<str>>(label: S, id: u8) -> Self {
Self { label: label.as_ref().to_string(), id }
}
pub fn view(&self, selected_id: u8) -> Container<Message> {
let label = text(&self.label);
let button = button(label).style(if self.id == selected_id {
button::text
} else {
button::primary
});
Container::new(button.on_press(Message::ButtonPressed(self.id)).padding(8))
}
}

View File

@ -1,48 +0,0 @@
use crate::Tab;
use crate::TabButton;
use crate::Message;
use iced::{Task as Command, Element};
use iced::widget::{button, column, pick_list, radio, text, Column, Container, scrollable};
pub struct TabPanel {
tabs: Vec<(TabButton, Tab)>,
current_tab: u8
}
impl TabPanel {
pub fn view(&self) -> Element<Message> {
//let selected_tab = self.tabs.iter()
// .filter(|t| t.0.id == self.current_tab)
// .next();
//if selected_tab.is_some() {}
let mut btns: Vec<Element<Message>> = Vec::new();
self.tabs.iter().for_each(|t| {
btns.push(t.0.view(self.current_tab).into());
});
scrollable(iced::widget::Column::with_children(btns)).into()
}
pub fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::ButtonPressed(id) => {
self.current_tab = id;
}
_ => {}
}
Command::none()
}
pub fn new() -> Self {
Self { tabs: Vec::new(), current_tab: 0 }
}
pub fn push_tab(&mut self, button: TabButton, tab: Tab) {
self.tabs.push((button, tab));
}
}

View File

@ -1,3 +1,291 @@
fn main() {
println!("Hello, world!");
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use eframe::egui::{self, Context, ScrollArea, Vec2};
use egui_file::FileDialog;
use tokio::sync::mpsc::UnboundedSender;
use std::{
cell::RefCell, ffi::OsStr, path::{Path, PathBuf}, rc::Rc, sync::Arc, thread
};
use egui_extras::{Column, TableBuilder};
use log::{info, error, LevelFilter};
use frida_core::config::ClientConfiguration;
use frida_client::client::{desktop::DesktopClient, general::VpnClient};
mod toggle_switch;
fn get_configs_dir() -> PathBuf {
let mut p = dirs::home_dir().unwrap();
p.push(".frida");
p
}
enum Message {
Open,
Buzz
}
#[tokio::main]
async fn main() -> Result<(), eframe::Error> {
egui_logger::builder().max_level(LevelFilter::Info).init().unwrap();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]),
..Default::default()
};
std::fs::create_dir_all(get_configs_dir());
let cfgs = std::fs::read_dir(get_configs_dir()).unwrap();
let mut cv = Vec::new();
for path in cfgs {
cv.push(path.unwrap().path());
}
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Option<ClientConfiguration>>();
tokio::spawn(async move {
let mut cl_th = None;
loop {
if let Some(config) = rx.recv().await {
if config.is_some() {
cl_th = Some(tokio::spawn(async move {
let client = DesktopClient{client_config: config.unwrap()};
client.start().await
}));
continue;
}
if cl_th.is_some() {
info!("STOP");
cl_th.unwrap().abort();
cl_th = None;
}
}
}
});
eframe::run_native(
env!("CARGO_PKG_NAME"),
options.clone(),
Box::new(move |cc| {
Ok(Box::new(App::new(cv, tx)))
}),
)
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, PartialEq)]
enum AppScreens {
Configs,
Log
}
struct App {
screen: AppScreens,
configs: Configs,
logs: Logs,
tx: UnboundedSender<Option<ClientConfiguration>>
}
impl App {
fn new(cfgs: Vec<PathBuf>, tx: UnboundedSender<Option<ClientConfiguration>>) -> Self {
Self {
screen: AppScreens::Configs,
configs: Configs::new(cfgs),
logs: Logs::default(),
tx: tx
}
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.screen, AppScreens::Configs, "Configs");
ui.selectable_value(&mut self.screen, AppScreens::Log, "Log");
});
ui.separator();
match self.screen {
AppScreens::Configs => {
self.configs.ui(ui, ctx);
self.configs.process_vpn(&self.tx);
}
AppScreens::Log => {
self.logs.ui(ui, ctx);
}
}
});
}
}
struct Logs {
}
impl Default for Logs {
fn default() -> Self {
Self{}
}
}
impl Logs {
fn ui(&mut self, ui: &mut egui::Ui, ctx: &Context) {
egui::CentralPanel::default()
.show_inside(ui, |ui| {
egui_logger::logger_ui().show(ui);
});
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
#[derive(Debug)]
struct Configs {
num: u32,
prev_btn_status: bool,
btn_status: bool,
open_config_dialog: Option<FileDialog>,
cfgs: Vec<PathBuf>,
selected_cfg: Option<(ClientConfiguration, String)>
}
impl Configs {
fn new(cfgs: Vec<PathBuf>) -> Self {
Self {
num: 32,
btn_status: false,
prev_btn_status: false,
open_config_dialog: None,
cfgs,
selected_cfg: None
}
}
fn ui(&mut self, ui: &mut egui::Ui, ctx: &Context) {
let Self {
num,
prev_btn_status,
btn_status,
open_config_dialog,
cfgs,
selected_cfg
} = self;
egui::SidePanel::left("clist")
.resizable(false)
.exact_width(150.0)
.show_inside(ui, |ui| {
ScrollArea::vertical()
.auto_shrink(false)
.show(ui, |ui| {
ui.set_width(ui.available_width());
self.cfgs.iter().for_each(|f| {
let filename = f.file_name().unwrap().to_str().unwrap();
let mut b = egui::Button::new(filename);
if self.selected_cfg.is_some() && self.selected_cfg.as_ref().unwrap().1 == filename.to_string() {
b = b.fill(egui::Color32::LIGHT_BLUE);
}
let e = ui.add_sized(
Vec2::new(ui.available_width(), 0.0),
b,
);
if e.clicked() {
let data = std::fs::read(f.clone());
let cfg_raw = &String::from_utf8(data.unwrap()).unwrap();
let config: ClientConfiguration = serde_yaml::from_str(cfg_raw).expect("Bad client config file structure");
self.selected_cfg = Some((config, filename.to_string()));
}
});
});
});
egui::CentralPanel::default()
.show_inside(ui, |ui| {
ui.spacing_mut().item_spacing.y = 20.0;
if self.selected_cfg.is_none() { return; }
let cfg = &self.selected_cfg.as_ref().unwrap().0;
ui.group(|ui| {
ui.spacing_mut().item_spacing.y = 5.0;
ui.set_width(ui.available_width());
ui.label(format!("Interface: {}", &self.selected_cfg.as_ref().unwrap().1));
ui.label("Status: inactive");
ui.label(format!("Public key: {}", &cfg.client.public_key));
ui.label(format!("Address: {}", &cfg.client.address));
ui.horizontal(|ui| {
ui.label("Activate: ");
ui.add(crate::toggle_switch::toggle(&mut self.btn_status));
});
});
ui.group(|ui| {
ui.spacing_mut().item_spacing.y = 5.0;
ui.set_width(ui.available_width());
ui.label(format!("Public key: {}", &cfg.server.public_key));
ui.label(format!("Endpoint: {}", &cfg.server.endpoint));
ui.label(format!("Keepalive: {}", &cfg.server.keepalive));
});
});
egui::TopBottomPanel::bottom("btns")
.resizable(false)
.show_inside(ui, |ui| {
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 10.0;
if ui.button("Add config").clicked() {
let filter = Box::new({
let ext = Some(OsStr::new("yaml"));
move |path: &Path| -> bool { path.extension() == ext }
});
let mut dialog = FileDialog::open_file(None).show_files_filter(filter);
dialog.open();
self.open_config_dialog = Some(dialog);
}
if let Some(dialog) = &mut self.open_config_dialog {
if dialog.show(ctx).selected() {
if let Some(file) = dialog.path() {
let mut h = get_configs_dir();
std::fs::create_dir_all(&h);
h.push(file.file_name().unwrap());
std::fs::copy(file, &h);
self.cfgs.push(h);
}
}
}
if ui.button("Remove selected").clicked() {
if self.selected_cfg.is_none() { return; }
let mut fp = get_configs_dir();
fp.push(&self.selected_cfg.as_ref().unwrap().1);
let path = &fp.to_str().unwrap().to_string();
if let Ok(r) = std::fs::remove_file(path) {
for i in 0..self.cfgs.len() {
if &self.selected_cfg.as_ref().unwrap().1 == self.cfgs[i].file_name().unwrap().to_str().unwrap() {
self.cfgs.remove(i);
break;
}
}
self.selected_cfg = None;
}
}
});
});
}
fn process_vpn(&mut self, tx: &UnboundedSender<Option<ClientConfiguration>>) {
if self.btn_status && !self.prev_btn_status {
log::info!("VPN ON");
tx.send(Some(self.selected_cfg.as_ref().unwrap().0.clone()));
} else if !self.btn_status && self.prev_btn_status {
log::info!("VPN OFF");
tx.send(None);
} else {
return;
}
self.prev_btn_status = self.btn_status;
}
}

View File

@ -0,0 +1,46 @@
/// iOS-style toggle switch:
///
/// ``` text
/// _____________
/// / /.....\
/// | |.......|
/// \_______\_____/
/// ```
///
/// ## Example:
/// ``` ignore
/// toggle_ui(ui, &mut my_bool);
/// ```
use eframe::egui;
fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
if response.clicked() {
*on = !*on;
response.mark_changed();
}
response.widget_info(|| {
egui::WidgetInfo::selected(egui::WidgetType::Checkbox, ui.is_enabled(), *on, "")
});
if ui.is_rect_visible(rect) {
let how_on = ui.ctx().animate_bool_responsive(response.id, *on);
let visuals = ui.style().interact_selectable(&response, *on);
let rect = rect.expand(visuals.expansion);
let radius = 0.5 * rect.height();
ui.painter()
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
let center = egui::pos2(circle_x, rect.center().y);
ui.painter()
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
}
response
}
pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ {
move |ui: &mut egui::Ui| toggle_ui(ui, on)
}