From ea09aeafe85b5b23243a931de7df603141071be3 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Sun, 11 Oct 2026 16:51:53 +0100 Subject: [PATCH] rust: system CJK font fallback, sidebar footer layout, --run-local test hook --- rust/app/src/app.rs | 16 ++++++++++++ rust/app/src/main.rs | 3 ++- rust/app/src/sidebar.rs | 56 ++++++++++++++++++++--------------------- rust/app/src/theme.rs | 26 +++++++++++++++++++ 4 files changed, 71 insertions(+), 30 deletions(-) diff --git a/rust/app/src/app.rs b/rust/app/src/app.rs index 02ba590..b284da7 100644 --- a/rust/app/src/app.rs +++ b/rust/app/src/app.rs @@ -18,6 +18,8 @@ use std::sync::{Arc, Mutex, mpsc}; pub struct Options { pub screenshot: Option, pub theme: Option, + /// Open a local terminal at startup and type this command once connected (testing aid). + pub run_local: Option, } /// Errors from worker threads, drained into toasts each frame. @@ -77,6 +79,7 @@ pub struct App { pub update: Option, screenshot: Option<(PathBuf, u32)>, shot_settle: u32, + run_local: Option<(String, Option)>, } const fn sc(m: Modifiers, k: Key) -> KeyboardShortcut { @@ -121,7 +124,13 @@ impl App { update: None, screenshot: opts.screenshot.map(|p| (p, 0)), shot_settle: 0, + run_local: opts.run_local.map(|c| (c, None)), }; + if let Some((_, sid)) = &mut app.run_local { + let id = app.s.svc.connect_local(); + *sid = Some(id.clone()); + app.add_session(&id, true); + } if let Some(from) = &svc.config.migrated_from { app.s.success(format!("Imported your Alchementary Link data from {}", from.display())); } @@ -426,6 +435,13 @@ impl App { let local = p.info.protocol == Protocol::Local; match state { SessionState::Connected => { + if let Some((cmd, Some(sid))) = &self.run_local + && sid == session + { + let _ = self.s.svc.sessions.write(session, format!("{cmd}\n").as_bytes()); + self.run_local = None; + } + let Some(p) = self.s.pane_mut(session) else { return }; p.local_exited = false; p.connected_since = Some(chrono::Local::now()); // The PTY was created before the view knew its size: send it now. diff --git a/rust/app/src/main.rs b/rust/app/src/main.rs index 76186c3..07c5e6f 100644 --- a/rust/app/src/main.rs +++ b/rust/app/src/main.rs @@ -33,13 +33,14 @@ use nexus_core::service::Service; use std::sync::Arc; fn main() -> eframe::Result { - let mut opts = app::Options { screenshot: None, theme: None }; + let mut opts = app::Options { screenshot: None, theme: None, run_local: None }; let mut size = [1360.0, 860.0]; let mut data_dir = None; let mut args = std::env::args().skip(1); while let Some(a) = args.next() { match a.as_str() { "--theme" => opts.theme = args.next(), + "--run-local" => opts.run_local = args.next(), "--screenshot" => opts.screenshot = args.next().map(Into::into), // Use another data directory (testing, screenshots, portable installs). "--data-dir" => data_dir = args.next(), diff --git a/rust/app/src/sidebar.rs b/rust/app/src/sidebar.rs index c6923cf..35ad290 100644 --- a/rust/app/src/sidebar.rs +++ b/rust/app/src/sidebar.rs @@ -31,36 +31,34 @@ pub fn ui(app: &mut App, ui: &mut Ui) { }); ui.add_space(4.0); - let footer_h = 36.0 + if app.sidebar.selected.is_empty() { 0.0 } else { 62.0 }; - let list_h = (ui.available_height() - footer_h).max(60.0); - egui::ScrollArea::vertical().max_height(list_h).auto_shrink([false, false]).show(ui, |ui| list(app, ui)); - - if !app.sidebar.selected.is_empty() { - bulk_bar(app, ui); - } - // Footer actions. - ui.separator(); - ui.horizontal(|ui| { - ui.add_space(6.0); - if widgets::icon_button(ui, icon::PLUS, "New connection (Ctrl/Cmd+N)").clicked() { - app.s.request(Request::NewProfile { protocol: None, folder: String::new() }); - } - if widgets::icon_button(ui, icon::COMMAND, "Command palette (Ctrl/Cmd+K)").clicked() { - app.palette = Some(Default::default()); - } - if widgets::icon_button(ui, icon::DOWNLOAD_SIMPLE, "Import profiles").clicked() { - app.s.request(Request::ImportEncrypted); - } - if widgets::icon_button(ui, icon::UPLOAD_SIMPLE, "Export profiles").clicked() { - app.s.request(Request::ExportEncrypted(vec![])); - } - if widgets::icon_button(ui, icon::FILM_STRIP, "Recordings").clicked() { - app.recordings.open(); - } - if widgets::icon_button(ui, icon::GEAR, "Settings (Ctrl/Cmd+,)").clicked() { - app.settings.open(crate::state::SettingsTab::General); - } + // Footer actions, pinned to the bottom of the sidebar. + egui::Panel::bottom("sidebar-footer").exact_size(36.0).frame(egui::Frame::NONE.fill(t.dock)).show_inside(ui, |ui| { + ui.horizontal_centered(|ui| { + ui.add_space(6.0); + if widgets::icon_button(ui, icon::PLUS, "New connection (Ctrl/Cmd+N)").clicked() { + app.s.request(Request::NewProfile { protocol: None, folder: String::new() }); + } + if widgets::icon_button(ui, icon::COMMAND, "Command palette (Ctrl/Cmd+K)").clicked() { + app.palette = Some(Default::default()); + } + if widgets::icon_button(ui, icon::DOWNLOAD_SIMPLE, "Import profiles").clicked() { + app.s.request(Request::ImportEncrypted); + } + if widgets::icon_button(ui, icon::UPLOAD_SIMPLE, "Export profiles").clicked() { + app.s.request(Request::ExportEncrypted(vec![])); + } + if widgets::icon_button(ui, icon::FILM_SLATE, "Recordings").clicked() { + app.recordings.open(); + } + if widgets::icon_button(ui, icon::GEAR, "Settings (Ctrl/Cmd+,)").clicked() { + app.settings.open(crate::state::SettingsTab::General); + } + }); }); + if !app.sidebar.selected.is_empty() { + egui::Panel::bottom("sidebar-bulk").frame(egui::Frame::NONE.fill(t.dock).inner_margin(6)).show_inside(ui, |ui| bulk_bar(app, ui)); + } + egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| list(app, ui)); } fn list(app: &mut App, ui: &mut Ui) { diff --git a/rust/app/src/theme.rs b/rust/app/src/theme.rs index 28a5b46..48a9e5e 100644 --- a/rust/app/src/theme.rs +++ b/rust/app/src/theme.rs @@ -196,12 +196,38 @@ pub fn install_fonts(ctx: &egui::Context) { // The bundled Inter has its private-use-area cmap stripped, so phosphor's icons win there. fonts.families.entry(FontFamily::Proportional).or_default().insert(0, "Inter".into()); fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "JetBrainsMono".into()); + // CJK and other scripts the bundled fonts lack: borrow a system font if one is installed. + if let Some(bytes) = system_fallback_font() { + fonts.font_data.insert("system-fallback".into(), Arc::new(FontData::from_owned(bytes))); + for fam in [FontFamily::Proportional, FontFamily::Monospace] { + fonts.families.entry(fam).or_default().push("system-fallback".into()); + } + } let mut semibold = vec!["Inter-SemiBold".to_owned()]; semibold.extend(fonts.families[&FontFamily::Proportional].iter().cloned()); fonts.families.insert(FontFamily::Name("semibold".into()), semibold); ctx.set_fonts(fonts); } +/// The first wide-coverage (CJK) system font found on this machine. +fn system_fallback_font() -> Option> { + let candidates: &[&str] = if cfg!(target_os = "macos") { + &["/System/Library/Fonts/PingFang.ttc", "/System/Library/Fonts/Hiragino Sans GB.ttc", "/Library/Fonts/Arial Unicode.ttf", "/System/Library/Fonts/Supplemental/Arial Unicode.ttf"] + } else if cfg!(windows) { + &["C:\\Windows\\Fonts\\msyh.ttc", "C:\\Windows\\Fonts\\YuGothM.ttc", "C:\\Windows\\Fonts\\msgothic.ttc", "C:\\Windows\\Fonts\\malgun.ttf"] + } else { + &[ + "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", + "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc", + "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc", + "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", + "/usr/share/fonts/wenquanyi/wqy-microhei/wqy-microhei.ttc", + "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", + ] + }; + candidates.iter().find_map(|p| std::fs::read(p).ok()) +} + pub fn semibold(size: f32) -> FontId { FontId::new(size, FontFamily::Name("semibold".into())) }