Initial import

This commit is contained in:
2026-03-30 22:51:56 -04:00
commit 08e2910b9d
103 changed files with 35475 additions and 0 deletions
+247
View File
@@ -0,0 +1,247 @@
use crate::ui::pages::PageBuildContext;
use crate::ui::toast::show_toast;
use crate::ui::widgets::tool_page;
use crate::window::{recompute_validation, refresh_save_button};
use gtk4::prelude::*;
use libadwaita::prelude::*;
use mangotune::config::parser::Parser;
pub fn build_page(ctx: &PageBuildContext) -> gtk4::ScrolledWindow {
let (page, body) = tool_page::build_tool_page(
"Raw Editor",
"Advanced",
"Edit the full MangoHud config as plain text when you need absolute control, while still keeping MangoTune validation and save protection in the loop.",
&["full config", "advanced", "validation-aware"],
);
tool_page::append_callout(
&body,
"Power tool",
"Raw edits bypass the guided controls",
"Use this when you know exactly what you want to change or when MangoHud adds an option before MangoTune has a polished control for it. Validation still runs before save.",
Some("tool-callout-warning"),
);
let session_group = tool_page::append_custom_section(
&body,
"Editor session",
"Open the dedicated raw text window, apply edits back into the in-memory config, then save normally once validation is clean.",
Some("Manual mode"),
);
let source_row = libadwaita::ActionRow::builder()
.title("Current config source")
.subtitle(current_config_path(ctx))
.build();
source_row.add_css_class("control-row");
session_group.add(&source_row);
let stats_row = libadwaita::ActionRow::builder()
.title("Current config snapshot")
.subtitle(current_stats_label(ctx))
.build();
stats_row.add_css_class("control-row");
session_group.add(&stats_row);
let launch_row = libadwaita::ActionRow::builder()
.title("Open raw text editor")
.subtitle("Launch a dedicated editor window with apply and reload actions")
.build();
launch_row.add_css_class("control-row");
let button_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
let open_button = gtk4::Button::with_label("Open Editor");
open_button.add_css_class("suggested-action");
let reload_button = gtk4::Button::with_label("Refresh Snapshot");
button_box.append(&reload_button);
button_box.append(&open_button);
let ctx_reload = ctx.clone();
let source_row_reload = source_row.clone();
let stats_row_reload = stats_row.clone();
reload_button.connect_clicked(move |_| {
source_row_reload.set_subtitle(&current_config_path(&ctx_reload));
stats_row_reload.set_subtitle(&current_stats_label(&ctx_reload));
});
let ctx_open = ctx.clone();
let source_row_open = source_row.clone();
let stats_row_open = stats_row.clone();
open_button.connect_clicked(move |_| {
source_row_open.set_subtitle(&current_config_path(&ctx_open));
stats_row_open.set_subtitle(&current_stats_label(&ctx_open));
open_editor_window(&ctx_open);
});
launch_row.add_suffix(&button_box);
session_group.add(&launch_row);
let workflow_group = tool_page::append_custom_section(
&body,
"When raw editing helps",
"Keep this lightweight: the guided controls should still be your default for most changes.",
Some("Quick guidance"),
);
let workflow_note = gtk4::Label::new(Some(
"Use the raw editor when you want to paste a known-good MangoHud snippet, touch an uncommon option before MangoTune has a dedicated control for it, or make several text edits at once without jumping across pages.",
));
workflow_note.set_wrap(true);
workflow_note.set_xalign(0.0);
workflow_note.add_css_class("dim-label");
workflow_group.add(&workflow_note);
page
}
fn open_editor_window(ctx: &PageBuildContext) {
let window = gtk4::Window::builder()
.title("MangoTune Raw Editor")
.default_width(940)
.default_height(680)
.transient_for(&ctx.parent_window)
.modal(false)
.build();
window.add_css_class("preferences-shell");
let (outer, body, footer_row) = tool_page::build_utility_window_shell(
"Manual editing",
"Raw Config Editor",
"Apply updates back into MangoTune when you want the dashboard and page controls to reflect them. Reload discards unsaved raw edits and restores the current in-memory config.",
);
let buffer = gtk4::TextBuffer::new(None::<&gtk4::TextTagTable>);
if let Ok(state) = ctx.state.lock() {
buffer.set_text(&Parser::to_string(&state.config));
}
let text_view = gtk4::TextView::builder()
.monospace(true)
.vexpand(true)
.hexpand(true)
.buffer(&buffer)
.top_margin(12)
.bottom_margin(12)
.left_margin(12)
.right_margin(12)
.build();
let scrolled = gtk4::ScrolledWindow::builder()
.child(&text_view)
.vexpand(true)
.hexpand(true)
.build();
scrolled.add_css_class("utility-window-scroller");
let footer = gtk4::Label::new(Some("Line count: 0 | Option count: 0"));
footer.add_css_class("dim-label");
footer.set_xalign(0.0);
let spacer = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
spacer.set_hexpand(true);
let reload = gtk4::Button::with_label("Reload");
let apply = gtk4::Button::with_label("Apply to Workspace");
apply.add_css_class("suggested-action");
footer_row.append(&spacer);
footer_row.append(&reload);
footer_row.append(&apply);
let ctx_apply = ctx.clone();
let buffer_apply = buffer.clone();
let footer_apply = footer.clone();
apply.connect_clicked(move |_| {
let start = buffer_apply.start_iter();
let end = buffer_apply.end_iter();
let text = buffer_apply.text(&start, &end, false).to_string();
if let Ok(mut state) = ctx_apply.state.lock() {
let parsed = Parser::parse_str(&text, state.config.path.clone());
state.config = parsed;
state.config.dirty = true;
state.dirty = true;
}
update_footer(&footer_apply, &text);
recompute_validation(&ctx_apply.state);
refresh_save_button(&ctx_apply.state, &ctx_apply.save_button);
show_toast(
&ctx_apply.toast_overlay,
"Applied raw text changes to the workspace",
);
});
let ctx_reload = ctx.clone();
let buffer_reload = buffer.clone();
let footer_reload = footer.clone();
reload.connect_clicked(move |_| {
if let Ok(state) = ctx_reload.state.lock() {
let text = Parser::to_string(&state.config);
buffer_reload.set_text(&text);
update_footer(&footer_reload, &text);
}
});
let initial = buffer.text(&buffer.start_iter(), &buffer.end_iter(), false);
update_footer(&footer, &initial);
body.append(&scrolled);
body.append(&footer);
window.set_child(Some(&outer));
window.present();
}
fn current_config_path(ctx: &PageBuildContext) -> String {
ctx.state
.lock()
.ok()
.and_then(|state| {
state
.config
.path
.as_ref()
.map(|path| path.display().to_string())
})
.unwrap_or_else(|| "Unsaved session".to_string())
}
fn current_stats_label(ctx: &PageBuildContext) -> String {
ctx.state
.lock()
.ok()
.map(|state| {
let text = Parser::to_string(&state.config);
let (line_count, option_count) = count_text_stats(&text);
format!("{line_count} lines | {option_count} active option rows")
})
.unwrap_or_else(|| "Unavailable".to_string())
}
fn update_footer(footer: &gtk4::Label, text: &str) {
let (line_count, option_count) = count_text_stats(text);
footer.set_text(&format!(
"Line count: {line_count} | Option count: {option_count}"
));
}
fn count_text_stats(text: &str) -> (usize, usize) {
let line_count = text.lines().count();
let option_count = text
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with('#')
})
.count();
(line_count, option_count)
}
#[cfg(test)]
mod tests {
use super::count_text_stats;
#[test]
fn counts_only_active_option_lines() {
let text = "# comment\nfps\n\nposition=top-left\n";
assert_eq!(count_text_stats(text), (4, 2));
}
}