From 8bc14d74065e05f7a95cfdd66c8d14cca0a345b3 Mon Sep 17 00:00:00 2001
From: 233 <85852251+wfion@users.noreply.github.com>
Date: Wed, 5 Aug 2026 10:54:19 +0800
Subject: [PATCH] feat(about): add support information
---
src-tauri/Cargo.toml | 2 +
src-tauri/src/cmd/app.rs | 124 ++++++++++++++++++++++
src-tauri/src/lib.rs | 1 +
src/components/dialog/app/AboutDialog.tsx | 67 +++++++++++-
src/i18n/locales/en.json | 12 +++
src/i18n/locales/ko.json | 12 +++
src/i18n/locales/zh-CN.json | 12 +++
src/i18n/locales/zh-TW.json | 12 +++
src/types/global.d.ts | 6 ++
9 files changed, 247 insertions(+), 1 deletion(-)
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 8275f188..2295d1d0 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -193,9 +193,11 @@ windows-sys = { version = "0.61", features = [
"Win32_System_Threading",
] }
windows = { version = "0.61", features = [
+ "Wdk_System_SystemServices",
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_Threading",
+ "Win32_System_SystemInformation",
"Win32_System_DataExchange",
"Win32_System_Memory",
"Win32_UI_Shell",
diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs
index 6893d4b8..7a6a24bb 100644
--- a/src-tauri/src/cmd/app.rs
+++ b/src-tauri/src/cmd/app.rs
@@ -103,6 +103,130 @@ pub fn get_app_runtime_info(
state.info()
}
+#[derive(Clone, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AppSupportInfo {
+ os: String,
+ architecture: String,
+ runtime: String,
+}
+
+#[tauri::command]
+pub fn get_support_info(state: tauri::State<'_, crate::runtime::AppRuntime>) -> AppSupportInfo {
+ AppSupportInfo {
+ os: operating_system_label(),
+ architecture: std::env::consts::ARCH.to_string(),
+ runtime: support_runtime_label(state.portable()).to_string(),
+ }
+}
+
+fn support_runtime_label(portable: bool) -> &'static str {
+ if portable { "portable" } else { "installed" }
+}
+
+fn operating_system_label() -> String {
+ #[cfg(target_os = "windows")]
+ {
+ return windows_version_label();
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ return command_output("sw_vers", &["-productName", "-productVersion"])
+ .unwrap_or_else(|| "macOS".to_string());
+ }
+
+ #[cfg(target_os = "linux")]
+ {
+ if let Ok(contents) = std::fs::read_to_string("/etc/os-release") {
+ let name = contents
+ .lines()
+ .find_map(|line| line.strip_prefix("PRETTY_NAME="))
+ .map(|value| value.trim_matches('"').to_string());
+ if let Some(name) = name {
+ return name;
+ }
+ }
+ return "Linux".to_string();
+ }
+
+ #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
+ {
+ std::env::consts::OS.to_string()
+ }
+}
+
+#[cfg(target_os = "windows")]
+fn windows_version_label() -> String {
+ use windows::Wdk::System::SystemServices::RtlGetVersion;
+ use windows::Win32::System::SystemInformation::OSVERSIONINFOW;
+
+ let mut version = OSVERSIONINFOW {
+ dwOSVersionInfoSize: std::mem::size_of::() as u32,
+ ..Default::default()
+ };
+
+ // RtlGetVersion queries the current Windows version in-process, avoiding a console process.
+ if unsafe { RtlGetVersion(&mut version) }.is_ok() {
+ windows_version_label_from_parts(
+ version.dwMajorVersion,
+ version.dwMinorVersion,
+ version.dwBuildNumber,
+ )
+ } else {
+ "Windows".to_string()
+ }
+}
+
+#[cfg(any(target_os = "windows", test))]
+fn windows_version_label_from_parts(major: u32, minor: u32, build: u32) -> String {
+ let name = match (major, minor, build) {
+ (10, 0, 22_000..) => "Windows 11",
+ (10, 0, _) => "Windows 10",
+ (6, 3, _) => "Windows 8.1",
+ (6, 2, _) => "Windows 8",
+ (6, 1, _) => "Windows 7",
+ _ => "Windows",
+ };
+ format!("{name} ({major}.{minor}.{build})")
+}
+
+#[cfg(target_os = "macos")]
+fn command_output(program: &str, args: &[&str]) -> Option {
+ let output = std::process::Command::new(program)
+ .args(args)
+ .output()
+ .ok()?;
+ if !output.status.success() {
+ return None;
+ }
+ let value = String::from_utf8(output.stdout).ok()?.trim().to_string();
+ (!value.is_empty()).then_some(value)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{support_runtime_label, windows_version_label_from_parts};
+
+ #[test]
+ fn support_runtime_label_matches_installation_mode() {
+ assert_eq!(support_runtime_label(true), "portable");
+ assert_eq!(support_runtime_label(false), "installed");
+ }
+
+ #[test]
+ fn windows_version_label_uses_native_version_parts() {
+ assert_eq!(
+ windows_version_label_from_parts(10, 0, 22_631),
+ "Windows 11 (10.0.22631)"
+ );
+ assert_eq!(
+ windows_version_label_from_parts(10, 0, 19_045),
+ "Windows 10 (10.0.19045)"
+ );
+ }
+}
+
#[tauri::command]
pub fn get_app_lock_state(state: tauri::State<'_, AppLockState>) -> bool {
state.is_locked()
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index fb6c2d53..012383bd 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -115,6 +115,7 @@ pub fn run() {
cmd::app::open_download_dir,
cmd::app::open_log_dir,
cmd::app::get_app_runtime_info,
+ cmd::app::get_support_info,
cmd::app::get_app_lock_state,
cmd::app::set_app_lock_state,
cmd::app::open_child_window,
diff --git a/src/components/dialog/app/AboutDialog.tsx b/src/components/dialog/app/AboutDialog.tsx
index 26186570..2c751b9b 100644
--- a/src/components/dialog/app/AboutDialog.tsx
+++ b/src/components/dialog/app/AboutDialog.tsx
@@ -1,7 +1,9 @@
import { getName, getVersion } from "@tauri-apps/api/app";
import { openUrl } from "@tauri-apps/plugin-opener";
+import { Check, Copy } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
+import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -12,6 +14,9 @@ import {
} from "@/components/ui/dialog";
import pkg from "../../../../package.json";
import NyaTermLogo from "../../NyaTermLogo";
+import { writeClipboardText } from "@/lib/clipboard";
+import { invoke } from "@/lib/invoke";
+import type { AppSupportInfo } from "@/types/global";
interface AboutDialogProps {
open: boolean;
@@ -22,17 +27,52 @@ export default function AboutDialog({ open, onClose }: AboutDialogProps) {
const { t } = useTranslation();
const [appName, setAppName] = useState("NyaTerm");
const [appVersion, setAppVersion] = useState("0.1.0");
+ const [supportInfo, setSupportInfo] = useState({
+ os: "unknown",
+ architecture: "unknown",
+ runtime: "installed",
+ });
+ const [copied, setCopied] = useState(false);
useEffect(() => {
if (open) {
getName().then(setAppName).catch(console.error);
getVersion().then(setAppVersion).catch(console.error);
+ invoke("get_support_info")
+ .then(setSupportInfo)
+ .catch(() => undefined);
+ setCopied(false);
}
}, [open]);
+ const copySupportInfo = async () => {
+ const text = [
+ "NyaTerm Support Information",
+ `Version: ${appVersion}`,
+ `Operating System: ${supportInfo.os}`,
+ `Architecture: ${supportInfo.architecture}`,
+ `Runtime: ${supportInfo.runtime === "portable" ? t("about.portable") : t("about.installed")}`,
+ ].join("\n");
+ try {
+ await writeClipboardText(text);
+ setCopied(true);
+ toast.success(t("about.supportInfoCopied"));
+ window.setTimeout(() => setCopied(false), 1800);
+ } catch {
+ toast.error(t("about.supportInfoCopyFailed"));
+ }
+ };
+
+ const supportRows = [
+ [t("about.version"), appVersion],
+ [t("about.operatingSystem"), supportInfo.os === "unknown" ? t("about.unknown") : supportInfo.os],
+ [t("about.architecture"), supportInfo.architecture === "unknown" ? t("about.unknown") : supportInfo.architecture],
+ [t("about.runtime"), supportInfo.runtime === "portable" ? t("about.portable") : t("about.installed")],
+ ];
+
return (
+
+
+
{t("about.supportInfo")}
+
+
+
+ {supportRows.map(([label, value]) => (
+
+
- {label}
+ - {value}
+
+ ))}
+
+
+