rust: system CJK font fallback, sidebar footer layout, --run-local test hook
This commit is contained in:
@@ -18,6 +18,8 @@ use std::sync::{Arc, Mutex, mpsc};
|
|||||||
pub struct Options {
|
pub struct Options {
|
||||||
pub screenshot: Option<PathBuf>,
|
pub screenshot: Option<PathBuf>,
|
||||||
pub theme: Option<String>,
|
pub theme: Option<String>,
|
||||||
|
/// Open a local terminal at startup and type this command once connected (testing aid).
|
||||||
|
pub run_local: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors from worker threads, drained into toasts each frame.
|
/// Errors from worker threads, drained into toasts each frame.
|
||||||
@@ -77,6 +79,7 @@ pub struct App {
|
|||||||
pub update: Option<nexus_core::updater::UpdateInfo>,
|
pub update: Option<nexus_core::updater::UpdateInfo>,
|
||||||
screenshot: Option<(PathBuf, u32)>,
|
screenshot: Option<(PathBuf, u32)>,
|
||||||
shot_settle: u32,
|
shot_settle: u32,
|
||||||
|
run_local: Option<(String, Option<String>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn sc(m: Modifiers, k: Key) -> KeyboardShortcut {
|
const fn sc(m: Modifiers, k: Key) -> KeyboardShortcut {
|
||||||
@@ -121,7 +124,13 @@ impl App {
|
|||||||
update: None,
|
update: None,
|
||||||
screenshot: opts.screenshot.map(|p| (p, 0)),
|
screenshot: opts.screenshot.map(|p| (p, 0)),
|
||||||
shot_settle: 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 {
|
if let Some(from) = &svc.config.migrated_from {
|
||||||
app.s.success(format!("Imported your Alchementary Link data from {}", from.display()));
|
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;
|
let local = p.info.protocol == Protocol::Local;
|
||||||
match state {
|
match state {
|
||||||
SessionState::Connected => {
|
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.local_exited = false;
|
||||||
p.connected_since = Some(chrono::Local::now());
|
p.connected_since = Some(chrono::Local::now());
|
||||||
// The PTY was created before the view knew its size: send it now.
|
// The PTY was created before the view knew its size: send it now.
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ use nexus_core::service::Service;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn main() -> eframe::Result {
|
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 size = [1360.0, 860.0];
|
||||||
let mut data_dir = None;
|
let mut data_dir = None;
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
while let Some(a) = args.next() {
|
while let Some(a) = args.next() {
|
||||||
match a.as_str() {
|
match a.as_str() {
|
||||||
"--theme" => opts.theme = args.next(),
|
"--theme" => opts.theme = args.next(),
|
||||||
|
"--run-local" => opts.run_local = args.next(),
|
||||||
"--screenshot" => opts.screenshot = args.next().map(Into::into),
|
"--screenshot" => opts.screenshot = args.next().map(Into::into),
|
||||||
// Use another data directory (testing, screenshots, portable installs).
|
// Use another data directory (testing, screenshots, portable installs).
|
||||||
"--data-dir" => data_dir = args.next(),
|
"--data-dir" => data_dir = args.next(),
|
||||||
|
|||||||
+27
-29
@@ -31,36 +31,34 @@ pub fn ui(app: &mut App, ui: &mut Ui) {
|
|||||||
});
|
});
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
|
|
||||||
let footer_h = 36.0 + if app.sidebar.selected.is_empty() { 0.0 } else { 62.0 };
|
// Footer actions, pinned to the bottom of the sidebar.
|
||||||
let list_h = (ui.available_height() - footer_h).max(60.0);
|
egui::Panel::bottom("sidebar-footer").exact_size(36.0).frame(egui::Frame::NONE.fill(t.dock)).show_inside(ui, |ui| {
|
||||||
egui::ScrollArea::vertical().max_height(list_h).auto_shrink([false, false]).show(ui, |ui| list(app, ui));
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
if !app.sidebar.selected.is_empty() {
|
if widgets::icon_button(ui, icon::PLUS, "New connection (Ctrl/Cmd+N)").clicked() {
|
||||||
bulk_bar(app, ui);
|
app.s.request(Request::NewProfile { protocol: None, folder: String::new() });
|
||||||
}
|
}
|
||||||
// Footer actions.
|
if widgets::icon_button(ui, icon::COMMAND, "Command palette (Ctrl/Cmd+K)").clicked() {
|
||||||
ui.separator();
|
app.palette = Some(Default::default());
|
||||||
ui.horizontal(|ui| {
|
}
|
||||||
ui.add_space(6.0);
|
if widgets::icon_button(ui, icon::DOWNLOAD_SIMPLE, "Import profiles").clicked() {
|
||||||
if widgets::icon_button(ui, icon::PLUS, "New connection (Ctrl/Cmd+N)").clicked() {
|
app.s.request(Request::ImportEncrypted);
|
||||||
app.s.request(Request::NewProfile { protocol: None, folder: String::new() });
|
}
|
||||||
}
|
if widgets::icon_button(ui, icon::UPLOAD_SIMPLE, "Export profiles").clicked() {
|
||||||
if widgets::icon_button(ui, icon::COMMAND, "Command palette (Ctrl/Cmd+K)").clicked() {
|
app.s.request(Request::ExportEncrypted(vec![]));
|
||||||
app.palette = Some(Default::default());
|
}
|
||||||
}
|
if widgets::icon_button(ui, icon::FILM_SLATE, "Recordings").clicked() {
|
||||||
if widgets::icon_button(ui, icon::DOWNLOAD_SIMPLE, "Import profiles").clicked() {
|
app.recordings.open();
|
||||||
app.s.request(Request::ImportEncrypted);
|
}
|
||||||
}
|
if widgets::icon_button(ui, icon::GEAR, "Settings (Ctrl/Cmd+,)").clicked() {
|
||||||
if widgets::icon_button(ui, icon::UPLOAD_SIMPLE, "Export profiles").clicked() {
|
app.settings.open(crate::state::SettingsTab::General);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
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) {
|
fn list(app: &mut App, ui: &mut Ui) {
|
||||||
|
|||||||
@@ -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.
|
// 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::Proportional).or_default().insert(0, "Inter".into());
|
||||||
fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "JetBrainsMono".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()];
|
let mut semibold = vec!["Inter-SemiBold".to_owned()];
|
||||||
semibold.extend(fonts.families[&FontFamily::Proportional].iter().cloned());
|
semibold.extend(fonts.families[&FontFamily::Proportional].iter().cloned());
|
||||||
fonts.families.insert(FontFamily::Name("semibold".into()), semibold);
|
fonts.families.insert(FontFamily::Name("semibold".into()), semibold);
|
||||||
ctx.set_fonts(fonts);
|
ctx.set_fonts(fonts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The first wide-coverage (CJK) system font found on this machine.
|
||||||
|
fn system_fallback_font() -> Option<Vec<u8>> {
|
||||||
|
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 {
|
pub fn semibold(size: f32) -> FontId {
|
||||||
FontId::new(size, FontFamily::Name("semibold".into()))
|
FontId::new(size, FontFamily::Name("semibold".into()))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user