#![allow(dead_code)] #![allow(unused_imports)] use std::path::PathBuf; use std::str::FromStr; use indexmap::IndexSet; use notan::{ draw::DrawConfig, egui::{self, *}, extra::FpsLimit, log::warn, prelude::*, Event, }; use serde::{Deserialize, Serialize}; use tokio::process::Command; use tray_icon::{ menu::{Menu, MenuEvent, MenuEventReceiver, MenuItem, PredefinedMenuItem}, MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent, TrayIconEventReceiver, }; #[derive(Serialize, Deserialize, Hash, PartialEq, Eq)] struct LogFile { name: String, path: PathBuf, } struct TrayData { tray_icon: Option, quit_i: MenuItem, menu_channel: MenuEventReceiver, tray_channel: TrayIconEventReceiver, } impl Default for TrayData { fn default() -> Self { let path = concat!(env!("CARGO_MANIFEST_DIR"), "/icon.png"); let tray_menu = Menu::new(); let test = MenuItem::new("Test", true, None); let quit_i = MenuItem::new("Quit", true, None); tray_menu .append_items(&[&test, &PredefinedMenuItem::separator(), &quit_i]) .unwrap(); let icon = load_icon(std::path::Path::new(path)); let tray_icon = Some( TrayIconBuilder::new() .with_menu_on_left_click(false) .with_menu(Box::new(tray_menu.clone())) .with_tooltip("Workspace") .with_icon(icon) .build() .unwrap(), ); let menu_channel = MenuEvent::receiver().clone(); let tray_channel = TrayIconEvent::receiver().clone(); TrayData { menu_channel, tray_icon, quit_i, tray_channel, } } } #[derive(AppState, Serialize, Deserialize)] struct State { #[serde(skip)] tray_data: TrayData, log_history: IndexSet, } impl State { fn save(&mut self) { let folder = known_folders::get_known_folder_path(known_folders::KnownFolder::LocalAppData) .unwrap() .join("workspace"); std::fs::create_dir_all(&folder).unwrap(); std::fs::write(folder.join("data.rsn"), rsn::to_string(self)).unwrap(); } } #[tokio::main] async fn main() -> Result<(), String> { let win = WindowConfig::new() .set_vsync(false) .set_lazy_loop(true) .set_transparent(true) .set_high_dpi(true); notan::init_with(init) .add_config(win) .add_config(EguiConfig) .add_config(DrawConfig) .add_plugin(FpsLimit::new(60)) .update(update) .event(event) .draw(draw) .build() } fn update(app: &mut App, state: &mut State) { if let Ok(event) = MenuEvent::receiver().try_recv() { if event.id == state.tray_data.quit_i.id() { state.tray_data.tray_icon.take(); app.exit(); } } } fn event(_app: &mut App, _assets: &mut Assets, state: &mut State, evt: Event) { match evt { Event::Exit => {} Event::Drop(file) => { if file.name.ends_with(".log") { state.log_history.shift_insert( 0, LogFile { name: file.name.clone(), path: file .path .unwrap_or_else(|| PathBuf::from_str(&file.name).unwrap()), }, ); state.save(); } } _ => {} } } fn draw(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut State) { let mut output = plugins.egui(|ctx| { egui::SidePanel::left("side_panel").show(ctx, |ui| { ui.collapsing("Logs", |ui| { egui::ScrollArea::vertical().show(ui, |ui| { let mut to_swap = None; let mut to_delete = None; let history_len = state.log_history.len(); for (i, log) in state.log_history.iter().enumerate() { let path_text = log.path.to_string_lossy().to_string(); let label = ui .add(Label::new(&log.name).sense(Sense::click())) .on_hover_text(&path_text); label.context_menu(|ui| { if ui.button("Open in Notepad").clicked() { _ = Command::new("notepad.exe").arg(&path_text).spawn(); ui.close_menu(); } if ui.button("Remove").clicked() { to_delete = Some(i); ui.close_menu(); } }); if label.clicked() { _ = Command::new("C:\\tools\\fast-log-viewer\\fast_log_viewer.exe") .arg(path_text) .spawn(); to_swap = Some(i); } if i < history_len - 1 { ui.separator(); } } if let Some(i) = to_swap { if let Some(value) = state.log_history.swap_remove_index(i) { state.log_history.shift_insert(0, value); } state.save(); } if let Some(i) = to_delete { _ = state.log_history.shift_remove_index(i); state.save(); } }) }) }); }); output.clear_color(Color::BLACK); gfx.render(&output); } fn init(_gfx: &mut Graphics) -> State { let data = known_folders::get_known_folder_path(known_folders::KnownFolder::LocalAppData) .unwrap() .join("workspace") .join("data.rsn"); let mut state = None; if data.exists() { match std::fs::read_to_string(data) { Ok(data) => { state = Some(rsn::from_str(&data).unwrap()); } Err(err) => { warn!("Couldn't read data file: {}", err); } } } state.unwrap_or_else(|| State { tray_data: TrayData::default(), log_history: IndexSet::new(), }) } fn load_icon(path: &std::path::Path) -> tray_icon::Icon { let (icon_rgba, icon_width, icon_height) = { let image = image::open(path) .expect("Failed to open icon path") .into_rgba8(); let (width, height) = image.dimensions(); let rgba = image.into_raw(); (rgba, width, height) }; tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") }