refactor: separate modules - app, mapping

This commit is contained in:
2026-08-26 15:53:38 +05:00
parent 03d4be8444
commit ea09f47938
4 changed files with 326 additions and 295 deletions
+5 -4
View File
@@ -3,10 +3,11 @@
> Скоуп: src/main.rs (401 строка), src/config.rs (~210), src/winit.rs (45), > Скоуп: src/main.rs (401 строка), src/config.rs (~210), src/winit.rs (45),
> ui/*.slint. Плюс вывод clippy. > ui/*.slint. Плюс вывод clippy.
> >
> Итог рефакторинга: main.rs 401→340 строк (и стал тонким по смыслу), > Итог рефакторинга: main.rs 401→32 строки (чистый скелет), появился launcher.rs
> появился launcher.rs, 12 юнит-тестов, tools/check.ps1 (fmt+clippy -D warnings > и app.rs (`struct App`: new/wire×4 группы/run), 12 юнит-тестов,
> +test) зелёный, дублирование запуска устранено, индекс из UI вместо поиска > tools/check.ps1 (fmt+clippy -D warnings+test) зелёный, дублирование запуска
> по имени, все P0-P2 закрыты. UI проверен скриншотом — идентичен. > устранено, индекс из UI вместо поиска по имени, все P0-P2 закрыты.
> UI проверен скриншотом — идентичен. Cold start release 110-133 ms.
## P0 — мусор и артефакты правок (чинить сразу) ## P0 — мусор и артефакты правок (чинить сразу)
+273
View File
@@ -0,0 +1,273 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Instant;
use i_slint_backend_winit::WinitWindowAccessor;
use slint::{ComponentHandle, Model};
use crate::config::{self, BrowserEntry, Config};
use crate::launcher;
use crate::mapping::to_ui_config;
use crate::winit;
use crate::{BrowserConfig, MainWindow, RememberedEntry, SettingsWindow, Theme};
const DEFAULT_URL: &str = "ku6epxboctuk.github.io";
const ENV_THEME: &str = "BP_THEME";
const ENV_VIEW: &str = "BP_VIEW";
const VIEW_SETTINGS: &str = "settings";
pub struct App {
main_window: MainWindow,
settings_window: SettingsWindow,
browser_model: Rc<slint::VecModel<BrowserConfig>>,
remembered_model: Rc<slint::VecModel<RememberedEntry>>,
config: Rc<RefCell<Config>>,
}
impl App {
pub fn new(cfg: Config, url: Option<String>) -> Self {
let config = Rc::new(RefCell::new(cfg));
let browser_model = Rc::new(slint::VecModel::<BrowserConfig>::from(vec![]));
let remembered_model = Rc::new(slint::VecModel::<RememberedEntry>::from(vec![]));
let main_window = MainWindow::new().unwrap();
let settings_window = SettingsWindow::new().unwrap();
if std::env::var(ENV_THEME).as_deref() == Ok("light") {
main_window.global::<Theme>().set_is_dark(false);
}
Self::refresh_models(&browser_model, &remembered_model, &config.borrow());
settings_window.set_browsers(browser_model.clone().into());
settings_window.set_remembered(remembered_model.clone().into());
main_window.set_browser_model(browser_model.clone().into());
main_window.set_current_url(
url.clone()
.unwrap_or_else(|| DEFAULT_URL.to_string())
.into(),
);
if let Some(u) = &url {
main_window.set_current_domain(config::domain_of(u).into());
}
main_window.set_remember_choice(config.borrow().settings.remember_choice);
main_window.set_always_ask(config.borrow().settings.always_ask);
Self {
main_window,
settings_window,
browser_model,
remembered_model,
config,
}
}
pub fn wire(&self) {
self.wire_launch();
self.wire_config_editor();
self.wire_settings_window();
self.wire_window_chrome();
}
fn wire_launch(&self) {
self.main_window.on_launch_browser({
let main_window = self.main_window.clone_strong();
let browser_model = self.browser_model.clone();
let config = self.config.clone();
move |index: i32| {
let index = index.max(0) as usize;
let Some(ui_entry) = browser_model.row_data(index) else {
return;
};
main_window.set_selected_index(index as i32);
let entry = BrowserEntry::from(&ui_entry);
let url = main_window.get_current_url().to_string();
let remember = main_window.get_remember_choice();
if launcher::launch_and_remember(&config, &entry, &url, remember) {
let _ = main_window.window().hide();
}
}
});
self.main_window.on_open_clicked({
let main_window = self.main_window.clone_strong();
let browser_model = self.browser_model.clone();
let config = self.config.clone();
move || {
let index = main_window.get_selected_index().max(0) as usize;
let Some(ui_entry) = browser_model.row_data(index) else {
return;
};
let entry = BrowserEntry::from(&ui_entry);
let url = main_window.get_current_url().to_string();
let remember = main_window.get_remember_choice();
if launcher::launch_and_remember(&config, &entry, &url, remember) {
let _ = main_window.window().hide();
}
}
});
self.main_window.on_toggle_remember({
let config = self.config.clone();
move |checked| {
config.borrow_mut().settings.remember_choice = checked;
config::save(&config.borrow());
}
});
self.main_window.on_toggle_always_ask({
let config = self.config.clone();
move |checked| {
config.borrow_mut().settings.always_ask = checked;
config::save(&config.borrow());
}
});
}
fn wire_config_editor(&self) {
self.main_window.on_settings_clicked(move || {
let path = config::config_path();
if !path.exists() {
let mut c = config::load();
config::ensure_browsers(&mut c);
}
let editor = std::process::Command::new("subl")
.arg(&path)
.spawn()
.or_else(|_| std::process::Command::new("notepad").arg(&path).spawn());
if let Err(e) = editor {
config::log(&format!("cannot open editor: {e}"));
}
});
}
fn wire_settings_window(&self) {
self.settings_window.on_remove_browser({
let browser_model = self.browser_model.clone();
let remembered_model = self.remembered_model.clone();
let config = self.config.clone();
move |name| {
let name = name.to_string();
config.borrow_mut().browsers.retain(|b| b.name != name);
config::save(&config.borrow());
Self::refresh_models(&browser_model, &remembered_model, &config.borrow());
}
});
self.settings_window.on_remove_remembered({
let browser_model = self.browser_model.clone();
let remembered_model = self.remembered_model.clone();
let config = self.config.clone();
move |domain| {
let domain = domain.to_string();
config.borrow_mut().remembered.remove(&domain);
config::save(&config.borrow());
Self::refresh_models(&browser_model, &remembered_model, &config.borrow());
}
});
self.settings_window.on_redetect({
let browser_model = self.browser_model.clone();
let remembered_model = self.remembered_model.clone();
let config = self.config.clone();
move || {
let detected = config::detect_browsers();
let mut cfg_ref = config.borrow_mut();
for entry in detected {
if !cfg_ref
.browsers
.iter()
.any(|b| b.path.eq_ignore_ascii_case(&entry.path))
{
cfg_ref.browsers.push(entry);
}
}
config::save(&cfg_ref);
drop(cfg_ref);
Self::refresh_models(&browser_model, &remembered_model, &config.borrow());
}
});
self.settings_window.on_esc_pressed({
let settings_window = self.settings_window.clone_strong();
let main_window = self.main_window.clone_strong();
move || {
let _ = settings_window.window().hide();
main_window.invoke_restore_focus();
}
});
self.settings_window.window().on_close_requested({
let main_window = self.main_window.clone_strong();
move || {
main_window.invoke_restore_focus();
slint::CloseRequestResponse::HideWindow
}
});
}
fn wire_window_chrome(&self) {
self.main_window.on_request_drag({
let main_window = self.main_window.clone_strong();
move || {
main_window.window().with_winit_window(|winit_window| {
winit_window.drag_window().ok();
});
}
});
self.main_window.on_esc_pressed({
let main_window = self.main_window.clone_strong();
move || {
let _ = main_window.window().hide();
}
});
}
fn refresh_models(
browser_model: &slint::VecModel<BrowserConfig>,
remembered_model: &slint::VecModel<RememberedEntry>,
cfg: &Config,
) {
let rows: Vec<BrowserConfig> = cfg.browsers.iter().map(to_ui_config).collect();
browser_model.set_vec(rows);
let rows: Vec<RememberedEntry> = cfg
.remembered
.iter()
.map(|(d, b)| RememberedEntry {
domain: d.clone().into(),
browser: b.clone().into(),
})
.collect();
remembered_model.set_vec(rows);
}
pub fn run(self, start: Instant) {
let settings_only = std::env::var(ENV_VIEW).as_deref() == Ok(VIEW_SETTINGS);
if settings_only {
self.settings_window.show().unwrap();
let weak = self.settings_window.as_weak();
let _ = slint::invoke_from_event_loop(move || {
if let Some(sw) = weak.upgrade() {
winit::center_window(sw.window());
sw.invoke_restore_focus();
}
config::log(&format!("cold start -> event loop: {:?}", start.elapsed()));
});
self.settings_window.run().unwrap();
} else {
self.main_window.show().unwrap();
let weak = self.main_window.as_weak();
let _ = slint::invoke_from_event_loop(move || {
if let Some(mw) = weak.upgrade() {
winit::center_window(mw.window());
mw.invoke_restore_focus();
}
config::log(&format!("cold start -> event loop: {:?}", start.elapsed()));
});
self.main_window.run().unwrap();
}
}
}
+5 -291
View File
@@ -3,301 +3,14 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use std::cell::RefCell; mod app;
use std::rc::Rc;
use i_slint_backend_winit::WinitWindowAccessor;
use slint::Model;
mod config; mod config;
mod launcher; mod launcher;
mod mapping;
mod winit; mod winit;
use config::{BrowserEntry, Config};
slint::include_modules!(); slint::include_modules!();
const DEFAULT_URL: &str = "ku6epxboctuk.github.io";
const ENV_THEME: &str = "BP_THEME";
const ENV_VIEW: &str = "BP_VIEW";
const VIEW_SETTINGS: &str = "settings";
impl From<&BrowserConfig> for BrowserEntry {
fn from(b: &BrowserConfig) -> Self {
Self {
name: b.name.to_string(),
path: b.path.to_string(),
flags: b.flags.to_string(),
icon: String::new(),
emoji: b.emoji.to_string(),
}
}
}
fn load_icon(rel: &str) -> slint::Image {
let from_exe = std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|dir| dir.join(rel)));
let path = from_exe
.filter(|p| p.exists())
.unwrap_or_else(|| std::path::PathBuf::from(rel));
slint::Image::load_from_path(&path).unwrap_or_default()
}
fn to_ui_config(entry: &BrowserEntry) -> BrowserConfig {
let icon_rel = if entry.icon.is_empty() {
config::icon_for(&entry.name)
} else {
&entry.icon
};
BrowserConfig {
icon: load_icon(icon_rel),
emoji: entry.emoji.clone().into(),
icon_mask: entry.icon.is_empty()
&& entry.emoji.is_empty()
&& !config::is_known_browser(&entry.name),
name: entry.name.clone().into(),
path: entry.path.clone().into(),
flags: entry.flags.clone().into(),
is_default: false,
}
}
fn refresh_models(
browser_model: &slint::VecModel<BrowserConfig>,
remembered_model: &slint::VecModel<RememberedEntry>,
cfg: &Config,
) {
let rows: Vec<BrowserConfig> = cfg.browsers.iter().map(to_ui_config).collect();
browser_model.set_vec(rows);
let rows: Vec<RememberedEntry> = cfg
.remembered
.iter()
.map(|(d, b)| RememberedEntry {
domain: d.clone().into(),
browser: b.clone().into(),
})
.collect();
remembered_model.set_vec(rows);
}
fn launch_index(
main_window: &MainWindow,
browser_model: &slint::VecModel<BrowserConfig>,
cfg: &Rc<RefCell<Config>>,
index: usize,
) {
let Some(ui_entry) = browser_model.row_data(index) else {
return;
};
main_window.set_selected_index(index as i32);
let entry = BrowserEntry::from(&ui_entry);
let url = main_window.get_current_url().to_string();
let remember = main_window.get_remember_choice();
if launcher::launch_and_remember(cfg, &entry, &url, remember) {
let _ = main_window.window().hide();
}
}
fn open_config_in_editor() {
let path = config::config_path();
if !path.exists() {
let mut c = config::load();
config::ensure_browsers(&mut c);
}
let editor = std::process::Command::new("subl")
.arg(&path)
.spawn()
.or_else(|_| std::process::Command::new("notepad").arg(&path).spawn());
if let Err(e) = editor {
config::log(&format!("cannot open editor: {e}"));
}
}
fn init(cfg: Config, url: Option<String>) -> (MainWindow, SettingsWindow) {
let cfg = Rc::new(RefCell::new(cfg));
let browser_model = Rc::new(slint::VecModel::<BrowserConfig>::from(vec![]));
let remembered_model = Rc::new(slint::VecModel::<RememberedEntry>::from(vec![]));
let main_window = MainWindow::new().unwrap();
let settings_window = SettingsWindow::new().unwrap();
if std::env::var(ENV_THEME).as_deref() == Ok("light") {
main_window.global::<Theme>().set_is_dark(false);
}
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
settings_window.set_browsers(browser_model.clone().into());
settings_window.set_remembered(remembered_model.clone().into());
main_window.set_browser_model(browser_model.clone().into());
main_window.set_current_url(
url.clone()
.unwrap_or_else(|| DEFAULT_URL.to_string())
.into(),
);
if let Some(u) = &url {
main_window.set_current_domain(config::domain_of(u).into());
}
main_window.set_remember_choice(cfg.borrow().settings.remember_choice);
main_window.set_always_ask(cfg.borrow().settings.always_ask);
main_window.on_launch_browser({
let main_window = main_window.clone_strong();
let browser_model = browser_model.clone();
let cfg = cfg.clone();
move |index: i32| {
launch_index(&main_window, &browser_model, &cfg, index.max(0) as usize);
}
});
main_window.on_open_clicked({
let main_window = main_window.clone_strong();
let browser_model = browser_model.clone();
let cfg = cfg.clone();
move || {
let index = main_window.get_selected_index().max(0) as usize;
launch_index(&main_window, &browser_model, &cfg, index);
}
});
main_window.on_toggle_remember({
let cfg = cfg.clone();
move |checked| {
cfg.borrow_mut().settings.remember_choice = checked;
config::save(&cfg.borrow());
}
});
main_window.on_toggle_always_ask({
let cfg = cfg.clone();
move |checked| {
cfg.borrow_mut().settings.always_ask = checked;
config::save(&cfg.borrow());
}
});
main_window.on_settings_clicked(move || {
open_config_in_editor();
});
main_window.on_request_drag({
let main_window = main_window.clone_strong();
move || {
main_window.window().with_winit_window(|winit_window| {
winit_window.drag_window().ok();
});
}
});
main_window.on_esc_pressed({
let main_window = main_window.clone_strong();
move || {
let _ = main_window.window().hide();
}
});
settings_window.on_remove_browser({
let browser_model = browser_model.clone();
let remembered_model = remembered_model.clone();
let cfg = cfg.clone();
move |name| {
let name = name.to_string();
cfg.borrow_mut().browsers.retain(|b| b.name != name);
config::save(&cfg.borrow());
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
}
});
settings_window.on_remove_remembered({
let browser_model = browser_model.clone();
let remembered_model = remembered_model.clone();
let cfg = cfg.clone();
move |domain| {
let domain = domain.to_string();
cfg.borrow_mut().remembered.remove(&domain);
config::save(&cfg.borrow());
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
}
});
settings_window.on_redetect({
let browser_model = browser_model.clone();
let remembered_model = remembered_model.clone();
let cfg = cfg.clone();
move || {
let detected = config::detect_browsers();
let mut cfg_ref = cfg.borrow_mut();
for entry in detected {
if !cfg_ref
.browsers
.iter()
.any(|b| b.path.eq_ignore_ascii_case(&entry.path))
{
cfg_ref.browsers.push(entry);
}
}
config::save(&cfg_ref);
drop(cfg_ref);
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
}
});
settings_window.on_esc_pressed({
let settings_window = settings_window.clone_strong();
let main_window = main_window.clone_strong();
move || {
let _ = settings_window.window().hide();
main_window.invoke_restore_focus();
}
});
settings_window.window().on_close_requested({
let main_window = main_window.clone_strong();
move || {
main_window.invoke_restore_focus();
slint::CloseRequestResponse::HideWindow
}
});
(main_window, settings_window)
}
fn show_and_run(
main_window: MainWindow,
settings_window: SettingsWindow,
start: std::time::Instant,
) {
let settings_only = std::env::var(ENV_VIEW).as_deref() == Ok(VIEW_SETTINGS);
if settings_only {
settings_window.show().unwrap();
let weak = settings_window.as_weak();
let _ = slint::invoke_from_event_loop(move || {
if let Some(sw) = weak.upgrade() {
winit::center_window(sw.window());
sw.invoke_restore_focus();
}
config::log(&format!("cold start -> event loop: {:?}", start.elapsed()));
});
settings_window.run().unwrap();
} else {
main_window.show().unwrap();
let weak = main_window.as_weak();
let _ = slint::invoke_from_event_loop(move || {
if let Some(mw) = weak.upgrade() {
winit::center_window(mw.window());
mw.invoke_restore_focus();
}
config::log(&format!("cold start -> event loop: {:?}", start.elapsed()));
});
main_window.run().unwrap();
}
}
pub fn main() { pub fn main() {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
@@ -321,6 +34,7 @@ pub fn main() {
} }
} }
let (main_window, settings_window) = init(cfg, url); let app = app::App::new(cfg, url);
show_and_run(main_window, settings_window, start); app.wire();
app.run(start);
} }
+43
View File
@@ -0,0 +1,43 @@
use crate::BrowserConfig;
use crate::config::{self, BrowserEntry};
impl From<&BrowserConfig> for BrowserEntry {
fn from(b: &BrowserConfig) -> Self {
Self {
name: b.name.to_string(),
path: b.path.to_string(),
flags: b.flags.to_string(),
icon: String::new(),
emoji: b.emoji.to_string(),
}
}
}
pub fn load_icon(rel: &str) -> slint::Image {
let from_exe = std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|dir| dir.join(rel)));
let path = from_exe
.filter(|p| p.exists())
.unwrap_or_else(|| std::path::PathBuf::from(rel));
slint::Image::load_from_path(&path).unwrap_or_default()
}
pub fn to_ui_config(entry: &BrowserEntry) -> BrowserConfig {
let icon_rel = if entry.icon.is_empty() {
config::icon_for(&entry.name)
} else {
&entry.icon
};
BrowserConfig {
icon: load_icon(icon_rel),
emoji: entry.emoji.clone().into(),
icon_mask: entry.icon.is_empty()
&& entry.emoji.is_empty()
&& !config::is_known_browser(&entry.name),
name: entry.name.clone().into(),
path: entry.path.clone().into(),
flags: entry.flags.clone().into(),
is_default: false,
}
}