This commit is contained in:
hardliner66 2024-10-08 22:20:43 +02:00
parent 94bcff177e
commit 56f2901b8e
5 changed files with 437 additions and 2080 deletions

18
.vscode/launch.json vendored
View File

@ -4,6 +4,24 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug 'log_history'",
"cargo": {
"args": [
"build",
"--bin=workspace_log_history",
"--package=workspace"
],
"filter": {
"name": "workspace_log_history",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{ {
"type": "lldb", "type": "lldb",
"request": "launch", "request": "launch",

2064
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,13 +6,21 @@ edition = "2021"
[dependencies] [dependencies]
tray-icon = "0.19.0" tray-icon = "0.19.0"
image = "0.25" image = "0.25"
notan = { version = "0.12.1", features = ["audio", "clipboard", "drop_files", "egui", "extra", "links", "notan_extra", "save_file", "serde", "text"] }
anyhow = "1.0.89" anyhow = "1.0.89"
notan_extra = "0.12.1"
tokio = { version = "1.40.0", features = ["full"] } tokio = { version = "1.40.0", features = ["full"] }
indexmap = { version = "2.6.0", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }
rsn = "0.1.0" rsn = "0.1.0"
serde = { version = "1.0.210", features = ["derive"] }
known-folders = "1.2.0" known-folders = "1.2.0"
eframe = "0.29.1" indexmap = { version = "2.6.0", features = ["serde"] }
egui = "0.29.1" notan = { version = "0.12.1", features = [
"audio",
"clipboard",
"drop_files",
"egui",
"extra",
"links",
"notan_extra",
"text",
] }
tao = "0.30.3"

View File

@ -0,0 +1,151 @@
use std::str::FromStr;
use std::{path::PathBuf, process::Command};
use indexmap::IndexSet;
use known_folders::{get_known_folder_path, KnownFolder};
use notan::egui::Sense;
use notan::log::warn;
use notan::{
draw::DrawConfig,
egui::{self, EguiConfig, EguiPluginSugar, Label},
extra::FpsLimit,
prelude::*,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq)]
struct LogFile {
name: String,
path: PathBuf,
}
#[derive(AppState, Serialize, Deserialize)]
struct State {
log_history: IndexSet<LogFile>,
}
impl State {
fn save(&mut self) {
let folder = get_known_folder_path(KnownFolder::LocalAppData)
.unwrap()
.join("workspace")
.join("log_history");
std::fs::create_dir_all(&folder).unwrap();
std::fs::write(folder.join("data.rsn"), rsn::to_string(self)).unwrap();
}
}
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) {}
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("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("log_history")
.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 {
log_history: IndexSet::new(),
})
}

View File

@ -1,52 +1,52 @@
#![allow(dead_code)] #![allow(dead_code)]
#![allow(unused_imports)] #![allow(unused_imports)]
use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::{path::PathBuf, time::Duration};
use indexmap::IndexSet; use tao::{
use notan::{ event::{Event, WindowEvent},
draw::DrawConfig, event_loop::{ControlFlow, EventLoop},
egui::{self, *}, window::WindowBuilder,
extra::FpsLimit,
log::warn,
prelude::*,
Event,
}; };
use serde::{Deserialize, Serialize}; use tokio::process::{Child, Command};
use tokio::process::Command;
use tray_icon::{ use tray_icon::{
menu::{Menu, MenuEvent, MenuEventReceiver, MenuItem, PredefinedMenuItem}, menu::{Menu, MenuEvent, MenuEventReceiver, MenuItem, PredefinedMenuItem},
MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent, TrayIconEventReceiver, MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent, TrayIconEventReceiver,
}; };
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq)] fn main() {
struct LogFile { let event_loop = EventLoop::new();
name: String,
path: PathBuf,
}
struct TrayData { let mut window = Some(
tray_icon: Option<TrayIcon>, WindowBuilder::new()
quit_i: MenuItem, .with_title("A fantastic window!")
menu_channel: MenuEventReceiver, .with_visible(false)
tray_channel: TrayIconEventReceiver, .build(&event_loop)
} .unwrap(),
);
impl Default for TrayData { let log_history_i = MenuItem::new("Logs", true, None);
fn default() -> Self { let quit_i = MenuItem::new("Quit", true, None);
let mut tray_icon = None;
let mut log_history: Option<Child> = None;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::WaitUntil(
std::time::Instant::now() + std::time::Duration::from_millis(16),
);
if let tao::event::Event::NewEvents(tao::event::StartCause::Init) = event {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/icon.png"); let path = concat!(env!("CARGO_MANIFEST_DIR"), "/icon.png");
let tray_menu = Menu::new(); let tray_menu = Menu::new();
let test = MenuItem::new("Test", true, None);
let quit_i = MenuItem::new("Quit", true, None);
tray_menu tray_menu
.append_items(&[&test, &PredefinedMenuItem::separator(), &quit_i]) .append_items(&[&log_history_i, &PredefinedMenuItem::separator(), &quit_i])
.unwrap(); .unwrap();
let icon = load_icon(std::path::Path::new(path)); let icon = load_icon(std::path::Path::new(path));
let tray_icon = Some( tray_icon = Some(
TrayIconBuilder::new() TrayIconBuilder::new()
.with_menu_on_left_click(false) .with_menu_on_left_click(false)
.with_menu(Box::new(tray_menu.clone())) .with_menu(Box::new(tray_menu.clone()))
@ -56,157 +56,59 @@ impl Default for TrayData {
.unwrap(), .unwrap(),
); );
let menu_channel = MenuEvent::receiver().clone(); // We have to request a redraw here to have the icon actually show up.
let tray_channel = TrayIconEvent::receiver().clone(); // Tao only exposes a redraw method on the Window so we use core-foundation directly.
TrayData { #[cfg(target_os = "macos")]
menu_channel, unsafe {
tray_icon, use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp};
quit_i,
tray_channel, let rl = CFRunLoopGetMain();
} CFRunLoopWakeUp(rl);
} }
} }
#[derive(AppState, Serialize, Deserialize)] match event {
struct State { Event::WindowEvent {
#[serde(skip)] event: WindowEvent::CloseRequested,
tray_data: TrayData, window_id: _,
log_history: IndexSet<LogFile>, ..
} => {
// drop the window to fire the `Destroyed` event
window = None;
} }
Event::WindowEvent {
impl State { event: WindowEvent::Destroyed,
fn save(&mut self) { window_id: _,
let folder = known_folders::get_known_folder_path(known_folders::KnownFolder::LocalAppData) ..
.unwrap() } => {
.join("workspace"); *control_flow = ControlFlow::Exit;
std::fs::create_dir_all(&folder).unwrap(); _ = tray_icon.take();
std::fs::write(folder.join("data.rsn"), rsn::to_string(self)).unwrap(); }
Event::MainEventsCleared => {
if let Some(w) = &window {
w.request_redraw();
} }
} }
_ => (),
#[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 let Ok(event) = MenuEvent::receiver().try_recv() {
if event.id == state.tray_data.quit_i.id() { if event.id == quit_i.id() {
state.tray_data.tray_icon.take(); window = None;
app.exit(); }
if event.id == log_history_i.id() {
match &mut log_history {
Some(log_history) => {
if let Ok(Some(_)) = log_history.try_wait() {
*log_history = Command::new("workspace_log_history").spawn().unwrap();
} }
} }
None => {
log_history = Some(Command::new("workspace_log_history").spawn().unwrap());
} }
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 { fn load_icon(path: &std::path::Path) -> tray_icon::Icon {