Ely Office: six offline-first desktop apps in Rust and GPUI

ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
use ely_platform::AppId;
use gpui::{Action, App, KeyBinding, Menu, MenuItem, SharedString, Window, actions};
use crate::i18n;
actions!(
office,
[
New,
Open,
Save,
SaveAs,
Rename,
Export,
Print,
CloseTab,
ToggleCommandPalette,
OpenSettings,
Quit
]
);
actions!(
apps,
[OpenElyDoc, OpenElyTable, OpenElyShow, OpenElyPdf, OpenElyMail, OpenElyTree, OpenElyMark, OpenElyAgent]
);
/// The action that starts `app` (§16 app switching).
pub fn switch_to(app: AppId) -> Box<dyn Action> {
match app {
AppId::Doc => Box::new(OpenElyDoc),
AppId::Table => Box::new(OpenElyTable),
AppId::Show => Box::new(OpenElyShow),
AppId::Pdf => Box::new(OpenElyPdf),
AppId::Mail => Box::new(OpenElyMail),
AppId::Tree => Box::new(OpenElyTree),
AppId::Mark => Box::new(OpenElyMark),
AppId::Agent => Box::new(OpenElyAgent),
}
}
/// One command: an id and a chord.
pub struct Command {
pub id: &'static str,
pub keys: &'static str,
pub blocked: Option<&'static str>,
pub action: fn() -> Box<dyn Action>,
}
const BLOCKED: &str = "command.blocked.no_document";
const NEW: Command = Command {
id: "file.new",
keys: "secondary-n",
blocked: None,
action: || Box::new(New),
};
const OPEN: Command = Command {
id: "file.open",
keys: "secondary-o",
blocked: None,
action: || Box::new(Open),
};
const SAVE: Command = Command {
id: "file.save",
keys: "secondary-s",
blocked: Some(BLOCKED),
action: || Box::new(Save),
};
const SAVE_AS: Command = Command {
id: "file.save_as",
keys: "secondary-shift-s",
blocked: Some(BLOCKED),
action: || Box::new(SaveAs),
};
const RENAME: Command = Command {
id: "file.rename",
keys: "f2",
blocked: Some(BLOCKED),
action: || Box::new(Rename),
};
const EXPORT: Command = Command {
id: "file.export",
keys: "secondary-e",
blocked: Some(BLOCKED),
action: || Box::new(Export),
};
const PRINT: Command = Command {
id: "file.print",
keys: "secondary-p",
blocked: Some(BLOCKED),
action: || Box::new(Print),
};
pub const FILE: [&Command; 6] = [&NEW, &OPEN, &SAVE, &SAVE_AS, &EXPORT, &PRINT];
/// A document app that also renames its files.
pub const RENAMING: [&Command; 7] = [&NEW, &OPEN, &SAVE, &SAVE_AS, &RENAME, &EXPORT, &PRINT];
/// A tool that starts and opens, but saves nothing itself.
pub const OPENING: [&Command; 2] = [&NEW, &OPEN];
pub const TOGGLE_PALETTE: &str = "secondary-shift-p";
pub const CONTEXT: &str = "office";
fn label(command: &Command, cx: &App) -> SharedString {
i18n::text(cx, &format!("command.{}", command.id), &[])
}
/// Why `id` cannot run; a document lifts the no-document block.
pub fn blocked(id: &str, document: bool, cx: &App) -> Option<SharedString> {
let command = RENAMING.iter().find(|command| command.id == id)?;
let key = command.blocked.filter(|_| !document)?;
Some(i18n::text(cx, key, &[]))
}
/// The app menu, File with `file`'s commands, then `extra`.
pub fn menus(
app: AppId,
file: &[&Command],
document: bool,
extra: Vec<Menu>,
cx: &App,
) -> Vec<Menu> {
let items = file.iter().map(|command| {
let off = blocked(command.id, document, cx).is_some();
MenuItem::Action {
name: label(command, cx),
action: (command.action)(),
os_action: None,
checked: false,
disabled: off,
}
});
// The other installed Ely apps, to switch to.
let others: Vec<MenuItem> = AppId::ALL
.into_iter()
.filter(|other| *other != app && ely_platform::apps::installed(*other))
.map(|other| MenuItem::Action {
name: other.name().into(),
action: switch_to(other),
os_action: None,
checked: false,
disabled: false,
})
.collect();
let mut first = Vec::new();
if !others.is_empty() {
first.push(MenuItem::submenu(Menu::new(i18n::text(cx, "menu.app.switch", &[])).items(others)));
first.push(MenuItem::separator());
}
first.push(MenuItem::action(i18n::text(cx, "menu.app.settings", &[]), OpenSettings));
first.push(MenuItem::separator());
first.push(MenuItem::action(i18n::text(cx, "menu.app.quit", &[("app", app.name())]), Quit));
let mut menus = vec![
Menu::new(app.name()).items(first),
Menu::new(i18n::text(cx, "menu.file", &[])).items(items.collect::<Vec<_>>()),
];
menus.extend(extra);
menus
}
pub fn bind_keys(cx: &mut App) {
cx.bind_keys([
KeyBinding::new(NEW.keys, New, Some(CONTEXT)),
KeyBinding::new(OPEN.keys, Open, Some(CONTEXT)),
KeyBinding::new(SAVE.keys, Save, Some(CONTEXT)),
KeyBinding::new(SAVE_AS.keys, SaveAs, Some(CONTEXT)),
KeyBinding::new(RENAME.keys, Rename, Some(CONTEXT)),
KeyBinding::new(EXPORT.keys, Export, Some(CONTEXT)),
KeyBinding::new(PRINT.keys, Print, Some(CONTEXT)),
KeyBinding::new("secondary-w", CloseTab, Some(CONTEXT)),
KeyBinding::new(TOGGLE_PALETTE, ToggleCommandPalette, Some(CONTEXT)),
KeyBinding::new("secondary-q", Quit, None),
KeyBinding::new("secondary-,", OpenSettings, None),
]);
}
pub fn run(id: &str, window: &mut Window, cx: &mut App) {
match RENAMING.iter().find(|command| command.id == id) {
Some(command) => window.dispatch_action((command.action)(), cx),
None => log::warn!("commands: {id} is not a command"),
}
}
+219
View File
@@ -0,0 +1,219 @@
use ely_gpui_component::i18n::I18n;
mod analysis;
mod calc;
mod data;
mod doc;
mod mark;
mod motion;
mod office;
mod output;
mod pdf;
mod play;
mod refs;
mod secure;
mod settings;
mod show;
mod table;
mod tools;
mod tree;
mod tree_ops;
mod tree_places;
mod web;
use gpui::{App, SharedString};
const EN: &[(&str, &str)] = &[
("menu.app.quit", "Quit {app}"),
("menu.app.switch", "Open Ely App"),
("toast.open", "Open"),
("toast.open_failed", "The file did not open: {error}"),
("menu.app.switch_failed", "{app} did not open: {error}"),
("menu.file", "File"),
("menu.insert", "Insert"),
("command.file.new", "New"),
("command.file.open", "Open…"),
("command.file.save", "Save"),
("command.file.save_as", "Save As…"),
("command.file.rename", "Rename…"),
("command.file.export", "Export…"),
("command.file.print", "Print…"),
("command.blocked.no_document", "No open document"),
("palette.group.file", "File"),
("gallery.ready", "Ready"),
("gallery.nav", "Navigation"),
("gallery.inbox", "Inbox"),
("gallery.archive", "Archive"),
("gallery.props", "Properties"),
("gallery.content", "Content"),
("gallery.mixed", "Mixed"),
("gallery.content_of", "Content of {page}"),
("gallery.rename", "Rename…"),
("gallery.delete", "Delete…"),
("gallery.group.selection", "Selection"),
("gallery.row.format", "Format"),
("gallery.row.pages", "Pages"),
("gallery.empty.pages", "No pages"),
("gallery.task.done", "Export done"),
("gallery.task.cancelled", "Export cancelled"),
("gallery.task.progress", "Export {state} · Cancel"),
("gallery.task.retry", "{error} · Retry"),
("gallery.error.disk_full", "The disk is full"),
("gallery.rename.title", "Rename Page"),
("gallery.rename.submit", "Rename"),
("gallery.rename.empty", "A page needs a name"),
("gallery.rename.placeholder", "Page name"),
(
"gallery.rename.failed",
"The disk is full; the name was not saved",
),
("gallery.delete.title", "Delete {title}?"),
(
"gallery.delete.message",
"{title} and its items are gone for good.",
),
("gallery.delete.confirm", "Delete Page"),
];
const ZH_CN: &[(&str, &str)] = &[
("menu.app.quit", "退出 {app}"),
("menu.app.switch", "打开 Ely 应用"),
("toast.open", "打开"),
("toast.open_failed", "文件没能打开:{error}"),
("menu.app.switch_failed", "{app} 没能打开:{error}"),
("menu.file", "文件"),
("menu.insert", "插入"),
("command.file.new", "新建"),
("command.file.open", "打开…"),
("command.file.save", "保存"),
("command.file.save_as", "另存为…"),
("command.file.rename", "重命名…"),
("command.file.export", "导出…"),
("command.file.print", "打印…"),
("command.blocked.no_document", "没有打开的文档"),
("palette.group.file", "文件"),
("gallery.ready", "就绪"),
("gallery.nav", "导航"),
("gallery.inbox", "收件箱"),
("gallery.archive", "归档"),
("gallery.props", "属性"),
("gallery.content", "内容"),
("gallery.mixed", "混合"),
("gallery.content_of", "{page}的内容"),
("gallery.rename", "重命名…"),
("gallery.delete", "删除…"),
("gallery.group.selection", "所选"),
("gallery.row.format", "格式"),
("gallery.row.pages", "页数"),
("gallery.empty.pages", "没有页面"),
("gallery.task.done", "导出完成"),
("gallery.task.cancelled", "导出已取消"),
("gallery.task.progress", "导出 {state} · 取消"),
("gallery.task.retry", "{error} · 重试"),
("gallery.error.disk_full", "磁盘已满"),
("gallery.rename.title", "重命名页面"),
("gallery.rename.submit", "重命名"),
("gallery.rename.empty", "页面需要名称"),
("gallery.rename.placeholder", "页面名称"),
("gallery.rename.failed", "磁盘已满,名称未保存"),
("gallery.delete.title", "删除{title}?"),
("gallery.delete.message", "{title}及其内容将被永久删除。"),
("gallery.delete.confirm", "删除页面"),
];
/// zh-* maps to zh-CN; anything else to en-US.
pub fn resolve(tag: &str) -> &'static str {
if tag.to_ascii_lowercase().starts_with("zh") {
"zh-CN"
} else {
"en-US"
}
}
/// Installs the catalogs and resolves the locale.
pub fn install(cx: &mut App) {
let tag = locale_tag(cx);
let locale = resolve(&tag);
log::info!("i18n: locale {tag} -> {locale}");
let en: Vec<_> = EN
.iter()
.chain(office::EN)
.chain(doc::EN)
.chain(refs::EN)
.chain(tools::EN)
.chain(table::EN)
.chain(calc::EN)
.chain(data::EN)
.chain(analysis::EN)
.chain(output::EN)
.chain(show::EN)
.chain(motion::EN)
.chain(play::EN)
.chain(pdf::EN)
.chain(secure::EN)
.chain(tree::EN)
.chain(tree_ops::EN)
.chain(tree_places::EN)
.chain(web::EN)
.chain(mark::EN)
.chain(settings::EN)
.copied()
.collect();
let zh: Vec<_> = ZH_CN
.iter()
.chain(office::ZH_CN)
.chain(doc::ZH_CN)
.chain(refs::ZH_CN)
.chain(tools::ZH_CN)
.chain(table::ZH_CN)
.chain(calc::ZH_CN)
.chain(data::ZH_CN)
.chain(analysis::ZH_CN)
.chain(output::ZH_CN)
.chain(show::ZH_CN)
.chain(motion::ZH_CN)
.chain(play::ZH_CN)
.chain(pdf::ZH_CN)
.chain(secure::ZH_CN)
.chain(tree::ZH_CN)
.chain(tree_ops::ZH_CN)
.chain(tree_places::ZH_CN)
.chain(web::ZH_CN)
.chain(mark::ZH_CN)
.chain(settings::ZH_CN)
.copied()
.collect();
cx.set_global(
I18n::new(locale)
.catalog("en-US", &en)
.catalog("zh-CN", &zh),
);
}
/// `ELY_LOCALE`, then the language setting, then the system's.
fn locale_tag(cx: &App) -> String {
use ely_platform::settings::{Language, word};
match crate::settings::current(cx).language {
Language::System => ely_platform::locale::system(),
_ if ely_platform::env::value("ELY_LOCALE").is_some() => ely_platform::locale::system(),
chosen => word(&Language::ALL, &chosen).into(),
}
}
pub fn text(cx: &App, key: &str, args: &[(&str, &str)]) -> SharedString {
cx.global::<I18n>().text(key, args)
}
#[cfg(test)]
mod tests {
use super::resolve;
#[test]
fn zh_variants_pick_zh_cn_others_pick_en() {
for tag in ["zh-CN", "zh-Hans", "zh", "ZH-TW"] {
assert_eq!(resolve(tag), "zh-CN", "{tag}");
}
for tag in ["en", "en-US", "fr", "ja", "", "x9!"] {
assert_eq!(resolve(tag), "en-US", "{tag}");
}
}
}
+57
View File
@@ -0,0 +1,57 @@
//! ElyTable's pivot table and chart messages.
pub(super) const EN: &[(&str, &str)] = &[
("table.pivot.make", "Pivot Table…"),
("table.pivot.refresh", "Refresh Pivot Table"),
("table.pivot.title", "Pivot Table"),
("table.pivot.source", "Source: {range}"),
("table.pivot.unused", "Not used"),
("table.pivot.rows", "Rows"),
("table.pivot.columns", "Columns"),
("table.pivot.pages", "Filters"),
("table.pivot.data", "Values"),
("table.pivot.sum", "Sum"),
("table.pivot.count", "Count"),
("table.pivot.average", "Average"),
("table.pivot.max", "Max"),
("table.pivot.min", "Min"),
("table.pivot.other", "Other"),
("table.pivot.function_of", "{field} summary"),
("table.pivot.go", "Create"),
("table.pivot.apply", "Apply"),
(
"table.pivot.headers",
"Every column needs a header to make a pivot table.",
),
("table.pivot.none", "The cursor is not in a pivot table."),
("table.chart.make", "Chart…"),
("table.chart.range", "Data range"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("table.pivot.make", "数据透视表…"),
("table.pivot.refresh", "刷新数据透视表"),
("table.pivot.title", "数据透视表"),
("table.pivot.source", "源数据:{range}"),
("table.pivot.unused", "不使用"),
("table.pivot.rows", "行"),
("table.pivot.columns", "列"),
("table.pivot.pages", "筛选"),
("table.pivot.data", "值"),
("table.pivot.sum", "求和"),
("table.pivot.count", "计数"),
("table.pivot.average", "平均值"),
("table.pivot.max", "最大值"),
("table.pivot.min", "最小值"),
("table.pivot.other", "其他"),
("table.pivot.function_of", "{field} 汇总方式"),
("table.pivot.go", "创建"),
("table.pivot.apply", "应用"),
(
"table.pivot.headers",
"每一列都需要标题才能创建数据透视表。",
),
("table.pivot.none", "光标不在数据透视表中。"),
("table.chart.make", "图表…"),
("table.chart.range", "数据区域"),
];
+140
View File
@@ -0,0 +1,140 @@
//! ElyTable's formula, calculation and name messages.
pub(super) const EN: &[(&str, &str)] = &[
("menu.formula", "Formula"),
("table.calc.auto", "Automatic Calculation"),
("table.calc.now", "Recalculate"),
("table.calc.all", "Recalculate All"),
("table.calc.manual", "Manual calculation"),
("table.names.define", "Define Name…"),
("table.names.manage", "Names…"),
("table.names.define_title", "Define Name"),
("table.names.manage_title", "Names"),
("table.names.name", "Name"),
("table.names.refers", "Refers to"),
("table.names.scope", "Scope"),
("table.names.workbook", "Workbook"),
("table.names.go_define", "Define"),
("table.names.apply", "Apply"),
("table.names.goto", "Go To"),
("table.names.remove", "Delete"),
("table.names.entry", "{name} ({scope}) {formula}"),
(
"table.names.entry_broken",
"{name} ({scope}) {formula} Invalid reference",
),
("table.names.none", "This workbook has no names."),
(
"table.names.unresolved",
"Several ranges are selected; type what the name refers to.",
),
(
"table.names.taken",
"{name} already names a range in this scope.",
),
(
"table.names.address",
"A name cannot read as a cell address.",
),
(
"table.names.characters",
"A name starts with a letter or underscore and has no spaces or symbols.",
),
("table.trace.error", "Trace Error"),
("table.trace.precedents", "Trace Precedents"),
("table.trace.dependents", "Trace Dependents"),
("table.trace.clear", "Remove Arrows"),
("table.error.div_zero", "Error: division by zero"),
("table.error.no_value", "Error: wrong type of value"),
("table.error.no_ref", "Error: invalid reference"),
("table.error.no_name", "Error: unknown name"),
("table.error.number", "Error: invalid number"),
("table.error.not_available", "Error: value not available"),
("table.error.no_addin", "Error: add-in not found"),
("table.error.no_macro", "Error: macro not found"),
("table.error.syntax", "Error: unreadable formula"),
("table.error.no_code", "Error: no intersection"),
("table.error.argument", "Error: invalid argument"),
("table.error.parameters", "Error: wrong arguments"),
("table.error.character", "Error: invalid character"),
("table.error.brackets", "Error: unmatched brackets"),
("table.error.operator", "Error: missing operator"),
("table.error.variable", "Error: missing operand"),
("table.error.formula_overflow", "Error: formula too long"),
("table.error.string_overflow", "Error: text too long"),
("table.error.internal_overflow", "Error: formula too deep"),
("table.error.array_size", "Error: array size"),
("table.error.circular", "Error: circular reference"),
("table.error.converge", "Error: iteration does not converge"),
("table.error.nested_array", "Error: nested arrays"),
(
"table.error.array_content",
"Error: unsupported array content",
),
("table.error.external", "Error: external content disabled"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("menu.formula", "公式"),
("table.calc.auto", "自动计算"),
("table.calc.now", "重新计算"),
("table.calc.all", "全部重新计算"),
("table.calc.manual", "手动计算"),
("table.names.define", "定义名称…"),
("table.names.manage", "名称…"),
("table.names.define_title", "定义名称"),
("table.names.manage_title", "名称"),
("table.names.name", "名称"),
("table.names.refers", "引用位置"),
("table.names.scope", "范围"),
("table.names.workbook", "工作簿"),
("table.names.go_define", "定义"),
("table.names.apply", "应用"),
("table.names.goto", "转到"),
("table.names.remove", "删除"),
("table.names.entry", "{name}({scope}) {formula}"),
(
"table.names.entry_broken",
"{name}({scope}) {formula} 引用无效",
),
("table.names.none", "此工作簿没有名称。"),
(
"table.names.unresolved",
"选中了多个区域,请填写名称的引用位置。",
),
("table.names.taken", "此范围内已有名称 {name}。"),
("table.names.address", "名称不能读作单元格地址。"),
(
"table.names.characters",
"名称以字母或下划线开头,不含空格或符号。",
),
("table.trace.error", "追踪错误"),
("table.trace.precedents", "追踪引用单元格"),
("table.trace.dependents", "追踪从属单元格"),
("table.trace.clear", "移去箭头"),
("table.error.div_zero", "错误:除以零"),
("table.error.no_value", "错误:值的类型不对"),
("table.error.no_ref", "错误:引用无效"),
("table.error.no_name", "错误:名称未知"),
("table.error.number", "错误:数值无效"),
("table.error.not_available", "错误:值不可用"),
("table.error.no_addin", "错误:找不到加载项"),
("table.error.no_macro", "错误:找不到宏"),
("table.error.syntax", "错误:公式无法读取"),
("table.error.no_code", "错误:无交集"),
("table.error.argument", "错误:参数无效"),
("table.error.parameters", "错误:参数不对"),
("table.error.character", "错误:字符无效"),
("table.error.brackets", "错误:括号不成对"),
("table.error.operator", "错误:缺少运算符"),
("table.error.variable", "错误:缺少运算数"),
("table.error.formula_overflow", "错误:公式过长"),
("table.error.string_overflow", "错误:文本过长"),
("table.error.internal_overflow", "错误:公式嵌套过深"),
("table.error.array_size", "错误:数组大小"),
("table.error.circular", "错误:循环引用"),
("table.error.converge", "错误:迭代不收敛"),
("table.error.nested_array", "错误:嵌套数组"),
("table.error.array_content", "错误:不支持的数组内容"),
("table.error.external", "错误:外部内容已停用"),
];
+321
View File
@@ -0,0 +1,321 @@
//! ElyTable's format, data and rule messages.
pub(super) const EN: &[(&str, &str)] = &[
("menu.data", "Data"),
("menu.format", "Format"),
("table.format.panel", "Format Panel"),
("table.format.number", "Number"),
("table.format.kind", "Format"),
("table.format.code", "Code"),
("table.format.general", "General"),
("table.format.decimal", "Number"),
("table.format.currency", "Currency"),
("table.format.percent", "Percent"),
("table.format.date", "Date"),
("table.format.custom", "Custom"),
("table.format.invalid", "\"{code}\" is not a format code."),
("table.format.alignment", "Alignment"),
("table.format.across", "Horizontal"),
("table.format.down", "Vertical"),
("table.format.text", "Text"),
("table.format.left", "Left"),
("table.format.center", "Center"),
("table.format.right", "Right"),
("table.format.justify", "Justify"),
("table.format.top", "Top"),
("table.format.middle", "Middle"),
("table.format.bottom", "Bottom"),
("table.format.wrap", "Wrap"),
("table.format.cell", "Cell"),
("table.format.borders", "Borders"),
("table.format.borders_all", "All"),
("table.format.borders_outer", "Outside"),
("table.format.borders_none", "None"),
("table.format.fill", "Fill"),
("table.format.no_fill", "No Fill"),
("table.data.sort", "Sort…"),
("table.data.autofilter", "AutoFilter"),
("table.data.filter", "Filter…"),
("table.data.unfilter", "Clear Filter"),
("table.data.dedupe", "Remove Duplicates…"),
("table.data.columns", "Text to Columns…"),
("table.data.paste_values", "Paste Values"),
("table.data.paste_formulas", "Paste Formulas"),
(
"table.data.paste_foreign",
"Paste Values and Paste Formulas take cells copied in this workbook. Use Paste for other content.",
),
(
"table.data.clipboard",
"The clipboard is unreadable: {error}",
),
("table.data.header", "First row is headers"),
("table.data.empty", "The selection holds no data."),
("table.data.one_range", "Select one range."),
("table.data.three", "Sorting takes three keys at most."),
("table.data.sort_title", "Sort"),
("table.data.sort_go", "Sort"),
("table.data.filter_title", "Filter"),
("table.data.filter_go", "Filter"),
("table.data.dedupe_title", "Remove Duplicates"),
(
"table.data.dedupe_detail",
"Rows in {range} that repeat an earlier row are removed.",
),
("table.data.dedupe_go", "Remove"),
("table.columns.title", "Text to Columns"),
("table.columns.tab", "Tab"),
("table.columns.comma", "Comma"),
("table.columns.semicolon", "Semicolon"),
("table.columns.space", "Space"),
("table.columns.other", "Other"),
("table.columns.go", "Split"),
("table.columns.one", "Select cells in one column."),
("table.filter.title", "Column {column}"),
("table.filter.up", "Sort A to Z"),
("table.filter.down", "Sort Z to A"),
("table.filter.all", "All"),
("table.filter.empty", "(Empty)"),
("table.filter.go", "Filter"),
("table.rules.validation", "Validation…"),
("table.rules.unvalidate", "Clear Validation"),
("table.rules.validation_title", "Validation"),
("table.rules.allow", "Allow"),
("table.rules.any", "Any value"),
("table.rules.whole", "Whole number"),
("table.rules.decimal", "Decimal"),
("table.rules.date", "Date"),
("table.rules.time", "Time"),
("table.rules.textlength", "Text length"),
("table.rules.list", "List"),
("table.rules.custom", "Formula"),
("table.rules.data", "Data"),
("table.rules.value", "Value"),
("table.rules.and", "And"),
("table.rules.formula", "Formula"),
("table.rules.entries", "Entries"),
("table.rules.dropdown", "Show list"),
("table.rules.blank", "Allow blank"),
("table.rules.help", "Input help"),
("table.rules.alert", "Error alert"),
("table.rules.style", "Style"),
("table.rules.title", "Title"),
("table.rules.message", "Message"),
("table.rules.stop", "Stop"),
("table.rules.warning", "Warning"),
("table.rules.info", "Information"),
("table.rules.apply", "Apply"),
("table.rules.choose", "Choose"),
("table.rules.between", "between"),
("table.rules.notbetween", "not between"),
("table.rules.equal", "equal to"),
("table.rules.notequal", "not equal to"),
("table.rules.greater", "greater than"),
("table.rules.less", "less than"),
("table.rules.greaterequal", "at least"),
("table.rules.lessequal", "at most"),
("table.rules.duplicate", "duplicate"),
("table.rules.unique", "unique"),
("table.rules.contains", "contains"),
("table.rules.beginswith", "begins with"),
("table.rules.endswith", "ends with"),
("table.rules.top", "top N"),
("table.rules.bottom", "bottom N"),
("table.rules.above_average", "above average"),
("table.rules.below_average", "below average"),
("table.rules.error", "an error"),
("table.rules.conditional", "Conditional Format…"),
("table.rules.conditionals", "Conditional Formats…"),
("table.rules.conditional_title", "Conditional Format"),
("table.rules.kind", "Type"),
("table.rules.cell", "Cell rule"),
("table.rules.scale", "Color scale"),
("table.rules.databar", "Data bar"),
("table.rules.condition", "When the cell is"),
("table.rules.cell_style", "Style"),
("table.rules.colors", "Colors"),
("table.rules.scale_rgy", "Red, yellow, green"),
("table.rules.scale_gyr", "Green, yellow, red"),
("table.rules.scale_wr", "White to red"),
("table.rules.scale_wg", "White to green"),
("table.rules.bar_blue", "Blue"),
("table.rules.bar_green", "Green"),
("table.rules.bar_red", "Red"),
("table.rules.bar_orange", "Orange"),
("table.rules.add", "Add"),
("table.rules.formats_title", "Conditional Formats"),
("table.rules.entry", "{range} {kind}"),
("table.rules.entry_cell", "{range} {style} {value}"),
("table.rules.remove", "Delete"),
(
"table.rules.no_formats",
"This sheet has no conditional formats.",
),
("table.style.good", "Good"),
("table.style.neutral", "Neutral"),
("table.style.bad", "Bad"),
("table.style.warning", "Warning"),
("table.style.error", "Error"),
("table.style.accent", "Accent"),
("table.style.accent1", "Accent 1"),
("table.style.accent2", "Accent 2"),
("table.style.accent3", "Accent 3"),
("table.style.note", "Note"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("menu.data", "数据"),
("menu.format", "格式"),
("table.format.panel", "格式面板"),
("table.format.number", "数字"),
("table.format.kind", "格式"),
("table.format.code", "代码"),
("table.format.general", "常规"),
("table.format.decimal", "数值"),
("table.format.currency", "货币"),
("table.format.percent", "百分比"),
("table.format.date", "日期"),
("table.format.custom", "自定义"),
("table.format.invalid", "“{code}”不是格式代码。"),
("table.format.alignment", "对齐"),
("table.format.across", "水平"),
("table.format.down", "垂直"),
("table.format.text", "文本"),
("table.format.left", "左对齐"),
("table.format.center", "居中"),
("table.format.right", "右对齐"),
("table.format.justify", "两端对齐"),
("table.format.top", "顶端"),
("table.format.middle", "居中"),
("table.format.bottom", "底端"),
("table.format.wrap", "自动换行"),
("table.format.cell", "单元格"),
("table.format.borders", "边框"),
("table.format.borders_all", "全部"),
("table.format.borders_outer", "外框"),
("table.format.borders_none", "无"),
("table.format.fill", "填充"),
("table.format.no_fill", "无填充"),
("table.data.sort", "排序…"),
("table.data.autofilter", "自动筛选"),
("table.data.filter", "筛选…"),
("table.data.unfilter", "清除筛选"),
("table.data.dedupe", "删除重复项…"),
("table.data.columns", "分列…"),
("table.data.paste_values", "粘贴为值"),
("table.data.paste_formulas", "粘贴为公式"),
(
"table.data.paste_foreign",
"粘贴为值和粘贴为公式只用于本工作簿复制的单元格;其他内容请用粘贴。",
),
("table.data.clipboard", "无法读取剪贴板:{error}"),
("table.data.header", "首行是标题"),
("table.data.empty", "所选区域没有数据。"),
("table.data.one_range", "请选一个区域。"),
("table.data.three", "排序最多三个关键字。"),
("table.data.sort_title", "排序"),
("table.data.sort_go", "排序"),
("table.data.filter_title", "筛选"),
("table.data.filter_go", "筛选"),
("table.data.dedupe_title", "删除重复项"),
(
"table.data.dedupe_detail",
"{range} 中与前面某行相同的行将被删除。",
),
("table.data.dedupe_go", "删除"),
("table.columns.title", "分列"),
("table.columns.tab", "制表符"),
("table.columns.comma", "逗号"),
("table.columns.semicolon", "分号"),
("table.columns.space", "空格"),
("table.columns.other", "其他"),
("table.columns.go", "分列"),
("table.columns.one", "请选同一列中的单元格。"),
("table.filter.title", "{column} 列"),
("table.filter.up", "升序排序"),
("table.filter.down", "降序排序"),
("table.filter.all", "全部"),
("table.filter.empty", "(空白)"),
("table.filter.go", "筛选"),
("table.rules.validation", "数据验证…"),
("table.rules.unvalidate", "清除数据验证"),
("table.rules.validation_title", "数据验证"),
("table.rules.allow", "允许"),
("table.rules.any", "任何值"),
("table.rules.whole", "整数"),
("table.rules.decimal", "小数"),
("table.rules.date", "日期"),
("table.rules.time", "时间"),
("table.rules.textlength", "文本长度"),
("table.rules.list", "序列"),
("table.rules.custom", "公式"),
("table.rules.data", "数据"),
("table.rules.value", "值"),
("table.rules.and", "与"),
("table.rules.formula", "公式"),
("table.rules.entries", "条目"),
("table.rules.dropdown", "显示下拉列表"),
("table.rules.blank", "允许空值"),
("table.rules.help", "输入提示"),
("table.rules.alert", "出错警告"),
("table.rules.style", "样式"),
("table.rules.title", "标题"),
("table.rules.message", "内容"),
("table.rules.stop", "停止"),
("table.rules.warning", "警告"),
("table.rules.info", "信息"),
("table.rules.apply", "应用"),
("table.rules.choose", "选择"),
("table.rules.between", "介于"),
("table.rules.notbetween", "未介于"),
("table.rules.equal", "等于"),
("table.rules.notequal", "不等于"),
("table.rules.greater", "大于"),
("table.rules.less", "小于"),
("table.rules.greaterequal", "大于或等于"),
("table.rules.lessequal", "小于或等于"),
("table.rules.duplicate", "重复值"),
("table.rules.unique", "唯一值"),
("table.rules.contains", "包含"),
("table.rules.beginswith", "开头是"),
("table.rules.endswith", "结尾是"),
("table.rules.top", "前 N 项"),
("table.rules.bottom", "后 N 项"),
("table.rules.above_average", "高于平均值"),
("table.rules.below_average", "低于平均值"),
("table.rules.error", "错误值"),
("table.rules.conditional", "条件格式…"),
("table.rules.conditionals", "管理条件格式…"),
("table.rules.conditional_title", "条件格式"),
("table.rules.kind", "类型"),
("table.rules.cell", "单元格规则"),
("table.rules.scale", "色阶"),
("table.rules.databar", "数据条"),
("table.rules.condition", "单元格值"),
("table.rules.cell_style", "样式"),
("table.rules.colors", "颜色"),
("table.rules.scale_rgy", "红、黄、绿"),
("table.rules.scale_gyr", "绿、黄、红"),
("table.rules.scale_wr", "白到红"),
("table.rules.scale_wg", "白到绿"),
("table.rules.bar_blue", "蓝"),
("table.rules.bar_green", "绿"),
("table.rules.bar_red", "红"),
("table.rules.bar_orange", "橙"),
("table.rules.add", "添加"),
("table.rules.formats_title", "条件格式"),
("table.rules.entry", "{range} {kind}"),
("table.rules.entry_cell", "{range} {style} {value}"),
("table.rules.remove", "删除"),
("table.rules.no_formats", "此工作表没有条件格式。"),
("table.style.good", "好"),
("table.style.neutral", "适中"),
("table.style.bad", "差"),
("table.style.warning", "警告"),
("table.style.error", "错误"),
("table.style.accent", "强调"),
("table.style.accent1", "强调 1"),
("table.style.accent2", "强调 2"),
("table.style.accent3", "强调 3"),
("table.style.note", "注释"),
];
+312
View File
@@ -0,0 +1,312 @@
//! ElyDoc's messages.
pub(super) const EN: &[(&str, &str)] = &[
("doc.new.title", "New Document"),
("doc.new.from_file", "From a Template File…"),
("doc.template.category", "Documents"),
("doc.template.blank", "Blank"),
("doc.template.blank.about", "An empty page"),
("doc.template.report", "Report"),
("doc.template.report.about", "A title and two sections"),
("doc.template.letter", "Letter"),
(
"doc.template.letter.about",
"Sender, date, recipient, greeting",
),
("doc.report.title", "Report Title"),
("doc.report.summary", "Summary"),
("doc.report.details", "Details"),
("doc.body", "Write here."),
("doc.letter.sender", "Your Name"),
("doc.letter.date", "Date"),
("doc.letter.recipient", "Recipient Name"),
("doc.letter.greeting", "Dear Recipient,"),
("doc.letter.closing", "Sincerely,"),
("doc.props.text", "Text"),
("doc.props.paragraph", "Paragraph"),
("doc.props.style", "Style"),
("doc.props.character", "Character"),
("doc.props.font", "Font"),
("doc.props.size", "Size"),
("doc.props.effects", "Effects"),
("doc.props.bold", "Bold"),
("doc.props.italic", "Italic"),
("doc.props.underline", "Underline"),
("doc.props.strikeout", "Strikethrough"),
("doc.props.superscript", "Superscript"),
("doc.props.subscript", "Subscript"),
("doc.props.color", "Color"),
("doc.props.highlight", "Highlight"),
("doc.props.no_highlight", "None"),
("doc.props.no_style", "The selection has no single style"),
("doc.props.align", "Alignment"),
("doc.props.left", "Align Left"),
("doc.props.center", "Center"),
("doc.props.right", "Align Right"),
("doc.props.justify", "Justify"),
("doc.props.indent", "Indent"),
("doc.props.line", "Line Spacing"),
("doc.props.above", "Space Above"),
("doc.props.below", "Space Below"),
("doc.props.less", "Less"),
("doc.props.more", "More"),
("doc.props.lists", "Lists"),
("doc.props.bullets", "Bullets"),
("doc.props.numbering", "Numbering"),
("doc.props.promote", "Promote"),
("doc.props.demote", "Demote"),
("doc.props.tabs", "Tab Stops"),
("doc.props.cm", "{n} cm"),
("doc.props.add_tab", "Add"),
("doc.props.choose", "Choose"),
("doc.props.new_style", "New…"),
("doc.props.update_style", "Update"),
("doc.style.new_paragraph", "New Paragraph Style"),
("doc.style.new_character", "New Character Style"),
("doc.style.name", "Name"),
("doc.style.create", "Create"),
("doc.style.empty", "A style needs a name"),
("doc.style.taken", "That name is taken"),
("doc.insert.table", "Table…"),
("doc.insert.image", "Image…"),
("doc.insert.page_break", "Page Break"),
("doc.insert.section_break", "Section Break"),
("doc.insert.page_number", "Page Number"),
("doc.insert.page_count", "Page Count"),
("doc.insert.table_title", "Insert Table"),
("doc.insert.columns", "Columns"),
("doc.insert.rows", "Rows"),
("doc.insert.insert", "Insert"),
("doc.props.page", "Page"),
("doc.props.paper", "Paper"),
("doc.props.size_cm", "{w} × {h} cm"),
("doc.props.orientation", "Orientation"),
("doc.props.portrait", "Portrait"),
("doc.props.landscape", "Landscape"),
("doc.props.top", "Top Margin"),
("doc.props.bottom", "Bottom Margin"),
("doc.props.margin_left", "Left Margin"),
("doc.props.margin_right", "Right Margin"),
("doc.props.columns", "Columns"),
("doc.props.section", "Section Style"),
("doc.props.header", "Header"),
("doc.props.footer", "Footer"),
("doc.props.on", "On"),
("doc.props.off", "Off"),
("doc.props.first_page", "Different First Page"),
("doc.props.odd_even", "Different Odd and Even"),
("doc.props.table", "Table"),
("doc.props.rows", "Rows"),
("doc.props.above_row", "Insert Above"),
("doc.props.below_row", "Insert Below"),
("doc.props.left_column", "Insert Left"),
("doc.props.right_column", "Insert Right"),
("doc.props.delete", "Delete"),
("doc.props.cells", "Cells"),
("doc.props.merge", "Merge"),
("doc.props.split_columns", "Split into Columns"),
("doc.props.split_rows", "Split into Rows"),
("doc.props.cell_size", "Size"),
("doc.props.even_columns", "Even Columns"),
("doc.props.even_rows", "Even Rows"),
("doc.props.fit_rows", "Fit Row Heights"),
("doc.props.borders", "Borders"),
("doc.props.borders_all", "All"),
("doc.props.borders_none", "None"),
("doc.props.shading", "Shading"),
("doc.props.heading", "Heading Row"),
("doc.props.repeat", "Repeat on Each Page"),
("doc.props.row_break", "Rows Across Pages"),
("doc.props.may_break", "Allow Breaking"),
("doc.props.keep_whole", "Keep Whole"),
("doc.props.delete_table", "Delete Table"),
("doc.props.object", "Image"),
("doc.props.width", "Width"),
("doc.props.height", "Height"),
("doc.props.image", "Image"),
("doc.props.crop", "Crop"),
("doc.props.replace", "Replace…"),
("doc.props.anchor", "Anchor"),
("doc.props.to_page", "Page"),
("doc.props.to_paragraph", "Paragraph"),
("doc.props.to_char", "Character"),
("doc.props.as_char", "As Character"),
("doc.props.wrap", "Wrap"),
("doc.props.wrap_none", "None"),
("doc.props.wrap_both", "Both Sides"),
("doc.props.wrap_ideal", "Optimal"),
("doc.props.wrap_left", "Left"),
("doc.props.wrap_right", "Right"),
("doc.props.wrap_through", "Through"),
("doc.props.order", "Order"),
("doc.props.front", "Bring to Front"),
("doc.props.forward", "Forward"),
("doc.props.backward", "Backward"),
("doc.props.back", "Send to Back"),
("doc.why.selection", "Not available for this selection"),
("doc.why.merge", "Select two or more cells to merge"),
("doc.why.heading", "Put the cursor in the table's first row"),
("doc.why.header", "Turn on the header first"),
("doc.why.footer", "Turn on the footer first"),
("doc.section.name", "Section {n}"),
("doc.section.title", "Insert a section break?"),
(
"doc.section.detail",
"The new section gets its own page style, copied from this page. The undo history clears.",
),
("doc.why.anchor", "This anchor does not fit here"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("doc.new.title", "新建文档"),
("doc.new.from_file", "从模板文件…"),
("doc.template.category", "文档"),
("doc.template.blank", "空白"),
("doc.template.blank.about", "一页空白"),
("doc.template.report", "报告"),
("doc.template.report.about", "标题与两节"),
("doc.template.letter", "信函"),
("doc.template.letter.about", "寄件人、日期、收件人、称呼"),
("doc.report.title", "报告标题"),
("doc.report.summary", "摘要"),
("doc.report.details", "详情"),
("doc.body", "在此书写。"),
("doc.letter.sender", "你的姓名"),
("doc.letter.date", "日期"),
("doc.letter.recipient", "收件人姓名"),
("doc.letter.greeting", "尊敬的收件人:"),
("doc.letter.closing", "此致"),
("doc.props.text", "文字"),
("doc.props.paragraph", "段落"),
("doc.props.style", "样式"),
("doc.props.character", "字符"),
("doc.props.font", "字体"),
("doc.props.size", "字号"),
("doc.props.effects", "效果"),
("doc.props.bold", "加粗"),
("doc.props.italic", "倾斜"),
("doc.props.underline", "下划线"),
("doc.props.strikeout", "删除线"),
("doc.props.superscript", "上标"),
("doc.props.subscript", "下标"),
("doc.props.color", "颜色"),
("doc.props.highlight", "高亮"),
("doc.props.no_highlight", "无"),
("doc.props.no_style", "所选内容没有单一样式"),
("doc.props.align", "对齐"),
("doc.props.left", "左对齐"),
("doc.props.center", "居中"),
("doc.props.right", "右对齐"),
("doc.props.justify", "两端对齐"),
("doc.props.indent", "缩进"),
("doc.props.line", "行距"),
("doc.props.above", "段前"),
("doc.props.below", "段后"),
("doc.props.less", "减少"),
("doc.props.more", "增加"),
("doc.props.lists", "列表"),
("doc.props.bullets", "项目符号"),
("doc.props.numbering", "编号"),
("doc.props.promote", "升级"),
("doc.props.demote", "降级"),
("doc.props.tabs", "制表位"),
("doc.props.cm", "{n} 厘米"),
("doc.props.add_tab", "添加"),
("doc.props.choose", "选择"),
("doc.props.new_style", "新建…"),
("doc.props.update_style", "更新"),
("doc.style.new_paragraph", "新建段落样式"),
("doc.style.new_character", "新建字符样式"),
("doc.style.name", "名称"),
("doc.style.create", "创建"),
("doc.style.empty", "样式需要名称"),
("doc.style.taken", "名称已存在"),
("doc.insert.table", "表格…"),
("doc.insert.image", "图片…"),
("doc.insert.page_break", "分页符"),
("doc.insert.section_break", "分节符"),
("doc.insert.page_number", "页码"),
("doc.insert.page_count", "总页数"),
("doc.insert.table_title", "插入表格"),
("doc.insert.columns", "列数"),
("doc.insert.rows", "行数"),
("doc.insert.insert", "插入"),
("doc.props.page", "页面"),
("doc.props.paper", "纸张"),
("doc.props.size_cm", "{w} × {h} 厘米"),
("doc.props.orientation", "方向"),
("doc.props.portrait", "纵向"),
("doc.props.landscape", "横向"),
("doc.props.top", "上边距"),
("doc.props.bottom", "下边距"),
("doc.props.margin_left", "左边距"),
("doc.props.margin_right", "右边距"),
("doc.props.columns", "分栏"),
("doc.props.section", "本节样式"),
("doc.props.header", "页眉"),
("doc.props.footer", "页脚"),
("doc.props.on", "开"),
("doc.props.off", "关"),
("doc.props.first_page", "首页不同"),
("doc.props.odd_even", "奇偶页不同"),
("doc.props.table", "表格"),
("doc.props.rows", "行"),
("doc.props.above_row", "上方插入"),
("doc.props.below_row", "下方插入"),
("doc.props.left_column", "左侧插入"),
("doc.props.right_column", "右侧插入"),
("doc.props.delete", "删除"),
("doc.props.cells", "单元格"),
("doc.props.merge", "合并"),
("doc.props.split_columns", "拆分为列"),
("doc.props.split_rows", "拆分为行"),
("doc.props.cell_size", "尺寸"),
("doc.props.even_columns", "平均列宽"),
("doc.props.even_rows", "平均行高"),
("doc.props.fit_rows", "适合行高"),
("doc.props.borders", "边框"),
("doc.props.borders_all", "全部"),
("doc.props.borders_none", "无"),
("doc.props.shading", "底色"),
("doc.props.heading", "标题行"),
("doc.props.repeat", "每页重复"),
("doc.props.row_break", "跨页"),
("doc.props.may_break", "允许断行"),
("doc.props.keep_whole", "保持整行"),
("doc.props.delete_table", "删除表格"),
("doc.props.object", "图片"),
("doc.props.width", "宽度"),
("doc.props.height", "高度"),
("doc.props.image", "图片"),
("doc.props.crop", "裁剪"),
("doc.props.replace", "替换…"),
("doc.props.anchor", "锚点"),
("doc.props.to_page", "到页"),
("doc.props.to_paragraph", "到段落"),
("doc.props.to_char", "到字符"),
("doc.props.as_char", "作为字符"),
("doc.props.wrap", "环绕"),
("doc.props.wrap_none", "无"),
("doc.props.wrap_both", "两侧"),
("doc.props.wrap_ideal", "最佳"),
("doc.props.wrap_left", "左侧"),
("doc.props.wrap_right", "右侧"),
("doc.props.wrap_through", "穿越"),
("doc.props.order", "层次"),
("doc.props.front", "置于顶层"),
("doc.props.forward", "上移一层"),
("doc.props.backward", "下移一层"),
("doc.props.back", "置于底层"),
("doc.why.selection", "当前选择不可用"),
("doc.why.merge", "选择两个以上单元格再合并"),
("doc.why.heading", "光标需在表格首行"),
("doc.why.header", "先打开页眉"),
("doc.why.footer", "先打开页脚"),
("doc.section.name", "第 {n} 节"),
("doc.section.title", "插入分节符?"),
(
"doc.section.detail",
"新节获得自己的页面样式,复制自当前页。撤销记录会清空。",
),
("doc.why.anchor", "此处不能用这种锚点"),
];
+193
View File
@@ -0,0 +1,193 @@
//! Words for ElyMark's page.
pub(super) const EN: &[(&str, &str)] = &[
("mark.search_language", "Search languages"),
("mark.no_result", "No match"),
("mark.copy", "Copy"),
("mark.preview", "Preview"),
("mark.loading", "Loading…"),
("mark.edit", "Edit"),
("mark.hide", "Hide"),
("mark.pane.files", "Files"),
("mark.pane.outline", "Outline"),
("mark.pane.search", "Search"),
("mark.recent", "Recent"),
("mark.recent.none", "Nothing opened yet"),
("mark.outline.none", "No headings"),
("mark.search", "Results"),
("mark.search.file", "This file"),
("mark.search.folder", "Folder"),
("mark.search.more", "The first {count} results"),
("mark.search.missed", "{name} was not searched"),
("mark.menu.table", "Table"),
("mark.table.insert", "Insert Table"),
("mark.table.row_above", "Add Row Above"),
("mark.table.row_below", "Add Row Below"),
("mark.table.column_left", "Add Column Left"),
("mark.table.column_right", "Add Column Right"),
("mark.table.delete_row", "Delete Row"),
("mark.table.delete_column", "Delete Column"),
("mark.table.none", "Put the caret in a table first"),
("mark.table.visual", "Tables change in the visual mode"),
("mark.attach.untitled", "Save the document first: images go beside it"),
("mark.attach.preview", "Switch to an editing mode to add images"),
("mark.attach.images", "Only images can be added"),
("mark.attach.moved", "The file moved while images copied; they are in {folder}, not linked"),
("mark.outside.changed", "{name} changed on disk; your unsaved text is kept"),
("mark.outside.deleted", "{name} was deleted on disk; your text is kept"),
("mark.outside.unread", "Changes to {name} on disk are not followed: {why}"),
("mark.outside.compare", "Compare…"),
("mark.outside.save_as", "Save As…"),
("mark.outside.refresh", "Check Again"),
("mark.outside.check", "Check File on Disk"),
("mark.compare.title", "{name}: yours and the disk's"),
("mark.compare.mine", "Yours"),
("mark.compare.base", "Last loaded or saved"),
("mark.compare.disk", "On disk"),
("mark.compare.use_disk", "Use the Disk's"),
("mark.compare.keep_mine", "Keep Yours"),
("mark.compare.use_merged", "Use the Merge"),
("mark.restore.missing", "A file from the last session did not open: {file}"),
("mark.compare.again", "The file changed on disk again; compare once more"),
("mark.restore.recovered", "{name}: unsaved text from {at} is back; its undo history is not"),
("mark.export", "Export"),
("mark.export.page", "HTML"),
("mark.export.linked", "HTML and Folder"),
("mark.export.pdf", "PDF"),
("mark.export.text", "Plain Text"),
("mark.export.go", "Export…"),
("mark.export.done", "Exported {name}"),
("mark.export.missing", "Exported without these images: {list}."),
("mark.export.diagrams", "Diagrams that did not draw stay as code, at lines {list}."),
("mark.export.formula", "The formula at line {line} did not render: {error}."),
("mark.export.failed", "The export failed: {error}"),
("mark.look.focus", "Focus Mode"),
("mark.look.typewriter", "Typewriter Scrolling"),
("mark.look.narrow", "Narrow Text"),
("mark.look.medium", "Medium Text"),
("mark.look.wide", "Wide Text"),
("mark.look.sans", "Sans-Serif Font"),
("mark.look.serif", "Serif Font"),
("mark.look.mono", "Monospace Font"),
("mark.look.larger", "Larger Text"),
("mark.look.smaller", "Smaller Text"),
("mark.look.actual", "Actual Size"),
("mark.git.unchanged", "Git: unchanged"),
("mark.git.changed", "Git: changed"),
("mark.git.untracked", "Git: untracked"),
("mark.git.ignored", "Git: ignored"),
("mark.git.conflicted", "Git: conflicted"),
("mark.git.unknown", "Git: unknown"),
("mark.git.open", "Show this file's history in ElyTree"),
("mark.git.missing", "ElyTree is not installed. Install ElyTree from the Ely Office release to see this file's history."),
("mark.git.failed", "ElyTree did not open: {error}"),
("mark.large", "This file is {size} MB: it shows as source. Visual and preview are off; saving keeps every byte."),
("mark.large.modes", "Large files stay in source mode"),
("mark.mode.visual", "Visual"),
("mark.mode.source", "Source"),
("mark.mode.preview", "Preview"),
("mark.rename", "Rename"),
("mark.rename.name", "Name"),
("mark.rename.apply", "Rename"),
("mark.rename.failed", "{name} was not renamed: {error}"),
("mark.rename.bad", "Enter a file name without slashes"),
("mark.rename.format", "The name must end in .{formats}"),
("mark.rename.untitled", "Save the document before renaming it"),
("mark.rename.saving", "Wait for the save to finish"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("mark.search_language", "搜索语言"),
("mark.no_result", "无匹配"),
("mark.copy", "拷贝"),
("mark.preview", "预览"),
("mark.loading", "正在载入…"),
("mark.edit", "编辑"),
("mark.hide", "隐藏"),
("mark.pane.files", "文件"),
("mark.pane.outline", "大纲"),
("mark.pane.search", "搜索"),
("mark.recent", "最近"),
("mark.recent.none", "还没有打开过文件"),
("mark.outline.none", "没有标题"),
("mark.search", "结果"),
("mark.search.file", "本文件"),
("mark.search.folder", "文件夹"),
("mark.search.more", "前 {count} 条结果"),
("mark.search.missed", "{name} 未能搜索"),
("mark.menu.table", "表格"),
("mark.table.insert", "插入表格"),
("mark.table.row_above", "在上方插入行"),
("mark.table.row_below", "在下方插入行"),
("mark.table.column_left", "在左侧插入列"),
("mark.table.column_right", "在右侧插入列"),
("mark.table.delete_row", "删除行"),
("mark.table.delete_column", "删除列"),
("mark.table.none", "请先把插入点放进表格"),
("mark.table.visual", "表格在可视模式下修改"),
("mark.attach.untitled", "请先保存文档:图片放在它旁边"),
("mark.attach.preview", "请切到编辑模式再添加图片"),
("mark.attach.images", "只能添加图片"),
("mark.attach.moved", "复制图片期间文件移动了;图片在 {folder},未写入链接"),
("mark.outside.changed", "{name} 在磁盘上被改了;未保存的内容保留"),
("mark.outside.deleted", "{name} 在磁盘上被删除了;内容保留"),
("mark.outside.unread", "无法跟随 {name} 在磁盘上的改动:{why}"),
("mark.outside.compare", "比较…"),
("mark.outside.save_as", "另存为…"),
("mark.outside.refresh", "再查一次"),
("mark.outside.check", "检查磁盘上的文件"),
("mark.compare.title", "{name}:你的与磁盘上的"),
("mark.compare.mine", "你的"),
("mark.compare.base", "上次载入或保存时"),
("mark.compare.disk", "磁盘上的"),
("mark.compare.use_disk", "用磁盘上的"),
("mark.compare.keep_mine", "保留你的"),
("mark.compare.use_merged", "用合并的"),
("mark.restore.missing", "上次的文件没能打开:{file}"),
("mark.compare.again", "文件又在磁盘上改了;请再比较一次"),
("mark.restore.recovered", "{name}:恢复了 {at} 时未保存的内容;撤销历史未恢复"),
("mark.export", "导出"),
("mark.export.page", "HTML"),
("mark.export.linked", "HTML 与资源文件夹"),
("mark.export.pdf", "PDF"),
("mark.export.text", "纯文本"),
("mark.export.go", "导出…"),
("mark.export.done", "已导出 {name}"),
("mark.export.missing", "已导出,缺这些图片:{list}。"),
("mark.export.diagrams", "没能画出的图表保留为代码,在第 {list} 行。"),
("mark.export.formula", "第 {line} 行的公式没能排出:{error}。"),
("mark.export.failed", "导出失败:{error}"),
("mark.look.focus", "专注模式"),
("mark.look.typewriter", "打字机滚动"),
("mark.look.narrow", "窄正文"),
("mark.look.medium", "中等正文"),
("mark.look.wide", "宽正文"),
("mark.look.sans", "无衬线字体"),
("mark.look.serif", "衬线字体"),
("mark.look.mono", "等宽字体"),
("mark.look.larger", "放大文字"),
("mark.look.smaller", "缩小文字"),
("mark.look.actual", "实际大小"),
("mark.git.unchanged", "Git:未改动"),
("mark.git.changed", "Git:有改动"),
("mark.git.untracked", "Git:未跟踪"),
("mark.git.ignored", "Git:已忽略"),
("mark.git.conflicted", "Git:有冲突"),
("mark.git.unknown", "Git:未知"),
("mark.git.open", "在 ElyTree 中查看此文件的历史"),
("mark.git.missing", "未安装 ElyTree。请从 Ely Office 发行包安装 ElyTree 后查看此文件的历史。"),
("mark.git.failed", "ElyTree 没能打开:{error}"),
("mark.large", "此文件 {size} MB,以源码显示。可视与预览已关闭;保存时保留全部内容。"),
("mark.large.modes", "大文件只用源码模式"),
("mark.mode.visual", "可视"),
("mark.mode.source", "源码"),
("mark.mode.preview", "预览"),
("mark.rename", "重命名"),
("mark.rename.name", "名称"),
("mark.rename.apply", "重命名"),
("mark.rename.failed", "{name} 未能改名:{error}"),
("mark.rename.bad", "请输入不含斜杠的文件名"),
("mark.rename.format", "名称须以 .{formats} 结尾"),
("mark.rename.untitled", "请先保存文档再改名"),
("mark.rename.saving", "请等保存结束"),
];
+105
View File
@@ -0,0 +1,105 @@
//! ElyShow transitions, animations and media.
pub(super) const EN: &[(&str, &str)] = &[
("show.motion.transition", "Transition"),
("show.motion.effects", "Animations"),
("show.motion.duration", "Duration (s)"),
("show.motion.delay", "Delay (s)"),
("show.motion.start", "Start"),
("show.motion.auto", "Advance by itself"),
("show.motion.time", "After (s)"),
("show.motion.all", "Apply to All Slides"),
("show.motion.preview_transition", "Preview Transition"),
("show.motion.preview_effects", "Preview Animations"),
("show.motion.add", "Add Animation"),
("show.motion.up", "Earlier"),
("show.motion.down", "Later"),
("show.motion.remove", "Remove"),
("show.transition.none", "None"),
("show.transition.other", "Other (from the file)"),
("show.transition.fade-smoothly", "Fade"),
("show.transition.fade-through-black", "Fade Through Black"),
("show.transition.push-left", "Push Left"),
("show.transition.push-up", "Push Up"),
("show.transition.cover-left", "Cover Left"),
("show.transition.uncover-left", "Uncover Left"),
("show.transition.wipe-right", "Wipe Right"),
("show.transition.split-vertical-out", "Split Out"),
("show.transition.dissolve", "Dissolve"),
("show.effect.entrance-appear", "Appear"),
("show.effect.entrance-fade-in", "Fade In"),
("show.effect.entrance-fly-in", "Fly In"),
("show.effect.entrance-wipe", "Wipe In"),
("show.effect.entrance-zoom", "Zoom In"),
("show.effect.entrance-float", "Float In"),
("show.effect.emphasis-grow-and-shrink", "Grow and Shrink"),
("show.effect.emphasis-blink", "Blink"),
("show.effect.exit-disappear", "Disappear"),
("show.effect.exit-fade-out", "Fade Out"),
("show.effect.exit-fly-out", "Fly Out"),
("show.effect.exit-wipe", "Wipe Out"),
("show.effect.exit-zoom", "Zoom Out"),
("show.effect.exit-float", "Float Out"),
("show.effect.start.click", "On click"),
("show.effect.start.with", "With previous"),
("show.effect.start.after", "After previous"),
("show.media.group", "Media"),
("show.media.file", "File"),
("show.media.play", "Play"),
("show.media.pause", "Pause"),
("show.media.missing", "The media file is not there: {path}"),
("show.media.failed", "It cannot play: {reason}"),
("show.media.refused", "{name} cannot go in: {reason}"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("show.motion.transition", "切换"),
("show.motion.effects", "动画"),
("show.motion.duration", "时长(秒)"),
("show.motion.delay", "延迟(秒)"),
("show.motion.start", "开始"),
("show.motion.auto", "自动换片"),
("show.motion.time", "等待(秒)"),
("show.motion.all", "应用到全部幻灯片"),
("show.motion.preview_transition", "预览切换"),
("show.motion.preview_effects", "预览动画"),
("show.motion.add", "添加动画"),
("show.motion.up", "提前"),
("show.motion.down", "推后"),
("show.motion.remove", "删除"),
("show.transition.none", "无"),
("show.transition.other", "其他(文件自带)"),
("show.transition.fade-smoothly", "淡化"),
("show.transition.fade-through-black", "经黑色淡化"),
("show.transition.push-left", "向左推入"),
("show.transition.push-up", "向上推入"),
("show.transition.cover-left", "向左覆盖"),
("show.transition.uncover-left", "向左揭开"),
("show.transition.wipe-right", "向右擦除"),
("show.transition.split-vertical-out", "向外分割"),
("show.transition.dissolve", "溶解"),
("show.effect.entrance-appear", "出现"),
("show.effect.entrance-fade-in", "淡入"),
("show.effect.entrance-fly-in", "飞入"),
("show.effect.entrance-wipe", "擦入"),
("show.effect.entrance-zoom", "放大进入"),
("show.effect.entrance-float", "浮入"),
("show.effect.emphasis-grow-and-shrink", "放大缩小"),
("show.effect.emphasis-blink", "闪烁"),
("show.effect.exit-disappear", "消失"),
("show.effect.exit-fade-out", "淡出"),
("show.effect.exit-fly-out", "飞出"),
("show.effect.exit-wipe", "擦出"),
("show.effect.exit-zoom", "缩小退出"),
("show.effect.exit-float", "浮出"),
("show.effect.start.click", "单击时"),
("show.effect.start.with", "与上一个同时"),
("show.effect.start.after", "上一个之后"),
("show.media.group", "媒体"),
("show.media.file", "文件"),
("show.media.play", "播放"),
("show.media.pause", "暂停"),
("show.media.missing", "找不到媒体文件:{path}"),
("show.media.failed", "无法播放:{reason}"),
("show.media.refused", "无法插入 {name}:{reason}"),
];
+171
View File
@@ -0,0 +1,171 @@
//! Messages of the shared document shell.
pub(super) const EN: &[(&str, &str)] = &[
("office.cancel", "Cancel"),
("office.conflict.copy", "Save a Copy…"),
("office.conflict.detail", "Choose which version to keep."),
("office.conflict.keep", "Keep Mine"),
("office.conflict.reload", "Reload from Disk"),
("office.conflict.title", "{name} changed on disk"),
(
"office.error.conflict",
"{name} changed on disk; the save stopped",
),
(
"office.error.convert",
"A conversion writes a new file; pick another name",
),
("office.error.changed", "The document changed meanwhile; try again"),
("office.recovery.failed", "A recovery copy was not written: {error}"),
("office.recovery.off", "Recovery copies are off for this run: {error}"),
("office.recovery.lost", "{name} could not be recovered: {error}"),
("office.recovery.back", "{name}: unsaved content from {at} is back; the cursor and undo history are not"),
("office.recovery.index", "Recovery points cannot be listed, so a crash may lose them: {error}"),
("office.stopped", "The document engine stopped ({why}). Last recovery point: {at}."),
("office.stopped.never", "none yet"),
("office.stopped.reopen", "Reopen documents"),
("office.stopped.lost", "Not reopened: {names}. Their unsaved changes had no recovery point."),
("office.error.engine", "{error}"),
("office.error.format", "Save As writes .{formats}"),
("office.error.open", "{name} did not open: {error}"),
("office.error.panel", "The file panel failed: {error}"),
("office.error.save", "{name} did not save: {error}"),
("office.error.taken", "{name} is open in another tab"),
("office.loading", "Opening {name}…"),
("office.locked.empty", "Type the password."),
("office.locked.label", "Password"),
("office.locked.open", "Open"),
("office.locked.title", "{name} is locked"),
(
"office.locked.wrong",
"That password did not open it. Password",
),
(
"office.risk",
"{name} holds macros. Ely runs none; Save As writes a {formats} copy.",
),
("office.state.dirty", "Edited"),
("office.state.external", "Changed on disk"),
("office.state.failed", "Save failed"),
("office.state.risk", "Risk mode"),
("office.state.signed", "Signed"),
("office.state.repaired", "Repaired copy"),
(
"office.repaired",
"{name} is damaged. Ely opened a repaired copy; Save As writes it to a new file and the damaged original stays as it is.",
),
(
"office.signed",
"{name} is signed. An edit breaks its signatures, so Save As writes an unsigned copy; the signed file stays as it is.",
),
("office.state.saved", "Saved"),
("office.state.saving", "Saving…"),
(
"office.unsaved.detail",
"Closing without saving loses the changes.",
),
("office.unsaved.discard", "Don't Save"),
("office.unsaved.save", "Save"),
("office.unsaved.title", "Save changes to {name}?"),
("office.untitled", "Untitled {n}"),
("office.props.automatic", "Automatic"),
("office.props.mixed", "Mixed"),
("office.chart.insert_title", "Insert Chart"),
("office.chart.edit_title", "Edit Chart"),
("office.chart.kind", "Type"),
("office.chart.column", "Column"),
("office.chart.bar", "Bar"),
("office.chart.line", "Line"),
("office.chart.scatter", "Scatter"),
("office.chart.pie", "Pie"),
("office.chart.other", "Other"),
("office.chart.heading", "Title"),
("office.chart.legend", "Legend"),
("office.chart.x", "X axis title"),
("office.chart.y", "Y axis title"),
("office.chart.go", "Insert"),
("office.chart.apply", "Apply"),
("office.zoom.in", "Zoom In"),
("office.zoom.out", "Zoom Out"),
("office.zoom.actual", "Actual Size"),
("office.zoom.fit", "Fit Slide"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("office.cancel", "取消"),
("office.conflict.copy", "另存副本…"),
("office.conflict.detail", "选择要保留的版本。"),
("office.conflict.keep", "保留我的"),
("office.conflict.reload", "从磁盘重新载入"),
("office.conflict.title", "{name} 已在磁盘上改动"),
("office.error.conflict", "{name} 已在磁盘上改动,保存已停止"),
("office.error.convert", "格式转换要写成新文件,请换个名字"),
("office.error.changed", "文档在此间已改动,请重试"),
("office.recovery.failed", "恢复副本未能写入:{error}"),
("office.recovery.off", "本次运行不写恢复副本:{error}"),
("office.recovery.lost", "{name} 未能恢复:{error}"),
("office.recovery.back", "{name}:恢复了 {at} 时未保存的内容;光标与撤销历史未恢复"),
("office.recovery.index", "恢复点清单写不进去,崩溃后可能找不回:{error}"),
("office.stopped", "文档引擎已停止({why})。最近的恢复点:{at}。"),
("office.stopped.never", "尚无"),
("office.stopped.reopen", "重新打开文档"),
("office.stopped.lost", "未能重新打开:{names}。其未保存的改动没有恢复点。"),
("office.error.engine", "{error}"),
("office.error.format", "另存为只写 .{formats}"),
("office.error.open", "{name} 未能打开:{error}"),
("office.error.panel", "文件面板出错:{error}"),
("office.error.save", "{name} 未能保存:{error}"),
("office.error.taken", "{name} 已在另一个标签页打开"),
("office.loading", "正在打开 {name}…"),
("office.locked.empty", "请输入密码。"),
("office.locked.label", "密码"),
("office.locked.open", "打开"),
("office.locked.title", "{name} 已加密"),
("office.locked.wrong", "密码不对,请重输"),
(
"office.risk",
"{name} 含宏。Ely 不运行宏;另存为会写出不含宏的 {formats} 副本。",
),
("office.state.dirty", "已编辑"),
("office.state.external", "磁盘上已改动"),
("office.state.failed", "保存失败"),
("office.state.risk", "风险模式"),
("office.state.signed", "已签名"),
("office.state.repaired", "修复的副本"),
(
"office.repaired",
"{name} 已损坏。Ely 打开的是修复后的副本;“另存为”把它写成新文件,损坏的原件保持原样。",
),
(
"office.signed",
"{name} 已签名。修改会使签名失效,因此“另存为”写出未签名的副本;已签名的文件保持原样。",
),
("office.state.saved", "已保存"),
("office.state.saving", "正在保存…"),
("office.unsaved.detail", "不保存就关闭,修改会丢失。"),
("office.unsaved.discard", "不保存"),
("office.unsaved.save", "保存"),
("office.unsaved.title", "保存对 {name} 的修改?"),
("office.untitled", "未命名 {n}"),
("office.props.automatic", "自动"),
("office.props.mixed", "混合"),
("office.chart.insert_title", "插入图表"),
("office.chart.edit_title", "编辑图表"),
("office.chart.kind", "类型"),
("office.chart.column", "柱形图"),
("office.chart.bar", "条形图"),
("office.chart.line", "折线图"),
("office.chart.scatter", "散点图"),
("office.chart.pie", "饼图"),
("office.chart.other", "其他"),
("office.chart.heading", "标题"),
("office.chart.legend", "图例"),
("office.chart.x", "X 轴标题"),
("office.chart.y", "Y 轴标题"),
("office.chart.go", "插入"),
("office.chart.apply", "应用"),
("office.zoom.in", "放大"),
("office.zoom.out", "缩小"),
("office.zoom.actual", "实际大小"),
("office.zoom.fit", "适合幻灯片"),
];
+171
View File
@@ -0,0 +1,171 @@
//! ElyTable's text files, export, page and protection messages.
pub(super) const EN: &[(&str, &str)] = &[
("menu.page", "Page"),
("table.page.setup", "Page Setup…"),
("table.page.row_break", "Insert Row Break"),
("table.page.column_break", "Insert Column Break"),
("table.page.remove_row_break", "Remove Row Break"),
("table.page.remove_column_break", "Remove Column Break"),
("table.page.no_break", "No manual break here"),
("table.page.title", "Page Setup"),
("table.page.shared", "Applies to {sheets}"),
("table.page.area", "Print area"),
("table.page.area_hint", "Used cells"),
("table.page.rows", "Rows to repeat"),
("table.page.rows_hint", "1:1"),
("table.page.columns", "Columns to repeat"),
("table.page.columns_hint", "A:A"),
("table.page.orientation", "Orientation"),
("table.page.portrait", "Portrait"),
("table.page.landscape", "Landscape"),
("table.page.scale", "Scale"),
("table.page.percent", "Percent"),
("table.page.fit", "Pages wide × tall"),
("table.page.pages", "Pages in all"),
("table.page.apply", "Apply"),
(
"table.page.bad_reference",
"Write ranges like A1:D20, rows like 1:2, columns like A:B",
),
("table.page.bad_fit", "Fit needs a width or a height"),
("table.export.title", "Export"),
("table.export.go", "Export…"),
(
"table.export.loss",
"Only “{sheet}” goes out, as its shown values. Formulas, formatting, other sheets and charts stay behind.",
),
("table.export.open", "{name} is open; close it first"),
("table.export.done", "Exported {name}"),
("table.export.failed", "Export failed: {error}"),
("table.import.title", "Open {name}"),
("table.import.go", "Open"),
("table.import.encoding", "Encoding"),
("table.import.separator", "Separator"),
("table.import.quote", "Quote"),
("table.import.no_quote", "None"),
("table.import.comma", "Comma"),
("table.import.semicolon", "Semicolon"),
("table.import.tab", "Tab"),
("table.import.pipe", "Bar"),
("table.import.space", "Space"),
("table.import.kind_of", "Column {n}"),
("table.import.standard", "Standard"),
("table.import.text", "Text"),
("table.import.mdy", "Date M/D/Y"),
("table.import.dmy", "Date D/M/Y"),
("table.import.ymd", "Date Y/M/D"),
("table.import.skip", "Skip"),
(
"table.import.broken",
"This encoding cannot read the file; pick another",
),
("table.protect.menu", "Sheet Protection…"),
("table.protect.lock", "Lock Cells"),
("table.protect.unlock", "Unlock Cells"),
("table.protect.on_title", "Protect Sheet"),
("table.protect.on", "Protect"),
("table.protect.off_title", "Unprotect Sheet"),
("table.protect.off", "Unprotect"),
("table.protect.password", "Password"),
("table.protect.confirm", "Confirm"),
("table.protect.mismatch", "The passwords differ"),
(
"table.protect.wrong",
"Wrong password; the sheet stays protected",
),
("table.protect.selectLocked", "Select locked cells"),
("table.protect.selectUnlocked", "Select unlocked cells"),
("table.protect.insertColumns", "Insert columns"),
("table.protect.insertRows", "Insert rows"),
("table.protect.deleteColumns", "Delete columns"),
("table.protect.deleteRows", "Delete rows"),
("table.protect.autofilter", "Use filters"),
("table.protect.pivot", "Use pivot tables"),
(
"table.links.stale",
"Linked values are as last saved; not refreshed: {sources}",
),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("menu.page", "页面"),
("table.page.setup", "页面设置…"),
("table.page.row_break", "插入行分页符"),
("table.page.column_break", "插入列分页符"),
("table.page.remove_row_break", "删除行分页符"),
("table.page.remove_column_break", "删除列分页符"),
("table.page.no_break", "此处没有手动分页符"),
("table.page.title", "页面设置"),
("table.page.shared", "作用于 {sheets}"),
("table.page.area", "打印区域"),
("table.page.area_hint", "已用单元格"),
("table.page.rows", "重复的行"),
("table.page.rows_hint", "1:1"),
("table.page.columns", "重复的列"),
("table.page.columns_hint", "A:A"),
("table.page.orientation", "方向"),
("table.page.portrait", "纵向"),
("table.page.landscape", "横向"),
("table.page.scale", "缩放"),
("table.page.percent", "百分比"),
("table.page.fit", "宽 × 高页数"),
("table.page.pages", "总页数"),
("table.page.apply", "应用"),
(
"table.page.bad_reference",
"区域写作 A1:D20,行写作 1:2,列写作 A:B",
),
("table.page.bad_fit", "缩放到页需要宽或高"),
("table.export.title", "导出"),
("table.export.go", "导出…"),
(
"table.export.loss",
"只导出“{sheet}”,按显示的值写出。公式、格式、其他工作表和图表都不保留。",
),
("table.export.open", "{name} 正在打开,请先关闭"),
("table.export.done", "已导出 {name}"),
("table.export.failed", "导出失败:{error}"),
("table.import.title", "打开 {name}"),
("table.import.go", "打开"),
("table.import.encoding", "编码"),
("table.import.separator", "分隔符"),
("table.import.quote", "引号"),
("table.import.no_quote", "无"),
("table.import.comma", "逗号"),
("table.import.semicolon", "分号"),
("table.import.tab", "制表符"),
("table.import.pipe", "竖线"),
("table.import.space", "空格"),
("table.import.kind_of", "第 {n} 列"),
("table.import.standard", "常规"),
("table.import.text", "文本"),
("table.import.mdy", "日期 月/日/年"),
("table.import.dmy", "日期 日/月/年"),
("table.import.ymd", "日期 年/月/日"),
("table.import.skip", "跳过"),
("table.import.broken", "此编码读不了这个文件,请换一种"),
("table.protect.menu", "工作表保护…"),
("table.protect.lock", "锁定单元格"),
("table.protect.unlock", "解锁单元格"),
("table.protect.on_title", "保护工作表"),
("table.protect.on", "保护"),
("table.protect.off_title", "取消保护工作表"),
("table.protect.off", "取消保护"),
("table.protect.password", "密码"),
("table.protect.confirm", "确认"),
("table.protect.mismatch", "两次密码不同"),
("table.protect.wrong", "密码错误,工作表仍受保护"),
("table.protect.selectLocked", "选择锁定的单元格"),
("table.protect.selectUnlocked", "选择未锁定的单元格"),
("table.protect.insertColumns", "插入列"),
("table.protect.insertRows", "插入行"),
("table.protect.deleteColumns", "删除列"),
("table.protect.deleteRows", "删除行"),
("table.protect.autofilter", "使用筛选"),
("table.protect.pivot", "使用数据透视表"),
(
"table.links.stale",
"链接的值为上次保存时的值,未刷新:{sources}",
),
];
+413
View File
@@ -0,0 +1,413 @@
//! ElyPDF's reader.
pub(super) const EN: &[(&str, &str)] = &[
("menu.edit", "Edit"),
("pdf.failed", "{what} failed: {error}"),
("pdf.status.page", "Page {n} of {count}"),
("pdf.copy", "Copy"),
("pdf.find", "Find…"),
("pdf.view.continuous", "Continuous"),
("pdf.view.single", "Single Page"),
("pdf.view.two", "Two Pages"),
("pdf.view.zoom_in", "Zoom In"),
("pdf.view.zoom_out", "Zoom Out"),
("pdf.view.fit_width", "Fit Width"),
("pdf.view.fit_page", "Fit Page"),
("pdf.view.actual", "Actual Size"),
("pdf.view.rotate_left", "Rotate Left"),
("pdf.view.rotate_right", "Rotate Right"),
("menu.pages", "Pages"),
("pdf.undo", "Undo"),
("pdf.redo", "Redo"),
("pdf.pages.blank", "Insert Blank Page"),
("pdf.pages.file", "Insert Pages from File…"),
("pdf.pages.delete", "Delete Pages"),
("pdf.pages.rotate_left", "Rotate Pages Left"),
("pdf.pages.rotate_right", "Rotate Pages Right"),
("pdf.pages.crop", "Crop Pages…"),
("pdf.pages.up", "Move Pages Up"),
("pdf.pages.down", "Move Pages Down"),
("pdf.pages.first", "Move Pages to Start"),
("pdf.pages.last", "Move Pages to End"),
("pdf.pages.extract", "Extract Pages…"),
("pdf.pages.split", "Split into Files…"),
("pdf.pages.keep_one", "A PDF keeps at least one page"),
("pdf.side.pages", "Pages"),
("pdf.side.marks", "Bookmarks"),
("pdf.page.label", "Page {n}"),
("pdf.crop.title", "Crop {count} pages"),
("pdf.crop.top", "Top"),
("pdf.crop.right", "Right"),
("pdf.crop.bottom", "Bottom"),
("pdf.crop.left", "Left"),
("pdf.crop.go", "Crop"),
("pdf.split.title", "Split into Files"),
("pdf.split.every", "Pages per file"),
("pdf.split.go", "Choose Folder…"),
("pdf.split.done", "{count} files written"),
("pdf.extract.taken", "{name} is open; choose another name"),
("pdf.extract.done", "Pages written to {name}"),
("pdf.font.original", "The document's font"),
("pdf.font.helvetica", "Helvetica"),
("pdf.font.times", "Times"),
("pdf.font.courier", "Courier"),
("pdf.font.sc", "Chinese (Simplified)"),
("pdf.font.tc", "Chinese (Traditional)"),
("pdf.font.jp", "Japanese"),
("pdf.font.kr", "Korean"),
("pdf.text.font", "Font"),
("pdf.text.size", "Size"),
("pdf.text.color", "Color"),
("pdf.text.width", "Box width"),
("pdf.text.apply", "Apply"),
("pdf.text.erase", "Delete Text"),
(
"pdf.text.missing",
"This font cannot draw “{char}”. Choose another font.",
),
("pdf.object.replace", "Replace Image…"),
("pdf.object.delete", "Delete"),
(
"pdf.object.changed",
"The document changed meanwhile; nothing was done.",
),
("pdf.tool.edit", "Edit Text and Images"),
("pdf.tool.text", "Add Text"),
("pdf.tool.image", "Add Image…"),
("pdf.preview.title", "Check the Change"),
(
"pdf.preview.detail",
"The new text takes a different space on the page.",
),
("menu.annotate", "Annotate"),
("menu.form", "Form"),
("pdf.tool.highlight", "Highlight"),
("pdf.tool.underline", "Underline"),
("pdf.tool.strikeout", "Strikethrough"),
("pdf.tool.note", "Note"),
("pdf.tool.stamp", "Stamp"),
("pdf.tool.square", "Rectangle"),
("pdf.tool.circle", "Ellipse"),
("pdf.tool.line", "Line"),
("pdf.tool.ink", "Freehand"),
("pdf.tool.field_text", "Text Field"),
("pdf.tool.field_check", "Checkbox"),
("pdf.tool.field_radio", "Radio Button"),
("pdf.tool.field_choice", "Choice"),
("pdf.note.add", "Add"),
("pdf.note.kind", "Kind"),
("pdf.note.author", "Author"),
("pdf.note.contents", "Contents"),
("pdf.field.name", "Name"),
("pdf.field.options", "Options"),
("pdf.side.notes", "Annotations"),
("pdf.notes.none", "No annotations"),
("pdf.annot.Highlight", "Highlight"),
("pdf.annot.Underline", "Underline"),
("pdf.annot.StrikeOut", "Strikethrough"),
("pdf.annot.Squiggly", "Squiggly"),
("pdf.annot.Text", "Note"),
("pdf.annot.FreeText", "Text Box"),
("pdf.annot.Square", "Rectangle"),
("pdf.annot.Circle", "Ellipse"),
("pdf.annot.Line", "Line"),
("pdf.annot.Polygon", "Polygon"),
("pdf.annot.PolyLine", "Polyline"),
("pdf.annot.Ink", "Freehand"),
("pdf.annot.Stamp", "Stamp"),
("pdf.annot.Caret", "Insert Mark"),
("pdf.annot.FileAttachment", "Attachment"),
("pdf.stamp.Approved", "Approved"),
("pdf.stamp.AsIs", "As Is"),
("pdf.stamp.Confidential", "Confidential"),
("pdf.stamp.Departmental", "Departmental"),
("pdf.stamp.Draft", "Draft"),
("pdf.stamp.Experimental", "Experimental"),
("pdf.stamp.Expired", "Expired"),
("pdf.stamp.Final", "Final"),
("pdf.stamp.ForComment", "For Comment"),
("pdf.stamp.ForPublicRelease", "For Public Release"),
("pdf.stamp.NotApproved", "Not Approved"),
("pdf.stamp.NotForPublicRelease", "Not for Public Release"),
("pdf.stamp.Sold", "Sold"),
("pdf.stamp.TopSecret", "Top Secret"),
("menu.document", "Document"),
("pdf.props.open", "Document Properties…"),
("pdf.props.heading", "Document Properties"),
("pdf.ocr.title", "Recognize Text"),
("pdf.props.title", "Title"),
("pdf.props.author", "Author"),
("pdf.props.subject", "Subject"),
("pdf.props.keywords", "Keywords"),
("pdf.props.creator", "Created with"),
("pdf.props.producer", "Produced by"),
("pdf.props.created", "Created"),
("pdf.props.modified", "Modified"),
("pdf.props.none", "None"),
("pdf.ocr.open", "Recognize Text…"),
("pdf.ocr.go", "Recognize"),
("pdf.ocr.pages", "Pages"),
("pdf.ocr.all", "All"),
("pdf.ocr.current", "Current"),
("pdf.ocr.range", "Range"),
("pdf.ocr.from", "From page"),
("pdf.ocr.to", "To page"),
("pdf.ocr.langs", "Languages"),
("pdf.ocr.eng", "English"),
("pdf.ocr.sim", "Simplified Chinese"),
("pdf.ocr.tra", "Traditional Chinese"),
("pdf.ocr.policy", "Pages with text"),
("pdf.ocr.skip", "Skip them"),
(
"pdf.ocr.replace",
"Read again, replacing Ely's earlier text",
),
("pdf.ocr.reading", "Reading page {n} of {count}"),
(
"pdf.ocr.done",
"Recognized {read} pages; skipped {skipped}.",
),
("pdf.mark.add", "Add Bookmark"),
("pdf.mark.rename", "Rename Bookmark"),
("pdf.mark.delete", "Delete Bookmark"),
("pdf.mark.title", "Title"),
("pdf.mark.empty", "A bookmark needs a title."),
("pdf.marks.none", "No bookmarks"),
("pdf.tool.link", "Add Link"),
("pdf.link.target", "Goes to"),
(
"pdf.link.bad",
"Type a page number, or a web or mail address.",
),
("pdf.link.refused", "Ely opens only web and mail links."),
("pdf.side.files", "Attachments"),
("pdf.files.none", "No attachments"),
("pdf.file.attach", "Attach File…"),
("pdf.file.save", "Save As…"),
("pdf.file.remove", "Remove"),
("pdf.redact.text", "Mark Text to Remove"),
("pdf.redact.area", "Mark Area to Remove"),
("pdf.redact.clear", "Clear Removal Marks"),
("pdf.redact.apply", "Remove Marked Content…"),
("pdf.redact.title", "Remove Marked Content from {name}"),
("pdf.redact.none", "Mark text or areas to remove first."),
(
"pdf.redact.count",
"{count} marked regions are removed for good.",
),
("pdf.redact.tail", "redacted"),
(
"pdf.redact.done",
"Saved {name}: {count} regions removed and checked.",
),
("pdf.clean.open", "Clean Document…"),
("pdf.clean.title", "Clean {name}"),
("pdf.clean.meta", "Properties and metadata ({count})"),
("pdf.clean.annots", "Annotations ({count})"),
("pdf.clean.files", "Attachments ({count})"),
(
"pdf.clean.hidden",
"Hidden text, scripts and hidden annotations ({count})",
),
("pdf.clean.tail", "cleaned"),
("pdf.clean.done", "Saved {name}, cleaned and checked."),
("pdf.removal.go", "Save As…"),
(
"pdf.removal.warning",
"This writes a new file; this document stays as it is. The original, old backups, sync history and copies already sent may still hold what is removed. Ely's working copies are deleted the ordinary way, not wiped from the disk.",
),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("menu.edit", "编辑"),
("pdf.failed", "{what}失败:{error}"),
("pdf.status.page", "第 {n} 页,共 {count} 页"),
("pdf.copy", "复制"),
("pdf.find", "查找…"),
("pdf.view.continuous", "连续"),
("pdf.view.single", "单页"),
("pdf.view.two", "双页"),
("pdf.view.zoom_in", "放大"),
("pdf.view.zoom_out", "缩小"),
("pdf.view.fit_width", "适合宽度"),
("pdf.view.fit_page", "适合页面"),
("pdf.view.actual", "实际大小"),
("pdf.view.rotate_left", "向左旋转"),
("pdf.view.rotate_right", "向右旋转"),
("menu.pages", "页面"),
("pdf.undo", "撤销"),
("pdf.redo", "重做"),
("pdf.pages.blank", "插入空白页"),
("pdf.pages.file", "从文件插入页面…"),
("pdf.pages.delete", "删除页面"),
("pdf.pages.rotate_left", "页面向左旋转"),
("pdf.pages.rotate_right", "页面向右旋转"),
("pdf.pages.crop", "裁剪页面…"),
("pdf.pages.up", "页面上移"),
("pdf.pages.down", "页面下移"),
("pdf.pages.first", "页面移到开头"),
("pdf.pages.last", "页面移到末尾"),
("pdf.pages.extract", "提取页面…"),
("pdf.pages.split", "拆分为多个文件…"),
("pdf.pages.keep_one", "PDF 至少保留一页"),
("pdf.side.pages", "页面"),
("pdf.side.marks", "书签"),
("pdf.page.label", "第 {n} 页"),
("pdf.crop.title", "裁剪 {count} 页"),
("pdf.crop.top", "上"),
("pdf.crop.right", "右"),
("pdf.crop.bottom", "下"),
("pdf.crop.left", "左"),
("pdf.crop.go", "裁剪"),
("pdf.split.title", "拆分为多个文件"),
("pdf.split.every", "每个文件的页数"),
("pdf.split.go", "选择文件夹…"),
("pdf.split.done", "已写入 {count} 个文件"),
("pdf.extract.taken", "{name} 已打开,请换一个名称"),
("pdf.extract.done", "页面已写入 {name}"),
("pdf.font.original", "文档自带字体"),
("pdf.font.helvetica", "Helvetica"),
("pdf.font.times", "Times"),
("pdf.font.courier", "Courier"),
("pdf.font.sc", "简体中文"),
("pdf.font.tc", "繁体中文"),
("pdf.font.jp", "日文"),
("pdf.font.kr", "韩文"),
("pdf.text.font", "字体"),
("pdf.text.size", "字号"),
("pdf.text.color", "颜色"),
("pdf.text.width", "文本框宽度"),
("pdf.text.apply", "应用"),
("pdf.text.erase", "删除文字"),
("pdf.text.missing", "这种字体画不出“{char}”。请换一种字体。"),
("pdf.object.replace", "替换图片…"),
("pdf.object.delete", "删除"),
("pdf.object.changed", "文档已改动,未执行。"),
("pdf.tool.edit", "编辑文字和图片"),
("pdf.tool.text", "添加文字"),
("pdf.tool.image", "添加图片…"),
("pdf.preview.title", "确认改动"),
("pdf.preview.detail", "新文字在页面上占的位置不同。"),
("menu.annotate", "批注"),
("menu.form", "表单"),
("pdf.tool.highlight", "高亮"),
("pdf.tool.underline", "下划线"),
("pdf.tool.strikeout", "删除线"),
("pdf.tool.note", "便签"),
("pdf.tool.stamp", "印章"),
("pdf.tool.square", "矩形"),
("pdf.tool.circle", "椭圆"),
("pdf.tool.line", "直线"),
("pdf.tool.ink", "手绘"),
("pdf.tool.field_text", "文本字段"),
("pdf.tool.field_check", "复选框"),
("pdf.tool.field_radio", "单选按钮"),
("pdf.tool.field_choice", "选择框"),
("pdf.note.add", "添加"),
("pdf.note.kind", "类型"),
("pdf.note.author", "作者"),
("pdf.note.contents", "内容"),
("pdf.field.name", "名称"),
("pdf.field.options", "选项"),
("pdf.side.notes", "批注"),
("pdf.notes.none", "没有批注"),
("pdf.annot.Highlight", "高亮"),
("pdf.annot.Underline", "下划线"),
("pdf.annot.StrikeOut", "删除线"),
("pdf.annot.Squiggly", "波浪线"),
("pdf.annot.Text", "便签"),
("pdf.annot.FreeText", "文本框"),
("pdf.annot.Square", "矩形"),
("pdf.annot.Circle", "椭圆"),
("pdf.annot.Line", "直线"),
("pdf.annot.Polygon", "多边形"),
("pdf.annot.PolyLine", "折线"),
("pdf.annot.Ink", "手绘"),
("pdf.annot.Stamp", "印章"),
("pdf.annot.Caret", "插入符"),
("pdf.annot.FileAttachment", "附件"),
("pdf.stamp.Approved", "已批准"),
("pdf.stamp.AsIs", "按原样"),
("pdf.stamp.Confidential", "机密"),
("pdf.stamp.Departmental", "部门"),
("pdf.stamp.Draft", "草稿"),
("pdf.stamp.Experimental", "试验"),
("pdf.stamp.Expired", "已过期"),
("pdf.stamp.Final", "最终"),
("pdf.stamp.ForComment", "征求意见"),
("pdf.stamp.ForPublicRelease", "可公开发布"),
("pdf.stamp.NotApproved", "未批准"),
("pdf.stamp.NotForPublicRelease", "不可公开发布"),
("pdf.stamp.Sold", "已售"),
("pdf.stamp.TopSecret", "绝密"),
("menu.document", "文档"),
("pdf.props.open", "文档属性…"),
("pdf.props.heading", "文档属性"),
("pdf.ocr.title", "识别文字"),
("pdf.props.title", "标题"),
("pdf.props.author", "作者"),
("pdf.props.subject", "主题"),
("pdf.props.keywords", "关键词"),
("pdf.props.creator", "创建程序"),
("pdf.props.producer", "生成程序"),
("pdf.props.created", "创建时间"),
("pdf.props.modified", "修改时间"),
("pdf.props.none", "无"),
("pdf.ocr.open", "识别文字…"),
("pdf.ocr.go", "识别"),
("pdf.ocr.pages", "页面"),
("pdf.ocr.all", "全部"),
("pdf.ocr.current", "当前页"),
("pdf.ocr.range", "范围"),
("pdf.ocr.from", "起始页"),
("pdf.ocr.to", "结束页"),
("pdf.ocr.langs", "语言"),
("pdf.ocr.eng", "英文"),
("pdf.ocr.sim", "简体中文"),
("pdf.ocr.tra", "繁体中文"),
("pdf.ocr.policy", "已有文字的页"),
("pdf.ocr.skip", "跳过"),
("pdf.ocr.replace", "重新识别,替换 Ely 此前的文字层"),
("pdf.ocr.reading", "正在识别第 {n} 页,共 {count} 页"),
("pdf.ocr.done", "识别了 {read} 页,跳过 {skipped} 页。"),
("pdf.mark.add", "添加书签"),
("pdf.mark.rename", "重命名书签"),
("pdf.mark.delete", "删除书签"),
("pdf.mark.title", "标题"),
("pdf.mark.empty", "书签需要标题。"),
("pdf.marks.none", "没有书签"),
("pdf.tool.link", "添加链接"),
("pdf.link.target", "目标"),
("pdf.link.bad", "请输入页码,或网址、邮件地址。"),
("pdf.link.refused", "Ely 只打开网页与邮件链接。"),
("pdf.side.files", "附件"),
("pdf.files.none", "没有附件"),
("pdf.file.attach", "添加附件…"),
("pdf.file.save", "另存为…"),
("pdf.file.remove", "移除"),
("pdf.redact.text", "标记文字以删除"),
("pdf.redact.area", "标记区域以删除"),
("pdf.redact.clear", "清除删除标记"),
("pdf.redact.apply", "删除标记的内容…"),
("pdf.redact.title", "删除 {name} 中标记的内容"),
("pdf.redact.none", "请先标记要删除的文字或区域。"),
("pdf.redact.count", "将彻底删除 {count} 处标记。"),
("pdf.redact.tail", "已删除"),
(
"pdf.redact.done",
"已保存 {name}:删除并核对了 {count} 处。",
),
("pdf.clean.open", "清理文档…"),
("pdf.clean.title", "清理 {name}"),
("pdf.clean.meta", "属性与元数据({count})"),
("pdf.clean.annots", "批注({count})"),
("pdf.clean.files", "附件({count})"),
("pdf.clean.hidden", "隐藏文字、脚本与隐藏的批注({count})"),
("pdf.clean.tail", "已清理"),
("pdf.clean.done", "已保存 {name},清理并核对完毕。"),
("pdf.removal.go", "另存为…"),
(
"pdf.removal.warning",
"将写成新文件,当前文档不变。原件、旧备份、同步历史与已发出的副本仍可能含有被删内容。Ely 的工作副本按普通方式删除,不是磁盘级擦除。",
),
];
+113
View File
@@ -0,0 +1,113 @@
//! ElyShow's slide show and presenter view.
pub(super) const EN: &[(&str, &str)] = &[
("menu.play", "Slide Show"),
("show.play.from_start", "From Beginning"),
("show.play.from_here", "From Current Slide"),
("show.play.title", "Slide Show"),
("show.play.audience", "Audience screen"),
("show.play.presenter", "Presenter view"),
("show.play.screen", "Screen {number} ({width} × {height})"),
("show.play.start", "Start"),
("show.play.none", "Every slide is hidden"),
("show.play.back", "Previous"),
("show.play.next", "Next"),
("show.play.place", "Slide {number} of {count}"),
("show.play.going", "Go to slide {number}"),
("show.play.ended", "End of show"),
("show.play.black", "Black Screen"),
("show.play.laser", "Laser Pointer"),
("show.play.end", "End Show"),
("show.output.export_title", "Export"),
("show.output.print_title", "Print"),
("show.output.export", "Export…"),
("show.output.print", "Print…"),
("show.output.what", "What"),
("show.output.which", "Slides"),
("show.output.pdf", "PDF"),
("show.output.slides", "Slides"),
("show.output.pictures", "Pictures"),
("show.output.handouts", "Handouts"),
("show.output.all", "All"),
("show.output.current", "Current"),
("show.output.range", "From–To"),
("show.output.from", "From slide"),
("show.output.to", "To slide"),
("show.output.per", "Slides a page"),
("show.output.open", "{name} is open; close it first"),
(
"show.output.taken",
"{name} is already there; nothing was written",
),
("show.output.done", "Exported {name}"),
(
"show.output.pictures_done",
"Wrote {n} pictures to {folder}",
),
("show.output.failed", "Output failed: {error}"),
("show.check.menu", "Check Presentation…"),
("show.check.title", "Presentation Check"),
("show.check.clear", "Nothing to fix."),
("show.check.font", "Missing font"),
("show.check.media", "Missing media"),
("show.check.link", "Linked file"),
("show.check.link_missing", "Missing linked file"),
("show.check.object", "Cannot be shown"),
("show.check.slide", "Slide {n}"),
("show.check.master", "Master {name}"),
("show.check.nowhere", "Not in slide text"),
("show.check.unnamed", "Unnamed object"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("menu.play", "放映"),
("show.play.from_start", "从头开始"),
("show.play.from_here", "从当前幻灯片开始"),
("show.play.title", "放映"),
("show.play.audience", "观众屏"),
("show.play.presenter", "演讲者视图"),
("show.play.screen", "屏幕 {number}({width} × {height})"),
("show.play.start", "开始放映"),
("show.play.none", "所有幻灯片都已隐藏"),
("show.play.back", "上一步"),
("show.play.next", "下一步"),
("show.play.place", "第 {number} 张,共 {count} 张"),
("show.play.going", "转到第 {number} 张"),
("show.play.ended", "放映结束"),
("show.play.black", "黑屏"),
("show.play.laser", "激光笔"),
("show.play.end", "结束放映"),
("show.output.export_title", "导出"),
("show.output.print_title", "打印"),
("show.output.export", "导出…"),
("show.output.print", "打印…"),
("show.output.what", "内容"),
("show.output.which", "幻灯片"),
("show.output.pdf", "PDF"),
("show.output.slides", "幻灯片"),
("show.output.pictures", "图片"),
("show.output.handouts", "讲义"),
("show.output.all", "全部"),
("show.output.current", "当前"),
("show.output.range", "从–到"),
("show.output.from", "起始页"),
("show.output.to", "结束页"),
("show.output.per", "每页张数"),
("show.output.open", "{name} 正在打开,请先关闭"),
("show.output.taken", "{name} 已存在,未写入任何文件"),
("show.output.done", "已导出 {name}"),
("show.output.pictures_done", "已写入 {n} 张图片到 {folder}"),
("show.output.failed", "输出失败:{error}"),
("show.check.menu", "检查演示…"),
("show.check.title", "演示检查"),
("show.check.clear", "没有需要处理的问题。"),
("show.check.font", "缺失字体"),
("show.check.media", "缺失媒体"),
("show.check.link", "外部链接"),
("show.check.link_missing", "缺失的链接文件"),
("show.check.object", "无法显示"),
("show.check.slide", "第 {n} 张"),
("show.check.master", "母版 {name}"),
("show.check.nowhere", "不在幻灯片文字中"),
("show.check.unnamed", "未命名对象"),
];
+109
View File
@@ -0,0 +1,109 @@
//! ElyDoc's reference and review messages.
pub(super) const EN: &[(&str, &str)] = &[
("menu.review", "Review"),
("doc.insert.contents", "Table of Contents"),
("doc.insert.update_contents", "Update Table of Contents"),
("doc.contents.title", "Contents"),
("doc.insert.footnote", "Footnote"),
("doc.insert.endnote", "Endnote"),
("doc.insert.bookmark", "Bookmark…"),
("doc.insert.link", "Hyperlink…"),
("doc.insert.reference", "Cross-Reference…"),
("doc.insert.caption", "Caption…"),
("doc.insert.formula", "Formula…"),
("doc.insert.comment", "Comment…"),
("doc.bookmark.title", "Insert Bookmark"),
("doc.bookmark.name", "Name"),
("doc.bookmark.empty", "Give the bookmark a name."),
("doc.bookmark.taken", "A bookmark already has this name."),
("doc.link.title", "Insert Hyperlink"),
("doc.link.text", "Text"),
("doc.link.url", "Address"),
("doc.reference.title", "Insert Cross-Reference"),
("doc.reference.target", "Bookmark"),
("doc.reference.show", "Show"),
("doc.reference.text", "Its text"),
("doc.reference.page", "Page number"),
("doc.reference.chapter", "Chapter number"),
("doc.reference.direction", "Above or below"),
(
"doc.reference.none",
"Cross-references point to bookmarks. Insert a bookmark first.",
),
("doc.caption.title", "Insert Caption"),
("doc.caption.text", "Caption"),
("doc.caption.empty", "Write the caption."),
(
"doc.caption.none",
"Select an image, a formula, or a table cell to caption.",
),
("doc.formula.title", "Insert Formula"),
("doc.formula.edit", "Edit Formula"),
("doc.formula.text", "Formula"),
("doc.formula.empty", "Write a formula, such as a over b."),
("doc.formula.apply", "Apply"),
("doc.comment.title", "New Comment"),
("doc.comment.text", "Comment"),
("doc.comment.empty", "Write the comment."),
("doc.review.track", "Track Changes"),
("doc.review.accept_all", "Accept All"),
("doc.review.reject_all", "Reject All"),
("doc.review.panel", "Comments and Changes"),
("doc.review.comments", "Comments {n}"),
("doc.review.changes", "Changes {n}"),
("doc.review.accept", "Accept"),
("doc.review.reject", "Reject"),
("doc.review.none", "No tracked changes"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("menu.review", "审阅"),
("doc.insert.contents", "目录"),
("doc.insert.update_contents", "更新目录"),
("doc.contents.title", "目录"),
("doc.insert.footnote", "脚注"),
("doc.insert.endnote", "尾注"),
("doc.insert.bookmark", "书签…"),
("doc.insert.link", "超链接…"),
("doc.insert.reference", "交叉引用…"),
("doc.insert.caption", "题注…"),
("doc.insert.formula", "公式…"),
("doc.insert.comment", "批注…"),
("doc.bookmark.title", "插入书签"),
("doc.bookmark.name", "名称"),
("doc.bookmark.empty", "请为书签命名。"),
("doc.bookmark.taken", "已有同名书签。"),
("doc.link.title", "插入超链接"),
("doc.link.text", "文字"),
("doc.link.url", "地址"),
("doc.reference.title", "插入交叉引用"),
("doc.reference.target", "书签"),
("doc.reference.show", "显示"),
("doc.reference.text", "书签文字"),
("doc.reference.page", "页码"),
("doc.reference.chapter", "章节号"),
("doc.reference.direction", "上方或下方"),
("doc.reference.none", "交叉引用指向书签。请先插入书签。"),
("doc.caption.title", "插入题注"),
("doc.caption.text", "题注"),
("doc.caption.empty", "请写题注内容。"),
("doc.caption.none", "请先选中图片、公式或表格单元格。"),
("doc.formula.title", "插入公式"),
("doc.formula.edit", "编辑公式"),
("doc.formula.text", "公式"),
("doc.formula.empty", "请写公式,如 a over b。"),
("doc.formula.apply", "应用"),
("doc.comment.title", "新批注"),
("doc.comment.text", "批注"),
("doc.comment.empty", "请写批注内容。"),
("doc.review.track", "修订"),
("doc.review.accept_all", "全部接受"),
("doc.review.reject_all", "全部拒绝"),
("doc.review.panel", "批注与修订"),
("doc.review.comments", "批注 {n}"),
("doc.review.changes", "修订 {n}"),
("doc.review.accept", "接受"),
("doc.review.reject", "拒绝"),
("doc.review.none", "没有修订"),
];
+253
View File
@@ -0,0 +1,253 @@
//! ElyPDF's passwords, permissions and signatures.
pub(super) const EN: &[(&str, &str)] = &[
(
"pdf.restricted",
"This document does not allow {what}. Enter the owner password to lift its limits.",
),
("pdf.may.print", "printing"),
("pdf.may.copy", "copying text and pages"),
("pdf.may.edit", "changing content"),
("pdf.may.annotate", "annotating"),
("pdf.may.fill", "filling forms"),
("pdf.may.assemble", "changing pages and bookmarks"),
("pdf.may.owner", "changing its protection"),
("pdf.owner.open", "Enter Owner Password…"),
("pdf.owner.title", "Owner password for {name}"),
("pdf.owner.field", "Owner password"),
("pdf.owner.go", "Unlock"),
("pdf.owner.done", "Limits lifted for {name}."),
("pdf.owner.wrong", "That is not the owner password."),
("pdf.owner.already", "This document has no limits to lift."),
("pdf.protect.open", "Password and Permissions…"),
("pdf.protect.title", "Protect {name}"),
("pdf.protect.on", "Protect with passwords"),
("pdf.protect.user", "Open password"),
("pdf.protect.owner", "Owner password"),
("pdf.protect.print", "Printing"),
("pdf.protect.copy", "Copying text and pages"),
("pdf.protect.edit", "Changing content"),
("pdf.protect.annotate", "Annotating"),
("pdf.protect.fill", "Filling forms"),
("pdf.protect.assemble", "Changing pages and bookmarks"),
(
"pdf.protect.detail",
"Writes a new file with AES-256. Leave the open password empty to open without asking; the limits still hold. Without protection, the new file has no passwords.",
),
(
"pdf.protect.needs_owner",
"An owner password is needed, and it must differ from the open password.",
),
("pdf.protect.tail", "protected"),
("pdf.protect.bare", "unprotected"),
("pdf.protect.done", "Saved {name}."),
("pdf.side.signs", "Signatures"),
("pdf.sign.show", "Signatures…"),
("pdf.sign.close", "Close"),
("pdf.sign.state", "State"),
("pdf.sign.integrity", "Integrity"),
("pdf.sign.trust", "Certificate"),
("pdf.sign.revoked", "Revocation"),
("pdf.sign.when", "Signed at (as claimed)"),
("pdf.sign.list", "Signatures in {name}"),
("pdf.sign.none", "No signature fields"),
("pdf.sign.empty", "Not signed"),
("pdf.sign.intact", "Unchanged since it was signed"),
(
"pdf.sign.later",
"Signed part unchanged; later updates are ones it allows",
),
(
"pdf.sign.broken",
"Changed after signing in ways it does not allow",
),
("pdf.sign.changed", "The signed content has changed"),
("pdf.sign.unreadable", "The signature cannot be read: {why}"),
("pdf.sign.trusted", "Certificate trusted by this system"),
(
"pdf.sign.untrusted",
"Certificate not trusted by this system: {why}",
),
("pdf.sign.unchecked", "Certificate trust not checked: {why}"),
("pdf.sign.no_certificate", "No certificate in the signature"),
("pdf.sign.revocation", "Revocation not checked"),
("pdf.sign.open", "Sign with Certificate…"),
("pdf.sign.title", "Sign with a certificate"),
(
"pdf.sign.detail",
"Writes a signed copy by an update to this file's bytes; this document stays as it is.",
),
("pdf.sign.certificate", "Certificate"),
("pdf.sign.no_file", "None chosen (.p12 or .pfx)"),
("pdf.sign.choose", "Choose…"),
("pdf.sign.password", "Certificate password"),
("pdf.sign.reason", "Reason"),
("pdf.sign.location", "Location"),
("pdf.sign.tail", "signed"),
("pdf.sign.done", "Saved {name}, signed and checked."),
("pdf.visible.open", "Add Visible Signature…"),
("pdf.visible.title", "Visible signature"),
(
"pdf.visible.detail",
"A picture of a signature on the page. It is not a digital signature and proves nothing about who signed.",
),
("pdf.visible.draw", "Draw"),
("pdf.visible.type", "Type"),
("pdf.visible.picture", "Picture"),
("pdf.visible.name", "Name"),
("pdf.visible.no_picture", "None chosen"),
("pdf.visible.place", "Place on Page"),
("pdf.font.script", "Script"),
("pdf.repair.show", "Report…"),
("pdf.repair.title", "Repair report for {name}"),
(
"pdf.repair.detail",
"What MuPDF found broken while reading the file. The repaired copy is open; the file itself is untouched.",
),
("pdf.export.title", "Export"),
("pdf.export.images", "Page images"),
("pdf.export.text", "Text"),
("pdf.export.optimized", "Optimized PDF"),
("pdf.export.resolution", "Resolution"),
("pdf.export.dpi72", "72 dpi"),
("pdf.export.dpi150", "150 dpi"),
("pdf.export.dpi300", "300 dpi"),
("pdf.export.pages", "Pages"),
("pdf.export.all", "All"),
("pdf.export.picked", "Picked"),
(
"pdf.export.compress",
"Compress streams, fonts and pictures",
),
("pdf.export.shrink", "Downsample pictures"),
("pdf.export.above", "Above"),
("pdf.export.to", "Down to"),
("pdf.export.go", "Export…"),
("pdf.export.tail", "optimized"),
("pdf.export.done", "Saved {name}."),
("pdf.export.images_done", "Saved {count} page images."),
(
"pdf.export.optimized_done",
"Saved {name}: {before} → {after}.",
),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
(
"pdf.restricted",
"此文档不允许{what}。输入所有者密码可解除限制。",
),
("pdf.may.print", "打印"),
("pdf.may.copy", "复制文字与页面"),
("pdf.may.edit", "修改内容"),
("pdf.may.annotate", "添加批注"),
("pdf.may.fill", "填写表单"),
("pdf.may.assemble", "调整页面与书签"),
("pdf.may.owner", "更改保护"),
("pdf.owner.open", "输入所有者密码…"),
("pdf.owner.title", "{name} 的所有者密码"),
("pdf.owner.field", "所有者密码"),
("pdf.owner.go", "解除限制"),
("pdf.owner.done", "已解除 {name} 的限制。"),
("pdf.owner.wrong", "所有者密码不对。"),
("pdf.owner.already", "此文档没有可解除的限制。"),
("pdf.protect.open", "密码与权限…"),
("pdf.protect.title", "保护 {name}"),
("pdf.protect.on", "以密码保护"),
("pdf.protect.user", "打开密码"),
("pdf.protect.owner", "所有者密码"),
("pdf.protect.print", "打印"),
("pdf.protect.copy", "复制文字与页面"),
("pdf.protect.edit", "修改内容"),
("pdf.protect.annotate", "添加批注"),
("pdf.protect.fill", "填写表单"),
("pdf.protect.assemble", "调整页面与书签"),
(
"pdf.protect.detail",
"以 AES-256 写出新文件。打开密码留空则打开时不询问,限制照样有效。不保护时,新文件没有密码。",
),
(
"pdf.protect.needs_owner",
"需要所有者密码,且须与打开密码不同。",
),
("pdf.protect.tail", "已保护"),
("pdf.protect.bare", "未保护"),
("pdf.protect.done", "已保存 {name}。"),
("pdf.side.signs", "签名"),
("pdf.sign.show", "签名…"),
("pdf.sign.close", "关闭"),
("pdf.sign.state", "状态"),
("pdf.sign.integrity", "完整性"),
("pdf.sign.trust", "证书"),
("pdf.sign.revoked", "吊销"),
("pdf.sign.when", "签名时间(签名者所称)"),
("pdf.sign.list", "{name} 中的签名"),
("pdf.sign.none", "没有签名字段"),
("pdf.sign.empty", "未签名"),
("pdf.sign.intact", "签名后未改动"),
("pdf.sign.later", "签名部分未改动;其后的更新在签名允许之内"),
("pdf.sign.broken", "签名后有签名不允许的改动"),
("pdf.sign.changed", "签名所覆盖的内容已改动"),
("pdf.sign.unreadable", "无法读取签名:{why}"),
("pdf.sign.trusted", "本机信任此证书"),
("pdf.sign.untrusted", "本机不信任此证书:{why}"),
("pdf.sign.unchecked", "未核对证书信任:{why}"),
("pdf.sign.no_certificate", "签名中没有证书"),
("pdf.sign.revocation", "未核验吊销状态"),
("pdf.sign.open", "以证书签名…"),
("pdf.sign.title", "以证书签名"),
(
"pdf.sign.detail",
"在此文件的字节上追加更新,写出已签名的副本;此文档保持原样。",
),
("pdf.sign.certificate", "证书"),
("pdf.sign.no_file", "未选择(.p12 或 .pfx)"),
("pdf.sign.choose", "选择…"),
("pdf.sign.password", "证书密码"),
("pdf.sign.reason", "原因"),
("pdf.sign.location", "地点"),
("pdf.sign.tail", "已签名"),
("pdf.sign.done", "已保存 {name},已签名并核对。"),
("pdf.visible.open", "添加可视签名…"),
("pdf.visible.title", "可视签名"),
(
"pdf.visible.detail",
"页面上的签名图样。它不是数字签名,不能证明签名人是谁。",
),
("pdf.visible.draw", "手写"),
("pdf.visible.type", "键入"),
("pdf.visible.picture", "图片"),
("pdf.visible.name", "姓名"),
("pdf.visible.no_picture", "未选择"),
("pdf.visible.place", "放到页面上"),
("pdf.font.script", "手写体"),
("pdf.repair.show", "报告…"),
("pdf.repair.title", "{name} 的修复报告"),
(
"pdf.repair.detail",
"MuPDF 读取文件时发现的损坏。打开的是修复后的副本;文件本身未改动。",
),
("pdf.export.title", "导出"),
("pdf.export.images", "页面图片"),
("pdf.export.text", "文字"),
("pdf.export.optimized", "优化的 PDF"),
("pdf.export.resolution", "分辨率"),
("pdf.export.dpi72", "72 dpi"),
("pdf.export.dpi150", "150 dpi"),
("pdf.export.dpi300", "300 dpi"),
("pdf.export.pages", "页面"),
("pdf.export.all", "全部"),
("pdf.export.picked", "所选"),
("pdf.export.compress", "压缩数据流、字体与图片"),
("pdf.export.shrink", "图片降采样"),
("pdf.export.above", "高于"),
("pdf.export.to", "降到"),
("pdf.export.go", "导出…"),
("pdf.export.tail", "已优化"),
("pdf.export.done", "已保存 {name}。"),
("pdf.export.images_done", "已保存 {count} 张页面图片。"),
(
"pdf.export.optimized_done",
"已保存 {name}:{before} → {after}。",
),
];
+111
View File
@@ -0,0 +1,111 @@
//! The settings window's words.
pub(super) const EN: &[(&str, &str)] = &[
("menu.app.settings", "Settings…"),
("settings.title", "Settings"),
("settings.general", "General"),
("settings.appearance", "Appearance"),
("settings.appearance.system", "System"),
("settings.appearance.light", "Light"),
("settings.appearance.dark", "Dark"),
("settings.omarchy", "Follow Omarchy"),
("settings.language", "Language"),
("settings.language.system", "System"),
("settings.language.next", "Applies the next time each app starts"),
("settings.motion", "Motion"),
("settings.motion.system", "System"),
("settings.motion.reduce", "Reduced"),
("settings.motion.full", "Full"),
("settings.documents", "Documents"),
("settings.author", "Author name"),
("settings.author.hint", "Signs new comments and tracked changes"),
("settings.autosave", "Save automatically"),
("settings.autosave.hint", "Files in this app's own formats save a few seconds after you stop typing. Signed files, files with macros, older formats, and files changed elsewhere still wait for you."),
("settings.git", "Git"),
("settings.git.path", "Git program"),
("settings.git.hint", "Empty uses the Git that comes with ElyTree"),
("settings.git.bad", "{path} is not Git 2.40 or later: {error}"),
("settings.storage", "Storage"),
("settings.storage.log", "This app's log"),
("settings.storage.cache", "Web content cache"),
("settings.storage.recovery", "Recovery copies"),
("settings.storage.clear", "Clear"),
("settings.storage.clear_later", "Clear at next start"),
("settings.storage.next_start", "Clears at next start"),
("settings.storage.keep", "Recovery points per document"),
("settings.storage.space", "Recovery space per document (MB)"),
("settings.storage.failed", "Not done: {error}"),
("settings.storage.size", "{size} KB"),
("settings.storage.delete", "Delete"),
("settings.storage.ask_title", "Clear {what}?"),
("settings.storage.ask_now", "{what} is deleted now. This cannot be undone."),
("settings.storage.ask_later", "{what} is deleted the next time this app starts, before anything opens. This cannot be undone."),
("settings.diagnostics", "Diagnostics"),
("settings.diagnostics.included", "Included"),
("settings.diagnostics.none", "Nothing to export yet"),
("settings.diagnostics.crash", "Crash report {name}"),
("settings.diagnostics.log", "This app's log"),
("settings.diagnostics.size", "{name} · {size}"),
("settings.diagnostics.redact", "Remove paths and personal names"),
("settings.diagnostics.export", "Export…"),
("settings.diagnostics.saved", "Saved to {path}"),
("settings.diagnostics.failed", "Not exported: {error}"),
("settings.unreadable", "The settings file cannot be read, so changes are not saved: {error}"),
("settings.unsaved", "The last change was not saved: {error}"),
("settings.app_unreadable", "This app's settings cannot be read, so defaults are in use: {error}"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("menu.app.settings", "设置…"),
("settings.title", "设置"),
("settings.general", "通用"),
("settings.appearance", "外观"),
("settings.appearance.system", "跟随系统"),
("settings.appearance.light", "浅色"),
("settings.appearance.dark", "深色"),
("settings.omarchy", "跟随 Omarchy"),
("settings.language", "语言"),
("settings.language.system", "跟随系统"),
("settings.language.next", "各应用下次启动时生效"),
("settings.motion", "动效"),
("settings.motion.system", "跟随系统"),
("settings.motion.reduce", "减少"),
("settings.motion.full", "完整"),
("settings.documents", "文档"),
("settings.author", "作者姓名"),
("settings.author.hint", "新批注与修订以此署名"),
("settings.autosave", "自动保存"),
("settings.autosave.hint", "本应用自有格式的文件在停止输入几秒后自动保存。已签名、含宏、旧格式以及在别处被改动的文件仍等你决定。"),
("settings.git", "Git"),
("settings.git.path", "Git 程序"),
("settings.git.hint", "留空即用 ElyTree 随带的 Git"),
("settings.git.bad", "{path} 不是 2.40 或更新的 Git:{error}"),
("settings.storage", "存储"),
("settings.storage.log", "本应用的日志"),
("settings.storage.cache", "网页内容缓存"),
("settings.storage.recovery", "恢复副本"),
("settings.storage.clear", "清除"),
("settings.storage.clear_later", "下次启动时清除"),
("settings.storage.next_start", "将在下次启动时清除"),
("settings.storage.keep", "每个文档保留的恢复点"),
("settings.storage.space", "每个文档的恢复空间(MB)"),
("settings.storage.failed", "未完成:{error}"),
("settings.storage.size", "{size} KB"),
("settings.storage.delete", "删除"),
("settings.storage.ask_title", "清除{what}?"),
("settings.storage.ask_now", "{what}将立即删除,无法撤销。"),
("settings.storage.ask_later", "{what}将在本应用下次启动、打开任何内容之前删除,无法撤销。"),
("settings.diagnostics", "诊断"),
("settings.diagnostics.included", "包含"),
("settings.diagnostics.none", "暂无可导出的内容"),
("settings.diagnostics.crash", "崩溃报告 {name}"),
("settings.diagnostics.log", "本应用的日志"),
("settings.diagnostics.size", "{name} · {size}"),
("settings.diagnostics.redact", "去掉路径与个人姓名"),
("settings.diagnostics.export", "导出…"),
("settings.diagnostics.saved", "已存到 {path}"),
("settings.diagnostics.failed", "未导出:{error}"),
("settings.unreadable", "设置文件读不出,改动不会保存:{error}"),
("settings.unsaved", "上一项改动没有保存:{error}"),
("settings.app_unreadable", "本应用的设置读不出,正用默认值:{error}"),
];
+366
View File
@@ -0,0 +1,366 @@
//! ElyShow's messages.
pub(super) const EN: &[(&str, &str)] = &[
("menu.slide", "Slide"),
("show.slide.new", "New Slide"),
("show.slide.duplicate", "Duplicate Slide"),
("show.slide.delete", "Delete Slide"),
("show.slide.hide", "Hide Slide"),
("show.slide.show", "Show Slide"),
("show.slide.up", "Move Up"),
("show.slide.down", "Move Down"),
("show.slide.first", "Move to Start"),
("show.slide.last", "Move to End"),
("show.slide.hidden", "hidden"),
("show.strip.slides", "Slides"),
("show.strip.masters", "Masters"),
("show.masters.busy", "Close the masters to change slides"),
("show.masters.edit", "Edit Masters"),
("show.masters.close", "Close Masters"),
("show.masters.new", "New Master"),
("show.design.layout", "Layout"),
("show.design.master", "Master"),
("show.design.theme", "Colors"),
("show.design.size", "Size"),
("show.design.width", "Width (cm)"),
("show.design.height", "Height (cm)"),
("show.layout.20", "Blank"),
("show.layout.0", "Title Slide"),
("show.layout.1", "Title, Content"),
("show.layout.3", "Title and 2 Content"),
("show.layout.19", "Title Only"),
("show.layout.32", "Centered Text"),
("show.layout.12", "Title, Content and 2 Content"),
("show.layout.15", "Title, 2 Content and Content"),
("show.layout.14", "Title, Content over Content"),
("show.layout.16", "Title, 2 Content over Content"),
("show.layout.18", "Title, 4 Content"),
("show.layout.34", "Title, 6 Content"),
("show.layout.other", "Other"),
("show.size.wide", "Widescreen 16:9"),
("show.size.hd", "16:9"),
("show.size.standard", "4:3"),
("show.size.a4", "A4"),
("show.size.letter", "Letter"),
("show.size.custom", "Custom"),
("show.new.title", "New Presentation"),
("show.new.category", "Presentations"),
("show.new.blank", "Blank"),
("show.new.blank_about", "One title slide, 16:9"),
("show.new.from_file", "From a File…"),
("menu.view", "View"),
("show.insert.text", "Text Box"),
("show.insert.rectangle", "Rectangle"),
("show.insert.ellipse", "Ellipse"),
("show.insert.triangle", "Triangle"),
("show.insert.line", "Line"),
("show.insert.arrow", "Arrow"),
("show.insert.connector", "Connector"),
("show.insert.picture", "Picture…"),
("show.insert.media", "Audio or Video…"),
("show.view.snap_grid", "Snap to Grid"),
("show.view.snap_guides", "Snap to Guides"),
("show.view.guide_across", "Add Horizontal Guide"),
("show.view.guide_down", "Add Vertical Guide"),
("show.objects", "Objects"),
("show.object.geometry", "Position and Size"),
("show.object.position", "Position"),
("show.object.size", "Size"),
("show.object.x", "X (cm)"),
("show.object.y", "Y (cm)"),
("show.object.width", "Width (cm)"),
("show.object.height", "Height (cm)"),
("show.object.rotation", "Rotation (°)"),
("show.object.lock", "Lock"),
("show.object.look", "Fill and Outline"),
("show.object.fill", "Fill"),
("show.object.no_fill", "No Fill"),
("show.object.transparency", "Transparency (%)"),
("show.object.shadow", "Shadow"),
("show.object.line", "Outline"),
("show.object.no_line", "No Outline"),
("show.object.line_width", "Outline Width (pt)"),
("show.object.text", "Text"),
("show.object.why_text", "Select text or a text box first"),
("show.object.font", "Font"),
("show.object.font_size", "Size"),
("show.object.effects", "Style"),
("show.object.color", "Color"),
("show.object.align", "Alignment"),
("show.object.lists", "Lists"),
("show.object.autofit", "Autofit"),
("show.fit.off", "Off"),
("show.fit.shrink", "Shrink Text on Overflow"),
("show.fit.grow", "Resize Box to Fit Text"),
("show.object.picture", "Picture"),
("show.object.replace", "Replace…"),
("show.object.crop", "Crop"),
("show.object.why_picture", "Select a picture first"),
("show.object.arrange", "Arrange"),
("show.object.order", "Order"),
("show.object.front", "Bring to Front"),
("show.object.forward", "Bring Forward"),
("show.object.backward", "Send Backward"),
("show.object.back", "Send to Back"),
("show.object.align_to", "Align"),
("show.object.align_left", "Align Left"),
("show.object.align_center", "Align Center"),
("show.object.align_right", "Align Right"),
("show.object.align_top", "Align Top"),
("show.object.align_middle", "Align Middle"),
("show.object.align_bottom", "Align Bottom"),
("show.object.spread", "Distribute"),
("show.object.spread_across", "Distribute Horizontally"),
("show.object.spread_down", "Distribute Vertically"),
("show.object.grouping", "Group"),
("show.object.group", "Group"),
("show.object.ungroup", "Ungroup"),
("show.placeholder.title", "Title placeholder"),
("show.placeholder.body", "Content placeholder"),
("show.placeholder.other", "Placeholder"),
("show.kind.text", "Text"),
("show.kind.shape", "Shape"),
("show.kind.line", "Line"),
("show.kind.connector", "Connector"),
("show.kind.image", "Picture"),
("show.kind.group", "Group"),
("show.kind.table", "Table"),
("show.kind.chart", "Chart"),
("show.kind.media", "Media"),
("show.kind.object", "Object"),
("show.kind.other", "Other"),
("show.chart.category", "Category {n}"),
("show.chart.series", "Series {n}"),
("show.chart.category_name", "Category name"),
("show.chart.series_name", "Series name"),
("show.chart.value", "Value"),
(
"show.chart.not_a_number",
"{series} for {category} is not a number",
),
(
"show.chart.no_table",
"This chart's data is not one table (each series has its own X values); only its look changes here",
),
(
"show.chart.x_not_a_number",
"The X value {category} is not a number",
),
("show.chart.add_category", "Add Category"),
("show.chart.drop_category", "Remove Category"),
("show.chart.add_series", "Add Series"),
("show.chart.drop_series", "Remove Series"),
("show.chart.edit", "Edit Chart…"),
("show.table.title", "Insert Table"),
("show.table.columns", "Columns"),
("show.table.rows", "Rows"),
("show.table.go", "Insert"),
("show.table.why", "Select cells in a table first"),
("show.table.row_above", "Insert Above"),
("show.table.row_below", "Insert Below"),
("show.table.column_left", "Insert Left"),
("show.table.column_right", "Insert Right"),
("show.table.delete", "Delete"),
("show.table.merge", "Merge Cells"),
("show.table.delete_table", "Delete Table"),
("show.table.group", "Table"),
("show.table.cells", "Cells"),
("show.notes.label", "Speaker notes"),
("show.notes.hint", "Click to add notes"),
("show.notes.none", "This slide has no notes box."),
(
"show.notes.rich",
"These notes are formatted; Ely shows them read-only to keep it.",
),
("show.remark.slide", "Slide {n}"),
("show.remark.text", "Comment"),
("show.remark.new_title", "New Comment"),
("show.remark.edit_title", "Edit Comment"),
("show.remark.save", "Save"),
("show.remark.blank", "Write the comment first"),
("show.remark.add", "Add Comment…"),
("show.remark.edit", "Edit…"),
("show.remark.delete", "Delete"),
("show.insert.table", "Table…"),
("show.insert.chart", "Chart…"),
("show.insert.remark", "Comment…"),
("show.view.remarks", "Comments"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("menu.slide", "幻灯片"),
("show.slide.new", "新建幻灯片"),
("show.slide.duplicate", "复制幻灯片"),
("show.slide.delete", "删除幻灯片"),
("show.slide.hide", "隐藏幻灯片"),
("show.slide.show", "显示幻灯片"),
("show.slide.up", "上移"),
("show.slide.down", "下移"),
("show.slide.first", "移到开头"),
("show.slide.last", "移到末尾"),
("show.slide.hidden", "已隐藏"),
("show.strip.slides", "幻灯片"),
("show.strip.masters", "母版"),
("show.masters.busy", "先关闭母版,再改幻灯片"),
("show.masters.edit", "编辑母版"),
("show.masters.close", "关闭母版"),
("show.masters.new", "新建母版"),
("show.design.layout", "版式"),
("show.design.master", "母版"),
("show.design.theme", "配色"),
("show.design.size", "尺寸"),
("show.design.width", "宽(厘米)"),
("show.design.height", "高(厘米)"),
("show.layout.20", "空白"),
("show.layout.0", "标题幻灯片"),
("show.layout.1", "标题,内容"),
("show.layout.3", "标题和两栏内容"),
("show.layout.19", "仅标题"),
("show.layout.32", "居中文本"),
("show.layout.12", "标题,内容与两栏内容"),
("show.layout.15", "标题,两栏内容与内容"),
("show.layout.14", "标题,内容在内容之上"),
("show.layout.16", "标题,两栏内容在内容之上"),
("show.layout.18", "标题,四栏内容"),
("show.layout.34", "标题,六栏内容"),
("show.layout.other", "其他"),
("show.size.wide", "宽屏 16:9"),
("show.size.hd", "16:9"),
("show.size.standard", "4:3"),
("show.size.a4", "A4"),
("show.size.letter", "Letter"),
("show.size.custom", "自定义"),
("show.new.title", "新建演示文稿"),
("show.new.category", "演示文稿"),
("show.new.blank", "空白"),
("show.new.blank_about", "一张标题幻灯片,16:9"),
("show.new.from_file", "从文件…"),
("menu.view", "视图"),
("show.insert.text", "文本框"),
("show.insert.rectangle", "矩形"),
("show.insert.ellipse", "椭圆"),
("show.insert.triangle", "三角形"),
("show.insert.line", "直线"),
("show.insert.arrow", "箭头"),
("show.insert.connector", "连接线"),
("show.insert.picture", "图片…"),
("show.insert.media", "音频或视频…"),
("show.view.snap_grid", "吸附到网格"),
("show.view.snap_guides", "吸附到参考线"),
("show.view.guide_across", "添加水平参考线"),
("show.view.guide_down", "添加垂直参考线"),
("show.objects", "对象"),
("show.object.geometry", "位置和大小"),
("show.object.position", "位置"),
("show.object.size", "大小"),
("show.object.x", "X(厘米)"),
("show.object.y", "Y(厘米)"),
("show.object.width", "宽(厘米)"),
("show.object.height", "高(厘米)"),
("show.object.rotation", "旋转(度)"),
("show.object.lock", "锁定"),
("show.object.look", "填充和轮廓"),
("show.object.fill", "填充"),
("show.object.no_fill", "无填充"),
("show.object.transparency", "透明度(%)"),
("show.object.shadow", "阴影"),
("show.object.line", "轮廓"),
("show.object.no_line", "无轮廓"),
("show.object.line_width", "轮廓宽度(磅)"),
("show.object.text", "文字"),
("show.object.why_text", "请先选中文字或文本框"),
("show.object.font", "字体"),
("show.object.font_size", "字号"),
("show.object.effects", "样式"),
("show.object.color", "颜色"),
("show.object.align", "对齐"),
("show.object.lists", "列表"),
("show.object.autofit", "自动适配"),
("show.fit.off", "不适配"),
("show.fit.shrink", "溢出时缩小文字"),
("show.fit.grow", "调整文本框以适应文字"),
("show.object.picture", "图片"),
("show.object.replace", "替换…"),
("show.object.crop", "裁剪"),
("show.object.why_picture", "请先选中图片"),
("show.object.arrange", "排列"),
("show.object.order", "层次"),
("show.object.front", "置于顶层"),
("show.object.forward", "上移一层"),
("show.object.backward", "下移一层"),
("show.object.back", "置于底层"),
("show.object.align_to", "对齐"),
("show.object.align_left", "左对齐"),
("show.object.align_center", "水平居中"),
("show.object.align_right", "右对齐"),
("show.object.align_top", "顶端对齐"),
("show.object.align_middle", "垂直居中"),
("show.object.align_bottom", "底端对齐"),
("show.object.spread", "分布"),
("show.object.spread_across", "横向分布"),
("show.object.spread_down", "纵向分布"),
("show.object.grouping", "组合"),
("show.object.group", "组合"),
("show.object.ungroup", "取消组合"),
("show.placeholder.title", "标题占位符"),
("show.placeholder.body", "内容占位符"),
("show.placeholder.other", "占位符"),
("show.kind.text", "文本"),
("show.kind.shape", "图形"),
("show.kind.line", "线条"),
("show.kind.connector", "连接线"),
("show.kind.image", "图片"),
("show.kind.group", "组合"),
("show.kind.table", "表格"),
("show.kind.chart", "图表"),
("show.kind.media", "媒体"),
("show.kind.object", "对象"),
("show.kind.other", "其他"),
("show.chart.category", "类别 {n}"),
("show.chart.series", "系列 {n}"),
("show.chart.category_name", "类别名称"),
("show.chart.series_name", "系列名称"),
("show.chart.value", "数值"),
("show.chart.not_a_number", "{category} 的 {series} 不是数字"),
(
"show.chart.no_table",
"此图表的数据不是一张表(各系列自带 X 值),这里只改外观",
),
("show.chart.x_not_a_number", "X 值 {category} 不是数字"),
("show.chart.add_category", "添加类别"),
("show.chart.drop_category", "删除类别"),
("show.chart.add_series", "添加系列"),
("show.chart.drop_series", "删除系列"),
("show.chart.edit", "编辑图表…"),
("show.table.title", "插入表格"),
("show.table.columns", "列"),
("show.table.rows", "行"),
("show.table.go", "插入"),
("show.table.why", "请先选中表格中的单元格"),
("show.table.row_above", "在上方插入"),
("show.table.row_below", "在下方插入"),
("show.table.column_left", "在左侧插入"),
("show.table.column_right", "在右侧插入"),
("show.table.delete", "删除"),
("show.table.merge", "合并单元格"),
("show.table.delete_table", "删除表格"),
("show.table.group", "表格"),
("show.table.cells", "单元格"),
("show.notes.label", "演讲者备注"),
("show.notes.hint", "单击此处添加备注"),
("show.notes.none", "这张幻灯片没有备注框。"),
("show.notes.rich", "备注带格式,为保留格式在此只读。"),
("show.remark.slide", "第 {n} 张幻灯片"),
("show.remark.text", "批注"),
("show.remark.new_title", "新建批注"),
("show.remark.edit_title", "编辑批注"),
("show.remark.save", "保存"),
("show.remark.blank", "请先写批注"),
("show.remark.add", "添加批注…"),
("show.remark.edit", "编辑…"),
("show.remark.delete", "删除"),
("show.insert.table", "表格…"),
("show.insert.chart", "图表…"),
("show.insert.remark", "批注…"),
("show.view.remarks", "批注"),
];
+173
View File
@@ -0,0 +1,173 @@
//! ElyTable's messages.
pub(super) const EN: &[(&str, &str)] = &[
("menu.sheet", "Sheet"),
("menu.cells", "Cells"),
("table.sheet.default", "Sheet{n}"),
("table.sheet.insert", "Insert Sheet"),
("table.sheet.rename", "Rename Sheet…"),
("table.sheet.duplicate", "Duplicate Sheet"),
("table.sheet.left", "Move Sheet Left"),
("table.sheet.right", "Move Sheet Right"),
("table.sheet.hide", "Hide Sheet"),
("table.sheet.show", "Show Sheet…"),
("table.sheet.delete", "Delete Sheet…"),
("table.sheet.add", "Add a sheet"),
("table.sheet.last", "A workbook keeps one visible sheet."),
("table.sheet.edge", "The sheet is already at that end."),
("table.rename.title", "Rename {name}"),
("table.rename.label", "Name"),
("table.rename.empty", "Type a name."),
("table.rename.taken", "Another sheet has that name."),
(
"table.rename.invalid",
"A sheet name cannot hold : \\ / ? * [ ] or start or end with '.",
),
("table.show.title", "Show a Hidden Sheet"),
("table.show.go", "Show"),
("table.show.none", "No sheet is hidden."),
("table.delete.title", "Delete {name}?"),
(
"table.delete.alone",
"Its cells go away; no other sheet or name refers to it.",
),
(
"table.delete.refs",
"{formulas} formulas on other sheets, first {first}, and {names} named ranges refer to it; they will show #REF!.",
),
(
"table.delete.dynamic",
"{count} formulas or names use INDIRECT; what they point to is known only when they run, so they may break too.",
),
("table.delete.go", "Delete"),
("table.cells.rows_above", "Insert Rows Above"),
("table.cells.rows_below", "Insert Rows Below"),
("table.cells.columns_before", "Insert Columns Before"),
("table.cells.columns_after", "Insert Columns After"),
("table.cells.insert_down", "Insert Cells, Shift Down"),
("table.cells.insert_right", "Insert Cells, Shift Right"),
("table.cells.delete_rows", "Delete Rows"),
("table.cells.delete_columns", "Delete Columns"),
("table.cells.delete_up", "Delete Cells, Shift Up"),
("table.cells.delete_left", "Delete Cells, Shift Left"),
("table.cells.merge", "Merge Cells"),
("table.cells.split", "Split Cells"),
("table.cells.row_height", "Row Height…"),
("table.cells.column_width", "Column Width…"),
("table.cells.fit_rows", "Fit Row Height"),
("table.cells.fit_columns", "Fit Column Width"),
("table.cells.hide_rows", "Hide Rows"),
("table.cells.show_rows", "Show Rows"),
("table.cells.hide_columns", "Hide Columns"),
("table.cells.show_columns", "Show Columns"),
("table.cells.freeze", "Freeze or Unfreeze Panes"),
("table.size.row", "Row Height"),
("table.size.column", "Column Width"),
("table.size.label", "Centimeters"),
("table.size.bad", "Type a size from 0.01 to 65 cm."),
("table.size.apply", "Apply"),
("table.merge.title", "Merge Cells That Hold Data?"),
(
"table.merge.detail",
"A merged cell shows only one value. Move every value into it, or keep the others hidden in the merge.",
),
("table.merge.move", "Move into First Cell"),
("table.merge.keep", "Keep Hidden"),
("table.name_box", "Cell address"),
("table.figure.sum", "Sum {value}"),
("table.figure.selected", "Selected {value}"),
("table.figure.count", "Count {value}"),
("table.figure.counta", "Filled {value}"),
("table.figure.average", "Average {value}"),
("table.figure.max", "Max {value}"),
("table.figure.min", "Min {value}"),
("table.header.column", "Column {name}"),
("table.header.row", "Row {name}"),
("table.formula", "Cell content"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("menu.sheet", "工作表"),
("menu.cells", "单元格"),
("table.sheet.default", "工作表{n}"),
("table.sheet.insert", "插入工作表"),
("table.sheet.rename", "重命名工作表…"),
("table.sheet.duplicate", "复制工作表"),
("table.sheet.left", "左移工作表"),
("table.sheet.right", "右移工作表"),
("table.sheet.hide", "隐藏工作表"),
("table.sheet.show", "显示工作表…"),
("table.sheet.delete", "删除工作表…"),
("table.sheet.add", "添加工作表"),
("table.sheet.last", "工作簿至少保留一个可见工作表。"),
("table.sheet.edge", "工作表已在这一端。"),
("table.rename.title", "重命名 {name}"),
("table.rename.label", "名称"),
("table.rename.empty", "请输入名称。"),
("table.rename.taken", "已有工作表用了这个名称。"),
(
"table.rename.invalid",
"工作表名称不能含 : \\ / ? * [ ],也不能以 ' 开头或结尾。",
),
("table.show.title", "显示隐藏的工作表"),
("table.show.go", "显示"),
("table.show.none", "没有隐藏的工作表。"),
("table.delete.title", "删除 {name}?"),
(
"table.delete.alone",
"其中的单元格将被删除;没有别的工作表或名称引用它。",
),
(
"table.delete.refs",
"其他工作表上有 {formulas} 个公式(第一个在 {first})和 {names} 个命名区域引用它;删除后它们显示 #REF!。",
),
(
"table.delete.dynamic",
"另有 {count} 个公式或名称用 INDIRECT,指向何处要运行时才知道,删除后也可能出错。",
),
("table.delete.go", "删除"),
("table.cells.rows_above", "在上方插入行"),
("table.cells.rows_below", "在下方插入行"),
("table.cells.columns_before", "在左侧插入列"),
("table.cells.columns_after", "在右侧插入列"),
("table.cells.insert_down", "插入单元格,下移"),
("table.cells.insert_right", "插入单元格,右移"),
("table.cells.delete_rows", "删除行"),
("table.cells.delete_columns", "删除列"),
("table.cells.delete_up", "删除单元格,上移"),
("table.cells.delete_left", "删除单元格,左移"),
("table.cells.merge", "合并单元格"),
("table.cells.split", "拆分单元格"),
("table.cells.row_height", "行高…"),
("table.cells.column_width", "列宽…"),
("table.cells.fit_rows", "最适行高"),
("table.cells.fit_columns", "最适列宽"),
("table.cells.hide_rows", "隐藏行"),
("table.cells.show_rows", "显示行"),
("table.cells.hide_columns", "隐藏列"),
("table.cells.show_columns", "显示列"),
("table.cells.freeze", "冻结或取消冻结窗格"),
("table.size.row", "行高"),
("table.size.column", "列宽"),
("table.size.label", "厘米"),
("table.size.bad", "请输入 0.01 到 65 厘米。"),
("table.size.apply", "应用"),
("table.merge.title", "合并有数据的单元格?"),
(
"table.merge.detail",
"合并后只显示一个值。可把所有值移入第一个单元格,或让其他值隐藏在合并区域中。",
),
("table.merge.move", "移入第一个单元格"),
("table.merge.keep", "保留隐藏内容"),
("table.name_box", "单元格地址"),
("table.figure.sum", "求和 {value}"),
("table.figure.selected", "已选 {value}"),
("table.figure.count", "数值个数 {value}"),
("table.figure.counta", "非空个数 {value}"),
("table.figure.average", "平均值 {value}"),
("table.figure.max", "最大值 {value}"),
("table.figure.min", "最小值 {value}"),
("table.header.column", "{name} 列"),
("table.header.row", "第 {name} 行"),
("table.formula", "单元格内容"),
];
+107
View File
@@ -0,0 +1,107 @@
//! ElyDoc's find, spelling, compare and merge messages.
pub(super) const EN: &[(&str, &str)] = &[
("menu.tools", "Tools"),
("doc.count.words", "{words} words, {characters} characters"),
("doc.count.selected", "{selected} of {words} words"),
("office.find.open", "Find and Replace"),
("office.find.stale", "Edited. Enter counts again."),
("doc.spell.ignore_all", "Ignore All"),
("doc.spell.none", "No suggestions"),
("doc.compare.open", "Compare With…"),
("doc.compare.name", "{name} compared"),
("doc.output.export_title", "Export as PDF"),
("doc.output.print_title", "Print"),
("doc.output.pages", "Pages"),
("doc.output.all", "All"),
("doc.output.range", "Range"),
("doc.output.marks", "Marks"),
("doc.output.comments", "Comments in the margin"),
("doc.output.changes", "Tracked changes marked"),
("doc.output.export", "Export…"),
("doc.output.print", "Print…"),
("doc.output.exported", "Exported {name}"),
(
"doc.output.open",
"{name} is an open document; pick another name",
),
("doc.output.failed", "The PDF was not written: {error}"),
("doc.nav.open", "Navigator"),
("doc.nav.back", "Back"),
("doc.nav.outline", "Outline"),
("doc.nav.pages", "Pages"),
("doc.nav.marks", "Bookmarks"),
("doc.nav.page", "Page {n}"),
("doc.nav.gone", "That document is closed."),
(
"doc.fonts.missing",
"Fonts not on this computer: {fonts}. Others stand in, so lines and pages may move.",
),
("doc.merge.open", "Mail Merge…"),
("doc.merge.title", "Mail Merge"),
("doc.merge.data", "Data"),
("doc.merge.choose", "Choose CSV…"),
("doc.merge.fields", "Fields"),
("doc.merge.record", "Record"),
("doc.merge.preview", "Preview"),
("doc.merge.output", "Output"),
("doc.merge.generate", "Generate…"),
("doc.merge.preview_name", "{name} record {n}"),
("doc.merge.unreadable", "{name} cannot be read: {error}"),
("doc.merge.exists", "{name} exists. Nothing was written."),
("doc.merge.done", "Wrote {n} files to {folder}"),
(
"doc.merge.failed",
"The merge stopped at record {n}: {error}",
),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("menu.tools", "工具"),
("doc.count.words", "{words} 字,{characters} 字符"),
("doc.count.selected", "已选 {selected} / {words} 字"),
("office.find.open", "查找和替换"),
("office.find.stale", "已改动,回车重新计数"),
("doc.spell.ignore_all", "全部忽略"),
("doc.spell.none", "无建议"),
("doc.compare.open", "与其他版本比较…"),
("doc.compare.name", "{name} 比较结果"),
("doc.output.export_title", "导出为 PDF"),
("doc.output.print_title", "打印"),
("doc.output.pages", "页面"),
("doc.output.all", "全部"),
("doc.output.range", "范围"),
("doc.output.marks", "标记"),
("doc.output.comments", "批注印在页边"),
("doc.output.changes", "标出修订"),
("doc.output.export", "导出…"),
("doc.output.print", "打印…"),
("doc.output.exported", "已导出 {name}"),
("doc.output.open", "{name} 是打开着的文档,请换个名字"),
("doc.output.failed", "未写出 PDF:{error}"),
("doc.nav.open", "导航"),
("doc.nav.back", "返回"),
("doc.nav.outline", "大纲"),
("doc.nav.pages", "页面"),
("doc.nav.marks", "书签"),
("doc.nav.page", "第 {n} 页"),
("doc.nav.gone", "那份文档已关闭。"),
(
"doc.fonts.missing",
"本机缺少字体:{fonts}。已用其他字体代替,行与页可能移动。",
),
("doc.merge.open", "邮件合并…"),
("doc.merge.title", "邮件合并"),
("doc.merge.data", "数据"),
("doc.merge.choose", "选择 CSV…"),
("doc.merge.fields", "字段"),
("doc.merge.record", "记录"),
("doc.merge.preview", "预览"),
("doc.merge.output", "输出"),
("doc.merge.generate", "生成…"),
("doc.merge.preview_name", "{name} 第 {n} 条"),
("doc.merge.unreadable", "无法读取 {name}:{error}"),
("doc.merge.exists", "{name} 已存在,未写入任何文件。"),
("doc.merge.done", "已写入 {n} 个文件到 {folder}"),
("doc.merge.failed", "合并在第 {n} 条停止:{error}"),
];
+246
View File
@@ -0,0 +1,246 @@
//! ElyTree's repositories and work tree.
pub(super) const EN: &[(&str, &str)] = &[
("tree.menu.repository", "Repository"),
("tree.open", "Open Repository…"),
("tree.new", "New Repository…"),
("tree.clone", "Clone…"),
("tree.close", "Close Repository"),
("tree.empty", "Open a repository"),
("tree.cloning", "Cloning into {name}"),
("tree.clone.title", "Clone a repository"),
("tree.clone.url", "URL"),
("tree.clone.next", "Choose Folder…"),
("tree.clone.empty", "Enter a URL"),
("tree.clone.stop", "Stop"),
("tree.clone.stopped", "Clone stopped."),
("tree.clone.busy", "A clone is already running."),
("tree.clean", "Nothing to commit"),
("tree.changed", "{n} files changed"),
("tree.no_git", "Git is not available"),
("tree.unreadable", "This repository could not be read"),
("tree.branch.detached", "Detached at {oid}"),
("tree.branch.unborn", "{branch}, no commits yet"),
("tree.ahead_behind", "{upstream} ↑{ahead} ↓{behind}"),
("tree.upstream_gone", "{upstream} is gone"),
("tree.git.bundled", "{version}, bundled, {path}"),
("tree.git.system", "{version}, system, {path}"),
("tree.busy", "Wait for this repository's changes to finish."),
("tree.gone", "{path} is not on disk."),
("tree.discard.title", "Discard changes to {path}?"),
(
"tree.discard.tracked",
"The file goes back to its staged or committed content. This cannot be undone.",
),
(
"tree.discard.untracked",
"{path} will be deleted. Git does not track it, so it cannot be restored.",
),
("tree.discard.go", "Discard"),
("tree.error.no_git", "Git is not available: {error}"),
("tree.error.open", "{path} could not be opened: {error}"),
(
"tree.error.recent_gone",
"{path} could not be opened and left the recent list: {error}",
),
("tree.error.recent", "The recent list: {error}"),
(
"tree.error.watch",
"Changes made outside Ely are not followed: {error}",
),
("tree.error.write", "Git failed: {error}"),
("tree.error.clone", "The clone failed: {error}"),
("tree.error.stop", "The clone did not stop: {error}"),
("tree.error.panel", "The folder panel failed: {error}"),
("tree.space.exact", "Exact"),
("tree.space.changes", "Ignore Space Changes"),
("tree.space.all", "Ignore All Space"),
(
"tree.space.note",
"Hunks and lines stage with exact whitespace.",
),
("tree.layout.unified", "Unified"),
("tree.layout.split", "Side by Side"),
("tree.lines.stage", "Stage {n} Lines"),
("tree.lines.unstage", "Unstage {n} Lines"),
("tree.hunk.stage", "Stage Hunk"),
("tree.hunk.unstage", "Unstage Hunk"),
("tree.open_file", "Open File"),
("tree.whole", "This file stages as a whole."),
("tree.diff.unread", "This diff could not be read"),
("tree.diff.none", "No changes to show"),
("tree.binary", "Binary file"),
("tree.folder", "Untracked folder"),
("tree.conflicted", "This file has conflicts"),
("tree.side.changes", "Changes"),
("tree.side.history", "History"),
("tree.commit.subject", "Summary"),
("tree.commit.body", "Description"),
("tree.commit.failed", "The commit did not go in"),
("tree.commit.pick", "Pick a commit"),
("tree.commit.pick_file", "Pick a file"),
("tree.commit.unread", "This commit could not be read"),
("tree.commit.author", "Author"),
("tree.commit.committer", "Committer"),
("tree.commit.id", "Commit"),
("tree.commit.parents", "Parents"),
("tree.commit.signature", "Signature"),
("tree.commit.refs", "Refs"),
("tree.sig.good", "Good signature"),
("tree.sig.bad", "Bad signature"),
("tree.sig.untrusted", "Good signature, unknown validity"),
("tree.sig.expired", "Good signature, expired"),
("tree.sig.expired_key", "Good signature, expired key"),
("tree.sig.revoked", "Good signature, revoked key"),
("tree.sig.unchecked", "Signature cannot be checked"),
("tree.sig.none", "Not signed"),
("tree.me.unset", "Identity not set"),
("tree.me.unread", "Identity unreadable: {error}"),
("tree.me.signs", "Signs with {format}"),
("tree.identity.title", "Commit identity"),
("tree.identity.name", "Name"),
("tree.identity.email", "Email"),
("tree.identity.global", "For all repositories"),
("tree.identity.save", "Save"),
("tree.identity.empty", "Enter a name and an email."),
("tree.cancel", "Cancel"),
("tree.ok", "OK"),
(
"tree.error.amend",
"The last commit could not be read: {error}",
),
("tree.search", "Search commits"),
("tree.find", "Search in"),
("tree.find.message", "Message"),
("tree.find.author", "Author"),
("tree.find.hash", "Commit ID"),
("tree.find.content", "Changed text"),
("tree.log.none", "No commits yet"),
("tree.log.none_found", "No commits match"),
("tree.log.unread", "History could not be read"),
("tree.file_log", "File History"),
("tree.file_log.of", "History of {path}"),
("tree.blame", "Blame"),
("tree.blame.of", "Blame of {path}"),
("tree.blame.uncommitted", "Not committed yet"),
("tree.close_look", "Close"),
("tree.look.unread", "This could not be read"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("tree.menu.repository", "仓库"),
("tree.open", "打开仓库…"),
("tree.new", "新建仓库…"),
("tree.clone", "克隆…"),
("tree.close", "关闭仓库"),
("tree.empty", "打开一个仓库"),
("tree.cloning", "正在克隆到 {name}"),
("tree.clone.title", "克隆仓库"),
("tree.clone.url", "地址"),
("tree.clone.next", "选择文件夹…"),
("tree.clone.empty", "请输入地址"),
("tree.clone.stop", "停止"),
("tree.clone.stopped", "克隆已停止。"),
("tree.clone.busy", "已有克隆正在进行。"),
("tree.clean", "没有要提交的改动"),
("tree.changed", "{n} 个文件有改动"),
("tree.no_git", "Git 不可用"),
("tree.unreadable", "无法读取此仓库"),
("tree.branch.detached", "游离于 {oid}"),
("tree.branch.unborn", "{branch},尚无提交"),
("tree.ahead_behind", "{upstream} ↑{ahead} ↓{behind}"),
("tree.upstream_gone", "{upstream} 已不存在"),
("tree.git.bundled", "{version},随附,{path}"),
("tree.git.system", "{version},系统,{path}"),
("tree.busy", "请等此仓库的改动完成。"),
("tree.gone", "{path} 不在磁盘上。"),
("tree.discard.title", "放弃 {path} 的改动?"),
(
"tree.discard.tracked",
"文件回到已暂存或已提交的内容。此操作无法撤销。",
),
(
"tree.discard.untracked",
"{path} 将被删除。Git 未跟踪它,无法恢复。",
),
("tree.discard.go", "放弃改动"),
("tree.error.no_git", "Git 不可用:{error}"),
("tree.error.open", "无法打开 {path}:{error}"),
(
"tree.error.recent_gone",
"无法打开 {path},已移出最近列表:{error}",
),
("tree.error.recent", "最近列表:{error}"),
("tree.error.watch", "不跟随 Ely 以外的改动:{error}"),
("tree.error.write", "Git 失败:{error}"),
("tree.error.clone", "克隆失败:{error}"),
("tree.error.stop", "克隆未能停止:{error}"),
("tree.error.panel", "文件夹面板失败:{error}"),
("tree.space.exact", "精确"),
("tree.space.changes", "忽略空白变化"),
("tree.space.all", "忽略全部空白"),
("tree.space.note", "按块和按行暂存需精确比较空白。"),
("tree.layout.unified", "行内"),
("tree.layout.split", "并排"),
("tree.lines.stage", "暂存 {n} 行"),
("tree.lines.unstage", "取消暂存 {n} 行"),
("tree.hunk.stage", "暂存此块"),
("tree.hunk.unstage", "取消暂存此块"),
("tree.open_file", "打开文件"),
("tree.whole", "此文件只能整体暂存。"),
("tree.diff.unread", "无法读取此差异"),
("tree.diff.none", "没有可显示的改动"),
("tree.binary", "二进制文件"),
("tree.folder", "未跟踪的文件夹"),
("tree.conflicted", "此文件有冲突"),
("tree.side.changes", "改动"),
("tree.side.history", "历史"),
("tree.commit.subject", "摘要"),
("tree.commit.body", "说明"),
("tree.commit.failed", "提交未完成"),
("tree.commit.pick", "选择一个提交"),
("tree.commit.pick_file", "选择一个文件"),
("tree.commit.unread", "无法读取此提交"),
("tree.commit.author", "作者"),
("tree.commit.committer", "提交者"),
("tree.commit.id", "提交"),
("tree.commit.parents", "父提交"),
("tree.commit.signature", "签名"),
("tree.commit.refs", "引用"),
("tree.sig.good", "签名有效"),
("tree.sig.bad", "签名无效"),
("tree.sig.untrusted", "签名有效,信任未知"),
("tree.sig.expired", "签名有效,已过期"),
("tree.sig.expired_key", "签名有效,密钥已过期"),
("tree.sig.revoked", "签名有效,密钥已吊销"),
("tree.sig.unchecked", "无法核验签名"),
("tree.sig.none", "未签名"),
("tree.me.unset", "未设置身份"),
("tree.me.unread", "无法读取身份:{error}"),
("tree.me.signs", "以 {format} 签名"),
("tree.identity.title", "提交身份"),
("tree.identity.name", "姓名"),
("tree.identity.email", "邮箱"),
("tree.identity.global", "用于所有仓库"),
("tree.identity.save", "保存"),
("tree.identity.empty", "请输入姓名和邮箱。"),
("tree.cancel", "取消"),
("tree.ok", "好"),
("tree.error.amend", "无法读取最近一次提交:{error}"),
("tree.search", "搜索提交"),
("tree.find", "搜索范围"),
("tree.find.message", "提交信息"),
("tree.find.author", "作者"),
("tree.find.hash", "提交号"),
("tree.find.content", "改动的文字"),
("tree.log.none", "尚无提交"),
("tree.log.none_found", "没有匹配的提交"),
("tree.log.unread", "无法读取历史"),
("tree.file_log", "文件历史"),
("tree.file_log.of", "{path} 的历史"),
("tree.blame", "逐行追溯"),
("tree.blame.of", "{path} 的逐行追溯"),
("tree.blame.uncommitted", "尚未提交"),
("tree.close_look", "关闭"),
("tree.look.unread", "无法读取"),
];
+263
View File
@@ -0,0 +1,263 @@
//! ElyTree's branches, tags, merges and conflicts.
pub(super) const EN: &[(&str, &str)] = &[
("tree.side.branches", "Branches"),
("tree.refs.unread", "Branches could not be read"),
("tree.ref.pick", "Pick a branch or tag"),
("tree.ref.commit", "Commit"),
("tree.ref.when", "Date"),
("tree.ref.upstream", "Upstream"),
("tree.ref.message", "Message"),
("tree.ref.switch", "Switch To"),
("tree.ref.merge", "Merge Into Current"),
("tree.ref.rebase", "Rebase Current Onto"),
("tree.ref.branch_here", "New Branch Here…"),
("tree.ref.tag_here", "New Tag Here…"),
("tree.ref.rename", "Rename…"),
("tree.ref.delete", "Delete…"),
("tree.ref.force_delete", "Force Delete…"),
("tree.ref.delete_tag", "Delete Tag…"),
("tree.branch.new", "New Branch…"),
("tree.branch.title", "New branch"),
("tree.branch.name", "Name"),
("tree.branch.switch", "Switch to it"),
("tree.branch.create", "Create"),
("tree.tag.new", "New Tag…"),
("tree.tag.title", "New tag"),
("tree.tag.name", "Name"),
("tree.tag.message", "Message, for an annotated tag"),
("tree.tag.create", "Create"),
("tree.rename.title", "Rename {name}"),
("tree.rename.go", "Rename"),
("tree.name.empty", "Enter a name."),
("tree.commit.pick_it", "Cherry-Pick"),
("tree.commit.revert_it", "Revert"),
("tree.rebase.title", "Rebase onto {name}?"),
(
"tree.rebase.message",
"The current branch's commits are rewritten on top of {name}. Others who have them will see new commits.",
),
("tree.rebase.go", "Rebase"),
("tree.delete_branch.title", "Delete branch {name}?"),
(
"tree.delete_branch.message",
"Git refuses if {name} has commits no other branch holds.",
),
("tree.delete_branch.go", "Delete"),
("tree.force_delete.title", "Force delete branch {name}?"),
(
"tree.force_delete.message",
"{name} goes even if its commits are held nowhere else; they may be lost.",
),
("tree.force_delete.go", "Force Delete"),
("tree.delete_tag.title", "Delete tag {name}?"),
(
"tree.delete_tag.message",
"The tag goes from this repository; copies already pushed stay.",
),
("tree.delete_tag.go", "Delete"),
("tree.abort.title", "Abort the {name}?"),
(
"tree.abort.message",
"The work tree goes back to before the {name} began; resolutions made so far are lost.",
),
("tree.abort.go", "Abort"),
("tree.skip.title", "Skip this commit in the {name}?"),
(
"tree.skip.message",
"The commit being applied is left out, with any resolution made for it.",
),
("tree.skip.go", "Skip"),
("tree.op.merging", "Merging {theirs} into {ours}"),
(
"tree.op.rebasing",
"Rebasing onto {ours}, applying {theirs}",
),
("tree.op.picking", "Cherry-picking {theirs} onto {ours}"),
("tree.op.reverting", "Reverting {theirs} on {ours}"),
(
"tree.op.next",
"Resolve the conflicts and mark each file resolved, then continue.",
),
("tree.op.continue", "Continue"),
("tree.op.skip", "Skip…"),
("tree.op.abort", "Abort…"),
(
"tree.op.stopped",
"Stopped on conflicts. Resolve them, then continue.",
),
("tree.op.failed", "Git did not do it"),
("tree.ours", "Current"),
("tree.theirs", "Incoming"),
("tree.base", "Base"),
("tree.ours.merge", "Current branch, {name}"),
("tree.theirs.merge", "Merged in, {name}"),
("tree.ours.rebase", "New base, {name}"),
("tree.theirs.rebase", "Commit replayed, {name}"),
("tree.ours.pick", "Current, {name}"),
("tree.theirs.pick", "Picked, {name}"),
("tree.ours.revert", "Current, {name}"),
("tree.theirs.revert", "Reverting, {name}"),
("tree.conflict.save", "Save"),
("tree.conflict.resolved", "Mark Resolved"),
("tree.conflict.use_ours", "Use Current File"),
("tree.conflict.use_theirs", "Use Incoming File"),
("tree.conflict.delete", "Resolve as Deleted…"),
(
"tree.conflict.hand",
"The result was edited by hand; settle the rest in the result.",
),
("tree.conflict.unsaved", "Save the result first."),
(
"tree.conflict.kept",
"The file changed on disk; your unsaved result is kept.",
),
("tree.conflict.marked.title", "Mark {name} resolved?"),
(
"tree.conflict.marked.message",
"{name} still has conflict markers.",
),
("tree.conflict.marked.go", "Mark Resolved"),
("tree.conflict.delete.title", "Resolve {name} as deleted?"),
(
"tree.conflict.delete.message",
"{name} is removed from the work tree and the index.",
),
("tree.conflict.delete.go", "Delete"),
("tree.error.save", "The result was not saved: {error}"),
(
"tree.conflict.whole",
"This conflict is settled by whole file",
),
(
"tree.conflict.gone_ours",
"The current side removed this file",
),
(
"tree.conflict.gone_theirs",
"The incoming side removed this file",
),
("tree.opname.merge", "merge"),
("tree.opname.rebase", "rebase"),
("tree.opname.cherry-pick", "cherry-pick"),
("tree.opname.revert", "revert"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("tree.side.branches", "分支"),
("tree.refs.unread", "无法读取分支"),
("tree.ref.pick", "选择一个分支或标签"),
("tree.ref.commit", "提交"),
("tree.ref.when", "日期"),
("tree.ref.upstream", "上游"),
("tree.ref.message", "说明"),
("tree.ref.switch", "切换到此"),
("tree.ref.merge", "合并到当前分支"),
("tree.ref.rebase", "把当前分支变基到此"),
("tree.ref.branch_here", "在此新建分支…"),
("tree.ref.tag_here", "在此新建标签…"),
("tree.ref.rename", "重命名…"),
("tree.ref.delete", "删除…"),
("tree.ref.force_delete", "强制删除…"),
("tree.ref.delete_tag", "删除标签…"),
("tree.branch.new", "新建分支…"),
("tree.branch.title", "新建分支"),
("tree.branch.name", "名称"),
("tree.branch.switch", "并切换过去"),
("tree.branch.create", "新建"),
("tree.tag.new", "新建标签…"),
("tree.tag.title", "新建标签"),
("tree.tag.name", "名称"),
("tree.tag.message", "说明(附注标签)"),
("tree.tag.create", "新建"),
("tree.rename.title", "重命名 {name}"),
("tree.rename.go", "重命名"),
("tree.name.empty", "请输入名称。"),
("tree.commit.pick_it", "拣选"),
("tree.commit.revert_it", "撤销提交"),
("tree.rebase.title", "变基到 {name}?"),
(
"tree.rebase.message",
"当前分支的提交会在 {name} 之上重写;已拿到它们的人会看到新的提交。",
),
("tree.rebase.go", "变基"),
("tree.delete_branch.title", "删除分支 {name}?"),
(
"tree.delete_branch.message",
"若 {name} 有别的分支都没有的提交,Git 会拒绝。",
),
("tree.delete_branch.go", "删除"),
("tree.force_delete.title", "强制删除分支 {name}?"),
(
"tree.force_delete.message",
"即使 {name} 的提交别处没有也照样删除,这些提交可能丢失。",
),
("tree.force_delete.go", "强制删除"),
("tree.delete_tag.title", "删除标签 {name}?"),
(
"tree.delete_tag.message",
"只从本仓库删除,已推送出去的不受影响。",
),
("tree.delete_tag.go", "删除"),
("tree.abort.title", "中止 {name}?"),
(
"tree.abort.message",
"工作区回到 {name} 开始之前,已做的冲突解决会丢失。",
),
("tree.abort.go", "中止"),
("tree.skip.title", "跳过 {name} 中的这个提交?"),
(
"tree.skip.message",
"正在应用的提交被略过,为它做的冲突解决一并丢弃。",
),
("tree.skip.go", "跳过"),
("tree.op.merging", "正在把 {theirs} 合并到 {ours}"),
("tree.op.rebasing", "正在变基到 {ours},应用 {theirs}"),
("tree.op.picking", "正在把 {theirs} 拣选到 {ours}"),
("tree.op.reverting", "正在 {ours} 上撤销 {theirs}"),
("tree.op.next", "解决冲突并把每个文件标为已解决,然后继续。"),
("tree.op.continue", "继续"),
("tree.op.skip", "跳过…"),
("tree.op.abort", "中止…"),
("tree.op.stopped", "因冲突停下。解决后继续。"),
("tree.op.failed", "Git 未能完成"),
("tree.ours", "当前"),
("tree.theirs", "传入"),
("tree.base", "共同祖先"),
("tree.ours.merge", "当前分支 {name}"),
("tree.theirs.merge", "合并进来的 {name}"),
("tree.ours.rebase", "新的基底 {name}"),
("tree.theirs.rebase", "正在重放的提交 {name}"),
("tree.ours.pick", "当前 {name}"),
("tree.theirs.pick", "拣选的 {name}"),
("tree.ours.revert", "当前 {name}"),
("tree.theirs.revert", "撤销的 {name}"),
("tree.conflict.save", "保存"),
("tree.conflict.resolved", "标为已解决"),
("tree.conflict.use_ours", "用当前侧的文件"),
("tree.conflict.use_theirs", "用传入侧的文件"),
("tree.conflict.delete", "以删除解决…"),
(
"tree.conflict.hand",
"结果已手动改过,其余冲突请在结果里处理。",
),
("tree.conflict.unsaved", "请先保存结果。"),
("tree.conflict.kept", "文件在磁盘上变了,未保存的结果保留。"),
("tree.conflict.marked.title", "把 {name} 标为已解决?"),
("tree.conflict.marked.message", "{name} 里还有冲突标记。"),
("tree.conflict.marked.go", "标为已解决"),
("tree.conflict.delete.title", "以删除解决 {name}?"),
(
"tree.conflict.delete.message",
"{name} 会从工作区和索引中删除。",
),
("tree.conflict.delete.go", "删除"),
("tree.error.save", "结果未保存:{error}"),
("tree.conflict.whole", "此冲突按整个文件解决"),
("tree.conflict.gone_ours", "当前侧删除了此文件"),
("tree.conflict.gone_theirs", "传入侧删除了此文件"),
("tree.opname.merge", "合并"),
("tree.opname.rebase", "变基"),
("tree.opname.cherry-pick", "拣选"),
("tree.opname.revert", "撤销"),
];
+287
View File
@@ -0,0 +1,287 @@
//! ElyTree's stashes, remotes, worktrees, submodules, LFS.
pub(super) const EN: &[(&str, &str)] = &[
("tree.side.places", "Repository"),
("tree.places.stashes", "Stashes"),
("tree.places.remotes", "Remotes"),
("tree.places.worktrees", "Worktrees"),
("tree.places.submodules", "Submodules"),
("tree.places.lfs", "LFS Files"),
(
"tree.places.pick",
"Pick a stash, remote, worktree or submodule",
),
("tree.net.fetch", "Fetch"),
("tree.net.pull", "Pull"),
("tree.net.push", "Push"),
("tree.net.running.fetch", "Fetching…"),
("tree.net.running.pull", "Pulling…"),
("tree.net.running.push", "Pushing…"),
("tree.net.running.submodule", "Updating the submodule…"),
("tree.net.stop", "Stop"),
("tree.net.stopped", "Stopped"),
(
"tree.push.detached",
"HEAD is not on a branch; nothing to push",
),
(
"tree.push.no_remote",
"This repository has no remote; add one first",
),
("tree.push.where", "Push {branch} to which remote?"),
("tree.remotes.unread", "Remotes could not be read"),
("tree.force.open", "Force Push…"),
("tree.force.title", "Force push?"),
("tree.force.go", "Force Push"),
("tree.force.unready", "A forced push needs an upstream"),
(
"tree.force.message",
"{branch} goes to {target} on {remote}, refused if the remote moved since the last fetch.\n\n{lost}",
),
(
"tree.force.nothing",
"The remote holds nothing this branch lacks.",
),
(
"tree.force.lost",
"These {n} commits on the remote would be overwritten:\n{commits}",
),
("tree.stash.open", "Stash Changes…"),
("tree.stash.title", "Stash changes"),
("tree.stash.message", "Message"),
("tree.stash.untracked", "Include untracked files"),
("tree.stash.go", "Stash"),
("tree.stash.when", "Stashed"),
("tree.stash.apply", "Apply"),
("tree.stash.pop", "Pop"),
("tree.stash.drop", "Drop…"),
("tree.stash.drop.title", "Drop {name}?"),
(
"tree.stash.drop.message",
"{name} and its changes are gone for good.",
),
("tree.stash.drop.go", "Drop"),
("tree.remote.open", "Add Remote…"),
("tree.remote.add", "Add a remote"),
("tree.remote.edit", "Change {name}"),
("tree.remote.name", "Name"),
("tree.remote.url", "URL"),
("tree.remote.push_url", "Push URL"),
("tree.remote.same", "Same as the URL"),
("tree.remote.save", "Save"),
("tree.remote.change", "Change…"),
("tree.remote.delete", "Remove…"),
("tree.remote.empty", "A remote needs a name and a URL"),
("tree.remote.remove.title", "Remove the remote {name}?"),
(
"tree.remote.remove.message",
"Its remote branches go too. The remote itself is not touched.",
),
("tree.remote.remove.go", "Remove"),
("tree.worktree.open", "Add Worktree…"),
("tree.worktree.title", "A worktree at {path}"),
("tree.worktree.branch", "Branch"),
("tree.worktree.new", "Make it a new branch"),
("tree.worktree.go", "Add"),
("tree.worktree.prune", "Prune"),
("tree.worktree.prunable", "Folder gone"),
("tree.worktree.bare", "Bare"),
("tree.worktree.on", "Branch"),
("tree.worktree.locked", "Locked"),
("tree.worktree.open_it", "Open"),
("tree.worktree.delete", "Remove…"),
("tree.worktree.remove.title", "Remove the worktree {name}?"),
(
"tree.worktree.remove.message",
"Its folder is deleted. Git refuses if it holds changes.",
),
("tree.worktree.remove.go", "Remove"),
("tree.submodule.conflicted", "Conflicted"),
("tree.submodule.missing", "Not checked out"),
("tree.submodule.matches", "At the recorded commit"),
("tree.submodule.differs", "At another commit"),
("tree.submodule.unread", "Could not be read"),
("tree.submodule.recorded", "Recorded"),
("tree.submodule.held", "Checked out"),
("tree.submodule.update", "Check Out Recorded Commit"),
("tree.submodule.open", "Open"),
("tree.lfs.here", "In the work tree"),
("tree.lfs.stored", "Object here, pointer in the work tree"),
("tree.lfs.pointer", "Pointer only; the object is not here"),
("tree.upstream.set", "Set Upstream…"),
("tree.upstream.unset", "Unset Upstream"),
(
"tree.upstream.title",
"Which remote branch does {branch} track?",
),
("tree.upstream.none", "No remote branches; fetch first"),
("tree.places.reflog", "Reflog"),
("tree.reflog.where", "Position"),
("tree.reflog.when", "Moved"),
("tree.move.open", "Move Current Branch Here…"),
("tree.move.title", "Move the branch back?"),
("tree.move.go", "Move"),
("tree.move.detached", "Detached HEAD"),
(
"tree.move.message",
"{what} moves from {from} to {to}. Changes in the work tree and index stay; Git refuses if they collide.\n\n{left}",
),
("tree.move.nothing", "No commit leaves the branch."),
(
"tree.move.left",
"These {n} commits leave it; the reflog still finds them:\n{commits}",
),
("tree.clean.open", "Clean Untracked Files…"),
("tree.clean.title", "Clean untracked files"),
("tree.clean.ignored", "Include ignored files"),
("tree.clean.reading", "Asking Git what it would remove…"),
("tree.clean.nothing", "Nothing to remove."),
("tree.clean.go", "Delete {n}"),
("tree.locked.title", "Another Git is at work"),
(
"tree.locked.message",
"A lock file is in place. Wait for the other Git to finish, then retry. Ely never deletes lock files.",
),
("tree.locked.retry", "Retry"),
("tree.tool.terminal", "Open in Terminal"),
("tree.tool.reveal", "Show in Finder"),
("tree.tool.editor", "Choose Editor…"),
("tree.tool.in_editor", "Open in Editor"),
("tree.tool.in_mark", "Open in ElyMark"),
("tree.error.tool", "Could not open it: {error}"),
(
"tree.error.tools_file",
"The editor setting could not be read: {error}",
),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("tree.side.places", "仓库"),
("tree.places.stashes", "储藏"),
("tree.places.remotes", "远程"),
("tree.places.worktrees", "工作树"),
("tree.places.submodules", "子模块"),
("tree.places.lfs", "LFS 文件"),
("tree.places.pick", "选一个储藏、远程、工作树或子模块"),
("tree.net.fetch", "取回"),
("tree.net.pull", "拉取"),
("tree.net.push", "推送"),
("tree.net.running.fetch", "正在取回…"),
("tree.net.running.pull", "正在拉取…"),
("tree.net.running.push", "正在推送…"),
("tree.net.running.submodule", "正在更新子模块…"),
("tree.net.stop", "停止"),
("tree.net.stopped", "已停止"),
("tree.push.detached", "HEAD 不在分支上,没有可推送的"),
("tree.push.no_remote", "此仓库没有远程,请先添加"),
("tree.push.where", "把 {branch} 推送到哪个远程?"),
("tree.remotes.unread", "读不出远程"),
("tree.force.open", "强制推送…"),
("tree.force.title", "强制推送?"),
("tree.force.go", "强制推送"),
("tree.force.unready", "强制推送需要上游分支"),
(
"tree.force.message",
"{branch} 推到 {remote} 的 {target};远端自上次取回后有变动即拒绝。\n\n{lost}",
),
("tree.force.nothing", "远端没有本分支缺少的提交。"),
("tree.force.lost", "远端这 {n} 个提交会被覆盖:\n{commits}"),
("tree.stash.open", "储藏改动…"),
("tree.stash.title", "储藏改动"),
("tree.stash.message", "说明"),
("tree.stash.untracked", "包括未跟踪的文件"),
("tree.stash.go", "储藏"),
("tree.stash.when", "储藏于"),
("tree.stash.apply", "应用"),
("tree.stash.pop", "弹出"),
("tree.stash.drop", "删除…"),
("tree.stash.drop.title", "删除 {name}?"),
("tree.stash.drop.message", "{name} 与其中的改动将永久删除。"),
("tree.stash.drop.go", "删除"),
("tree.remote.open", "添加远程…"),
("tree.remote.add", "添加远程"),
("tree.remote.edit", "修改 {name}"),
("tree.remote.name", "名称"),
("tree.remote.url", "地址"),
("tree.remote.push_url", "推送地址"),
("tree.remote.same", "同地址"),
("tree.remote.save", "保存"),
("tree.remote.change", "修改…"),
("tree.remote.delete", "移除…"),
("tree.remote.empty", "远程需要名称与地址"),
("tree.remote.remove.title", "移除远程 {name}?"),
(
"tree.remote.remove.message",
"它的远程分支一并移除,远端本身不动。",
),
("tree.remote.remove.go", "移除"),
("tree.worktree.open", "添加工作树…"),
("tree.worktree.title", "在 {path} 添加工作树"),
("tree.worktree.branch", "分支"),
("tree.worktree.new", "作为新分支"),
("tree.worktree.go", "添加"),
("tree.worktree.prune", "清理"),
("tree.worktree.prunable", "文件夹已不在"),
("tree.worktree.bare", "裸仓库"),
("tree.worktree.on", "分支"),
("tree.worktree.locked", "已锁定"),
("tree.worktree.open_it", "打开"),
("tree.worktree.delete", "移除…"),
("tree.worktree.remove.title", "移除工作树 {name}?"),
(
"tree.worktree.remove.message",
"其文件夹将被删除;有改动时 Git 会拒绝。",
),
("tree.worktree.remove.go", "移除"),
("tree.submodule.conflicted", "冲突"),
("tree.submodule.missing", "未检出"),
("tree.submodule.matches", "在记录的提交上"),
("tree.submodule.differs", "在别的提交上"),
("tree.submodule.unread", "读不出"),
("tree.submodule.recorded", "记录的提交"),
("tree.submodule.held", "检出的提交"),
("tree.submodule.update", "检出记录的提交"),
("tree.submodule.open", "打开"),
("tree.lfs.here", "工作树里是内容"),
("tree.lfs.stored", "对象在本机,工作树里是指针"),
("tree.lfs.pointer", "只有指针,对象不在本机"),
("tree.upstream.set", "设置上游…"),
("tree.upstream.unset", "取消上游"),
("tree.upstream.title", "{branch} 跟踪哪个远程分支?"),
("tree.upstream.none", "没有远程分支,请先取回"),
("tree.places.reflog", "引用日志"),
("tree.reflog.where", "位置"),
("tree.reflog.when", "移动于"),
("tree.move.open", "把当前分支移到此…"),
("tree.move.title", "移回分支?"),
("tree.move.go", "移动"),
("tree.move.detached", "游离的 HEAD"),
(
"tree.move.message",
"{what} 从 {from} 移到 {to}。工作区与暂存的改动保留;与之冲突时 Git 拒绝。\n\n{left}",
),
("tree.move.nothing", "没有提交离开该分支。"),
(
"tree.move.left",
"这 {n} 个提交将离开该分支,引用日志里仍可找回:\n{commits}",
),
("tree.clean.open", "清理未跟踪的文件…"),
("tree.clean.title", "清理未跟踪的文件"),
("tree.clean.ignored", "包括被忽略的文件"),
("tree.clean.reading", "正在问 Git 会删除什么…"),
("tree.clean.nothing", "没有要删除的。"),
("tree.clean.go", "删除 {n} 项"),
("tree.locked.title", "另一个 Git 正在工作"),
(
"tree.locked.message",
"锁文件存在。等另一个 Git 结束后重试。Ely 从不删除锁文件。",
),
("tree.locked.retry", "重试"),
("tree.tool.terminal", "在终端中打开"),
("tree.tool.reveal", "在访达中显示"),
("tree.tool.editor", "选择编辑器…"),
("tree.tool.in_editor", "在编辑器中打开"),
("tree.tool.in_mark", "在 ElyMark 中打开"),
("tree.error.tool", "打不开:{error}"),
("tree.error.tools_file", "读不出编辑器设置:{error}"),
];
+11
View File
@@ -0,0 +1,11 @@
//! Words for bundled web pages.
pub(super) const EN: &[(&str, &str)] = &[
("web.failed", "The page could not show"),
("web.reload", "Reload"),
];
pub(super) const ZH_CN: &[(&str, &str)] = &[
("web.failed", "页面无法显示"),
("web.reload", "重新载入"),
];
+13
View File
@@ -0,0 +1,13 @@
pub mod commands;
pub mod i18n;
mod opening;
mod run;
pub mod settings;
mod shell;
pub mod tasks;
mod theme;
pub use opening::{launch_files, on_open};
pub use run::run;
pub use shell::{Root, Shell, blank, document_root, root, set_document_menus, tool_root};
pub use tasks::{LongTask, TaskState};
+108
View File
@@ -0,0 +1,108 @@
//! Files the system hands the app: arguments, Finder.
use std::path::PathBuf;
use futures::{StreamExt, channel::mpsc};
use gpui::{App, Application, Global};
struct Asked(Option<mpsc::UnboundedReceiver<PathBuf>>);
impl Global for Asked {}
/// Queues launch arguments and every later open request.
pub(crate) fn listen(application: &Application) -> mpsc::UnboundedReceiver<PathBuf> {
let (sender, asked) = mpsc::unbounded();
// Relative to where the app was started.
for path in std::env::args_os().skip(1) {
match std::path::absolute(&path) {
Ok(path) => {
sender.unbounded_send(path).ok();
}
Err(error) => log::error!("run: a launch argument has no place: {error}"),
}
}
application.on_open_urls(move |urls| {
for url in urls {
match file_path(&url) {
Some(path) => {
log::info!("run: the system asks to open a file");
sender.unbounded_send(path).ok();
}
None => log::warn!("run: not a file URL, ignored"),
}
}
});
asked
}
pub(crate) fn keep(asked: mpsc::UnboundedReceiver<PathBuf>, cx: &mut App) {
cx.set_global(Asked(Some(asked)));
}
/// Files asked before the app started: arguments and Finder's.
pub fn launch_files(cx: &mut App) -> Vec<PathBuf> {
let asked = cx.global_mut::<Asked>().0.as_mut();
let asked = asked.expect("launch_files comes before on_open");
std::iter::from_fn(|| asked.try_recv().ok()).collect()
}
/// Hands each file asked from now on to `open`.
pub fn on_open(cx: &mut App, mut open: impl FnMut(PathBuf, &mut App) + 'static) {
let mut asked = cx
.global_mut::<Asked>()
.0
.take()
.expect("on_open runs once per app");
cx.spawn(async move |cx| {
while let Some(path) = asked.next().await {
cx.update(|cx| open(path, cx));
}
})
.detach();
}
/// A `file://` URL's path, percent escapes decoded.
fn file_path(url: &str) -> Option<PathBuf> {
let rest = url.strip_prefix("file://")?;
let rest = rest.strip_prefix("localhost").unwrap_or(rest);
if !rest.starts_with('/') {
return None;
}
let mut bytes = Vec::with_capacity(rest.len());
let mut raw = rest.bytes();
while let Some(byte) = raw.next() {
if byte != b'%' {
bytes.push(byte);
continue;
}
let hex = [raw.next()?, raw.next()?];
bytes.push(u8::from_str_radix(std::str::from_utf8(&hex).ok()?, 16).ok()?);
}
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
Some(PathBuf::from(std::ffi::OsString::from_vec(bytes)))
}
#[cfg(not(unix))]
{
// `file:///C:/a` holds `/C:/a`.
let text = String::from_utf8(bytes).ok()?;
Some(PathBuf::from(text.strip_prefix('/').unwrap_or(&text)))
}
}
#[cfg(all(test, unix))]
mod tests {
use super::file_path;
use std::path::PathBuf;
#[test]
fn file_urls_decode_and_others_do_not() {
assert_eq!(file_path("file:///Users/a/b%20c%23.md"), Some(PathBuf::from("/Users/a/b c#.md")));
assert_eq!(file_path("file://localhost/tmp/x"), Some(PathBuf::from("/tmp/x")));
assert_eq!(file_path("file:///a/%E4%BD%A0.md"), Some(PathBuf::from("/a/你.md")));
assert_eq!(file_path("https://example.com/a"), None);
assert_eq!(file_path("file:///a/%zz"), None);
assert_eq!(file_path("file:///a/%4"), None);
}
}
+163
View File
@@ -0,0 +1,163 @@
use std::time::Duration;
use ely_gpui_component::Assets;
use ely_gpui_component::theme::ActiveTheme;
use ely_platform::{AppId, env, graphics, logging, startup};
use gpui::{
App, Bounds, Entity, Render, TitlebarOptions, Window, WindowBounds, WindowDecorations,
WindowOptions, px, size,
};
use crate::theme;
/// Starts `app`; `ELY_SMOKE=1` draws a frame, `hold` stays up.
pub fn run<V: Render>(
app: AppId,
root: impl FnOnce(AppId, &mut Window, &mut App) -> Entity<V> + 'static,
) {
if let Err(error) = logging::init(app) {
startup::fail(app, &format!("logging did not start: {error:#}"));
}
if let Err(error) = graphics::check() {
startup::fail(app, &format!("{error:#}"));
}
startup::guard(app);
startup::no_window_restore();
let smoke = Smoke::from_env();
let application = gpui_platform::application().with_assets(Assets);
let asked = crate::opening::listen(&application);
application.run(move |cx: &mut App| {
crate::opening::keep(asked, cx);
open(app, smoke, root, cx)
});
}
#[derive(Clone, Copy, PartialEq)]
enum Smoke {
Off,
Once,
Hold,
}
impl Smoke {
fn from_env() -> Self {
Self::from_value(env::value("ELY_SMOKE").as_deref())
}
fn from_value(value: Option<&str>) -> Self {
match value {
None => Smoke::Off,
Some("1") => Smoke::Once,
Some("hold") => Smoke::Hold,
Some(other) => {
log::warn!("run: ELY_SMOKE={other} is not 1 or hold, ignored");
Smoke::Off
}
}
}
}
fn open<V: Render>(
app: AppId,
smoke: Smoke,
root: impl FnOnce(AppId, &mut Window, &mut App) -> Entity<V> + 'static,
cx: &mut App,
) {
if let Err(error) = ely_gpui_component::init(cx) {
startup::fail(app, &format!("the components did not start: {error:#}"));
}
crate::settings::install(app, cx);
// Clears asked for last time; nothing opens what stays.
if let Err(error) = ely_platform::storage::clear_marked(app) {
startup::fail(app, &format!("the data you asked to clear could not be deleted, so it is not opened; the next start tries again: {error}"));
}
theme::install(cx);
crate::i18n::install(cx);
crate::commands::bind_keys(cx);
cx.on_action(|_: &crate::commands::Quit, cx| cx.quit());
cx.on_action(move |_: &crate::commands::OpenSettings, cx| crate::settings::open(app, cx));
let mut options = WindowOptions {
focus: smoke == Smoke::Off,
inactive_frame_interval: (smoke != Smoke::Hold).then_some(Duration::from_millis(250)),
..window_options(app, size(px(1100.0), px(720.0)), cx)
};
if let Some(bounds) = crate::settings::restored_frame(app, cx) {
options.window_bounds = Some(bounds);
}
let opened = cx.open_window(options, |window, cx| {
window.on_next_frame(move |_, cx| {
startup::started();
if smoke == Smoke::Once {
println!("ready {}", app.name());
cx.quit();
}
});
theme::follow_system(window);
crate::settings::follow_frame(app, window, cx);
root(app, window, cx)
});
if let Err(error) = opened {
startup::fail(app, &format!("the window did not open: {error:#}"));
}
cx.on_window_closed(|cx, _| {
if cx.windows().is_empty() {
cx.quit();
}
})
.detach();
if smoke == Smoke::Off {
cx.activate(true);
}
// Benchmarks only: idle apps do nothing (PERF-02).
if ely_platform::env::flag("ELY_STALLS") == Some(true) {
watch_stalls(cx);
}
log::info!("{} window open", app.name());
}
/// Logs each main-thread stretch busy past 100 ms.
fn watch_stalls(cx: &mut App) {
const BEAT: Duration = Duration::from_millis(20);
cx.spawn(async move |cx: &mut gpui::AsyncApp| {
loop {
let asked = std::time::Instant::now();
cx.background_executor().timer(BEAT).await;
let late = asked.elapsed().saturating_sub(BEAT);
if late > Duration::from_millis(100) {
log::warn!("ui: the main thread was busy {} ms", late.as_millis());
}
}
})
.detach();
}
/// A window of `app`, `size` large, with Ely's title bar.
pub(crate) fn window_options(app: AppId, size: gpui::Size<gpui::Pixels>, cx: &App) -> WindowOptions {
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(None, size, cx))),
titlebar: Some(TitlebarOptions {
title: Some(app.name().into()),
appears_transparent: true,
traffic_light_position: Some(cx.theme().traffic_light_origin()),
}),
window_decorations: Some(WindowDecorations::Client),
window_min_size: Some(gpui::size(px(640.0), px(420.0))),
app_id: Some(app.slug().into()),
inactive_frame_interval: Some(Duration::from_millis(250)),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::Smoke;
#[test]
fn only_one_and_hold_start_a_smoke_run() {
assert!(Smoke::from_value(Some("1")) == Smoke::Once);
assert!(Smoke::from_value(Some("hold")) == Smoke::Hold);
for other in [None, Some("0"), Some(""), Some("true")] {
assert!(Smoke::from_value(other) == Smoke::Off, "{other:?}");
}
}
}
+92
View File
@@ -0,0 +1,92 @@
//! Saving ordinary files on their own, kept per app (§6.2).
use std::io;
use ely_platform::{AppId, settings::Store};
use gpui::{App, AsyncApp, Global};
use serde_json::Value;
const KEY: &str = "autosave";
/// Whether autosave is on, and the latest switch.
struct Autosave {
store: Option<Store>,
on: bool,
turns: u64,
}
impl Global for Autosave {}
/// Reads `app`'s choice; a bad entry is reported and off.
pub(super) fn install(app: AppId, cx: &mut App) {
let store = Store::app(app);
let read = store
.as_ref()
.map_err(|error| io::Error::new(error.kind(), error.to_string()))
.and_then(|store| parse(store.read()?.get(KEY)));
let on = read.unwrap_or_else(|error| {
log::error!("settings: autosave unreadable: {error}");
cx.global_mut::<super::Current>().app_error.insert(super::AUTOSAVE, error.to_string().into());
false
});
cx.set_global(Autosave { store: store.ok(), on, turns: 0 });
}
/// Absent is off; else a boolean.
fn parse(saved: Option<&Value>) -> io::Result<bool> {
match saved {
None => Ok(false),
Some(Value::Bool(on)) => Ok(*on),
Some(other) => Err(io::Error::new(io::ErrorKind::InvalidData, format!("settings: autosave cannot be {other}"))),
}
}
/// Off without reading the file: for tests.
#[cfg(any(test, feature = "test-support"))]
pub(super) fn install_defaults(cx: &mut App) {
cx.set_global(Autosave { store: None, on: false, turns: 0 });
}
/// Whether ordinary files save on their own.
pub fn on(cx: &App) -> bool {
cx.global::<Autosave>().on
}
/// Takes the switch now; the latest one is written.
pub(super) fn set(on: bool, cx: &mut App) {
let autosave = cx.global_mut::<Autosave>();
autosave.on = on;
autosave.turns += 1;
let turn = autosave.turns;
let Some(store) = autosave.store.clone() else {
return super::unsaved(super::AUTOSAVE, Some("no settings file for this app".into()), cx);
};
cx.spawn(async move |cx: &mut AsyncApp| {
let latest = cx.update(|cx| cx.global::<Autosave>().turns == turn);
if !latest {
return;
}
let written = cx.background_executor().spawn(async move { store.set(KEY, Value::Bool(on)) }).await;
cx.update(|cx| {
super::unsaved(super::AUTOSAVE, written.err().map(|error| {
log::error!("settings: autosave not saved: {error}");
error.to_string().into()
}), cx)
});
})
.detach();
}
#[cfg(test)]
mod tests {
use serde_json::json;
#[test]
fn the_switch_reads_strictly() {
assert!(!super::parse(None).unwrap());
assert!(super::parse(Some(&json!(true))).unwrap());
for bad in [json!("yes"), json!(1), json!(null)] {
assert!(super::parse(Some(&bad)).is_err(), "{bad}");
}
}
}
+153
View File
@@ -0,0 +1,153 @@
//! Diagnostics export: what goes in is shown first (§18.2).
use ely_gpui_component::{
buttons::Button,
feedback::Banner,
forms::Switch,
primitives::Severity,
settings::{SettingsRow, SettingsSection},
theme::{ActiveTheme as _, TextSize},
};
use ely_platform::diagnostics::{self, Part, Redact};
use gpui::{
AnyElement, App, Context, InteractiveElement as _, IntoElement, ParentElement as _, Role, SharedString,
StatefulInteractiveElement as _, Styled as _,
WeakEntity, div,
};
use super::window::SettingsView;
use crate::i18n::text;
/// What the bundle holds; how the last export went.
#[derive(Default)]
pub(crate) struct Diagnostics {
parts: Option<Result<Vec<Part>, SharedString>>,
/// Paths and names go out only when asked.
unredacted: bool,
status: Option<(Severity, SharedString)>,
}
impl SettingsView {
/// Lists the parts off the window thread.
pub(super) fn read_diagnostics(&mut self, cx: &mut Context<Self>) {
let app = self.app;
let read = cx.background_executor().spawn(async move { diagnostics::gather(app) });
cx.spawn(async move |this, cx| {
let parts = read.await.map_err(|error| SharedString::from(error.to_string()));
this.update(cx, |this, cx| {
this.diagnostics.parts = Some(parts);
cx.notify();
})
})
.detach();
}
pub(super) fn diagnostics_page(&self, this: WeakEntity<Self>, cx: &App) -> AnyElement {
let found = match &self.diagnostics.parts {
None => Vec::new(),
Some(Ok(parts)) if parts.is_empty() => vec![text(cx, "settings.diagnostics.none", &[])],
Some(Ok(parts)) => parts.iter().map(|part| line(part, cx)).collect(),
Some(Err(error)) => vec![text(cx, "settings.diagnostics.failed", &[("error", error)])],
};
let some = matches!(&self.diagnostics.parts, Some(Ok(parts)) if !parts.is_empty());
let (toggle, export) = (this.clone(), this);
let mut section = SettingsSection::new(text(cx, "settings.diagnostics", &[]))
.row(
SettingsRow::new(text(cx, "settings.diagnostics.included", &[]))
.control(
div()
.id("settings-parts")
.role(Role::List)
.aria_label(text(cx, "settings.diagnostics.included", &[]))
.flex()
.flex_col()
.text_size(cx.theme().text_size(TextSize::Sm))
.children(found.into_iter().enumerate().map(|(at, said)| {
div().id(("settings-part", at)).role(Role::ListItem).aria_label(said.clone()).child(said)
})),
),
)
.row(
SettingsRow::new(text(cx, "settings.diagnostics.redact", &[])).control(
Switch::new("settings-redact", !self.diagnostics.unredacted)
.name(text(cx, "settings.diagnostics.redact", &[]))
.on_change(move |on, _, cx| {
toggle
.update(cx, |this, cx| {
this.diagnostics.unredacted = !on;
cx.notify();
})
.ok();
}),
),
)
.row(
SettingsRow::new(text(cx, "settings.diagnostics.export", &[])).control(
Button::new("settings-export", text(cx, "settings.diagnostics.export", &[]))
.disabled(!some)
.on_click(move |_, _, cx| {
export.update(cx, |this, cx| this.export_diagnostics(cx)).ok();
}),
),
);
if let Some((severity, said)) = self.diagnostics.status.clone() {
section = section.row(Banner::new("settings-export-status", severity, said));
}
section.into_any_element()
}
/// Asks where, then writes the bundle in the background.
fn export_diagnostics(&mut self, cx: &mut Context<Self>) {
let Some(Ok(parts)) = self.diagnostics.parts.clone() else {
return;
};
let (app, redact) = (self.app, !self.diagnostics.unredacted);
let folder = directories::UserDirs::new()
.and_then(|dirs| dirs.document_dir().map(std::path::Path::to_path_buf))
.unwrap_or_else(std::env::temp_dir);
let name = format!("{}-diagnostics.txt", app.name());
let chosen = cx.prompt_for_new_path(&folder, Some(&name));
cx.spawn(async move |this, cx| {
let path = match chosen.await {
Ok(Ok(Some(path))) => path,
Ok(Ok(None)) => return log::info!("settings: diagnostics export cancelled"),
Ok(Err(error)) => return log::error!("settings: the save panel failed: {error}"),
Err(error) => return log::error!("settings: the save panel failed: {error}"),
};
let target = path.clone();
let written = cx
.background_executor()
.spawn(async move {
// Names that cannot be read stop the export.
let redact = redact.then(Redact::of_account).transpose()?;
let bundle = diagnostics::bundle(app, &parts, redact.as_ref())?;
std::fs::write(&target, bundle)
})
.await;
this.update(cx, |this, cx| {
let shown = path.display().to_string();
this.diagnostics.status = Some(match written {
Ok(()) => {
log::info!("settings: diagnostics exported, redacted {redact}");
(Severity::Success, text(cx, "settings.diagnostics.saved", &[("path", &shown)]))
}
Err(error) => (Severity::Danger, text(cx, "settings.diagnostics.failed", &[("error", &error.to_string())])),
});
cx.notify();
})
.ok();
})
.detach();
}
}
/// `ely.log · 120 KB` or a crash report's file name.
fn line(part: &Part, cx: &App) -> SharedString {
let file = part.path.file_name().map(|name| name.to_string_lossy().into_owned()).unwrap_or_default();
let name = match file.as_str() {
"ely.log" => text(cx, "settings.diagnostics.log", &[]).to_string(),
_ => text(cx, "settings.diagnostics.crash", &[("name", &file)]).to_string(),
};
let size = format!("{} KB", part.bytes.div_ceil(1024));
text(cx, "settings.diagnostics.size", &[("name", &name), ("size", &size)])
}
+144
View File
@@ -0,0 +1,144 @@
//! Each app's main window opens where it was left (§18.1).
use std::time::Duration;
use ely_platform::{AppId, settings::Store};
use gpui::{App, AppContext as _, Bounds, Context, Entity, Global, Pixels, Window, WindowBounds, point, px, size};
use serde_json::{Value, json};
const KEY: &str = "window";
const SETTLE: Duration = Duration::from_millis(500);
/// Where the window was last, if that place still shows.
pub fn restore(app: AppId, cx: &mut App) -> Option<WindowBounds> {
let unreadable = |why: String, cx: &mut App| {
log::error!("settings: the window place is unreadable: {why}");
cx.global_mut::<super::Current>().app_error.insert(super::WINDOW, why.into());
None
};
let saved = match Store::app(app).and_then(|store| store.read()) {
Ok(fields) => fields.get(KEY).cloned()?,
Err(error) => return unreadable(error.to_string(), cx),
};
let Some(bounds) = parse(&saved) else {
return unreadable(format!("the window place cannot be {saved}"), cx);
};
let center = bounds.get_bounds().center();
// A display since unplugged would hide the window.
cx.displays().iter().any(|display| display.bounds().contains(&center)).then_some(bounds)
}
fn parse(saved: &Value) -> Option<WindowBounds> {
let number = |key: &str| saved.get(key)?.as_f64().map(|n| px(n as f32));
let bounds = Bounds::new(point(number("x")?, number("y")?), size(number("width")?, number("height")?));
match saved.get("mode")?.as_str()? {
"windowed" => Some(WindowBounds::Windowed(bounds)),
"maximized" => Some(WindowBounds::Maximized(bounds)),
"fullscreen" => Some(WindowBounds::Fullscreen(bounds)),
_ => None,
}
}
fn written(bounds: WindowBounds) -> Value {
let (mode, frame): (&str, Bounds<Pixels>) = match bounds {
WindowBounds::Windowed(frame) => ("windowed", frame),
WindowBounds::Maximized(frame) => ("maximized", frame),
WindowBounds::Fullscreen(frame) => ("fullscreen", frame),
};
let n = |pixels: Pixels| f64::from(f32::from(pixels));
json!({
"mode": mode,
"x": n(frame.origin.x),
"y": n(frame.origin.y),
"width": n(frame.size.width),
"height": n(frame.size.height),
})
}
struct Frame {
app: AppId,
moves: u64,
saved: u64,
last: Option<WindowBounds>,
}
impl Frame {
/// Records a write; failures show and retry later.
fn settled(&mut self, moves: u64, written: std::io::Result<()>, cx: &mut App) {
match written {
Ok(()) => {
self.saved = moves;
super::unsaved(super::WINDOW, None, cx);
}
Err(error) => {
log::error!("settings: the window place is not saved: {error}");
super::unsaved(super::WINDOW, Some(error.to_string().into()), cx);
}
}
}
/// Writes a pending place now; quitting cannot wait.
fn flush(&mut self) {
let Some(bounds) = self.last.filter(|_| self.moves != self.saved) else {
return;
};
match Store::app(self.app).and_then(|store| store.set(KEY, written(bounds))) {
Ok(()) => self.saved = self.moves,
Err(error) => log::error!("settings: the window place is not saved at quit: {error}"),
}
}
}
/// The followed windows stay alive with the app.
struct Frames(Vec<Entity<Frame>>);
impl Global for Frames {}
/// Saves `window`'s place shortly after it stops changing.
pub fn follow(app: AppId, window: &mut Window, cx: &mut App) {
let frame = cx.new(|cx: &mut Context<Frame>| {
// Quitting right after a move still keeps the place.
cx.on_app_quit(|frame, _| {
frame.flush();
async {}
})
.detach();
cx.observe_window_bounds(window, |frame, window, cx| {
frame.moves += 1;
frame.last = Some(window.window_bounds());
let (moves, app, bounds) = (frame.moves, frame.app, window.window_bounds());
cx.spawn(async move |frame, cx| {
cx.background_executor().timer(SETTLE).await;
if frame.read_with(cx, |frame, _| frame.moves != moves).unwrap_or(true) {
return;
}
let saved = cx
.background_executor()
.spawn(async move { Store::app(app)?.set(KEY, written(bounds)) })
.await;
frame.update(cx, |frame, cx| frame.settled(moves, saved, cx)).ok();
})
.detach();
})
.detach();
Frame { app, moves: 0, saved: 0, last: None }
});
if !cx.has_global::<Frames>() {
cx.set_global(Frames(Vec::new()));
}
cx.global_mut::<Frames>().0.push(frame);
}
#[cfg(test)]
mod tests {
use gpui::{Bounds, WindowBounds, point, px, size};
#[test]
fn a_place_reads_back_as_written() {
let frame = Bounds::new(point(px(40.0), px(60.0)), size(px(900.0), px(600.0)));
for bounds in [WindowBounds::Windowed(frame), WindowBounds::Maximized(frame), WindowBounds::Fullscreen(frame)] {
assert_eq!(super::parse(&super::written(bounds)), Some(bounds));
}
assert_eq!(super::parse(&serde_json::json!({"mode": "tiled", "x": 0})), None);
}
}
+168
View File
@@ -0,0 +1,168 @@
//! The shared settings: read at start, followed while running (§18.1).
use std::{
collections::BTreeMap,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
time::Duration,
};
use ely_platform::settings::{Shared, Store};
use futures::{StreamExt, channel::mpsc};
use gpui::{App, AsyncApp, Global, SharedString};
use notify::{RecursiveMode, Watcher as _};
use serde_json::Value;
pub mod autosave;
mod diagnostics;
mod frame;
mod storage;
pub(crate) mod panels;
#[cfg(test)]
mod tests;
mod window;
pub use frame::{follow as follow_frame, restore as restored_frame};
pub use window::open;
const SETTLE: Duration = Duration::from_millis(150);
/// The settings in force, why reading failed, why saving failed.
pub struct Current {
pub shared: Shared,
pub error: Option<SharedString>,
/// Each save kind's last failure, cleared by its success.
pub unsaved: BTreeMap<&'static str, SharedString>,
/// Why each kind of app setting was unreadable.
pub app_error: BTreeMap<&'static str, SharedString>,
}
impl Global for Current {}
/// The settings every app follows.
pub fn current(cx: &App) -> &Shared {
&cx.global::<Current>().shared
}
/// Defaults in force without reading the file, for tests.
#[cfg(any(test, feature = "test-support"))]
pub fn install_defaults(cx: &mut App) {
cx.set_global(Current { shared: Shared::default(), error: None, unsaved: BTreeMap::new(), app_error: BTreeMap::new() });
panels::install_defaults(cx);
autosave::install_defaults(cx);
}
/// Reads the shared file and follows later writes to it.
pub fn install(app: ely_platform::AppId, cx: &mut App) {
let (shared, error) = read();
cx.set_global(Current { shared, error, unsaved: BTreeMap::new(), app_error: BTreeMap::new() });
panels::install(app, cx);
autosave::install(app, cx);
if let Err(error) = follow(cx) {
log::error!("settings: changes from other apps are not followed: {error}");
}
}
/// The file's settings; defaults with the reason when unreadable.
fn read() -> (Shared, Option<SharedString>) {
match Shared::load() {
Ok(shared) => (shared, None),
Err(error) => {
log::error!("settings: unreadable, defaults in force: {error}");
(Shared::default(), Some(error.to_string().into()))
}
}
}
fn follow(cx: &mut App) -> std::io::Result<()> {
let path = Store::shared()?.path().to_path_buf();
let dir = path.parent().expect("the settings folder").to_path_buf();
std::fs::create_dir_all(&dir)?;
let (sender, mut changes) = mpsc::unbounded();
let mut watcher = notify::recommended_watcher(move |event: notify::Result<notify::Event>| match event {
Ok(event) if event.paths.iter().any(|changed| changed.file_name() == path.file_name()) => {
let _ = sender.unbounded_send(());
}
Ok(_) => {}
Err(error) => log::warn!("settings: watching failed: {error}"),
})
.map_err(std::io::Error::other)?;
watcher.watch(&dir, RecursiveMode::NonRecursive).map_err(std::io::Error::other)?;
cx.spawn(async move |cx: &mut AsyncApp| {
let _watcher = watcher;
while changes.next().await.is_some() {
cx.background_executor().timer(SETTLE).await;
while changes.try_recv().is_ok() {}
let read = cx.background_executor().spawn(async { read() }).await;
cx.update(|cx| changed(read, cx));
}
})
.detach();
Ok(())
}
fn changed((shared, error): (Shared, Option<SharedString>), cx: &mut App) {
let current = cx.global::<Current>();
if current.shared == shared && current.error == error {
return;
}
log::info!("settings: changed on disk");
let (unsaved, app_error) = (current.unsaved.clone(), current.app_error.clone());
cx.set_global(Current { shared, error, unsaved, app_error });
crate::theme::settings_changed(cx);
cx.refresh_windows();
}
/// Save kinds, each with its own failure.
pub(crate) const SHARED: &str = "shared";
pub(crate) const PANELS: &str = "panels";
pub(crate) const WINDOW: &str = "window";
pub(crate) const AUTOSAVE: &str = "autosave";
/// Records and shows how `kind`'s last save went.
pub(crate) fn unsaved(kind: &'static str, failed: Option<SharedString>, cx: &mut App) {
let current = cx.global_mut::<Current>();
match failed {
Some(error) => {
current.unsaved.insert(kind, error);
}
// A good write also mends what could not be read.
None => {
current.unsaved.remove(kind);
current.app_error.remove(kind);
}
}
cx.refresh_windows();
}
/// Writes one shared field in the background; others stay.
pub fn set(key: &'static str, value: Value, cx: &mut App) {
saved(key, value, None, cx).detach();
}
/// A field's latest commit number, and this commit's.
pub(crate) type Turn = (Arc<AtomicU64>, u64);
/// As `set`; whether written, a later turn skipping it.
pub(crate) fn saved(key: &'static str, value: Value, turn: Option<Turn>, cx: &mut App) -> gpui::Task<bool> {
let write = cx.background_executor().spawn(async move {
let still = || turn.is_none_or(|(latest, asked)| latest.load(Ordering::SeqCst) == asked);
Store::shared()?.set_if(key, value, still)
});
cx.spawn(async move |cx: &mut AsyncApp| {
let written = write.await;
let read = cx.background_executor().spawn(async { read() }).await;
cx.update(|cx| {
changed(read, cx);
let ok = matches!(written, Ok(true));
// The reread must not hide this write's failure.
unsaved(SHARED, written.err().map(|error| {
log::error!("settings: {key} not saved: {error}");
error.to_string().into()
}), cx);
ok
})
})
}
+135
View File
@@ -0,0 +1,135 @@
//! Side panel widths, kept per app (§18.1).
use std::{io, time::Duration};
use ely_platform::{AppId, settings::Store};
use gpui::{App, AsyncApp, Global, Pixels, px};
use serde_json::{Value, json};
const KEY: &str = "panels";
const SETTLE: Duration = Duration::from_millis(500);
/// Widths the user dragged to; none keeps the default.
pub(crate) struct Panels {
store: Option<Store>,
left: Option<Pixels>,
right: Option<Pixels>,
moves: u64,
saved: u64,
}
impl Global for Panels {}
/// Reads `app`'s widths; a bad entry is reported, not guessed.
pub(super) fn install(app: AppId, cx: &mut App) {
let store = Store::app(app);
let read = store.as_ref().map_err(|error| io::Error::new(error.kind(), error.to_string())).and_then(|store| {
let fields = store.read()?;
parse(fields.get(KEY))
});
let (left, right) = read.unwrap_or_else(|error| {
log::error!("settings: panel widths unreadable: {error}");
cx.global_mut::<super::Current>().app_error.insert(super::PANELS, error.to_string().into());
(None, None)
});
cx.set_global(Panels { store: store.ok(), left, right, moves: 0, saved: 0 });
// Quitting right after a drag still keeps the width.
cx.on_app_quit(|cx| {
flush(cx);
async {}
})
.detach();
}
/// Absent or null is default; else a positive width.
fn parse(saved: Option<&Value>) -> io::Result<(Option<Pixels>, Option<Pixels>)> {
let invalid = |why: String| io::Error::new(io::ErrorKind::InvalidData, format!("settings: panels {why}"));
let fields = match saved {
None | Some(Value::Null) => return Ok((None, None)),
Some(Value::Object(fields)) => fields,
Some(other) => return Err(invalid(format!("cannot be {other}"))),
};
let width = |side: &str| match fields.get(side) {
None | Some(Value::Null) => Ok(None),
Some(Value::Number(n)) if n.as_f64().is_some_and(|n| n.is_finite() && n > 0.0) => {
Ok(n.as_f64().map(|n| px(n as f32)))
}
Some(other) => Err(invalid(format!("{side} cannot be {other}"))),
};
Ok((width("left")?, width("right")?))
}
/// Defaults in force, nothing kept: for tests.
#[cfg(any(test, feature = "test-support"))]
pub(super) fn install_defaults(cx: &mut App) {
cx.set_global(Panels { store: None, left: None, right: None, moves: 0, saved: 0 });
}
/// The user's width for the left or right panel.
pub(crate) fn width(left: bool, cx: &App) -> Option<Pixels> {
let panels = cx.global::<Panels>();
if left { panels.left } else { panels.right }
}
/// Takes a dragged width; saved once dragging rests.
pub(crate) fn set_width(left: bool, width: Pixels, cx: &mut App) {
let panels = cx.global_mut::<Panels>();
*(if left { &mut panels.left } else { &mut panels.right }) = Some(width);
panels.moves += 1;
let moves = panels.moves;
cx.spawn(async move |cx: &mut AsyncApp| {
cx.background_executor().timer(SETTLE).await;
let Some((store, saved)) = cx.update(|cx| pending(cx).filter(|_| cx.global::<Panels>().moves == moves)) else {
return;
};
let written = cx.background_executor().spawn(async move { store.set(KEY, saved) }).await;
cx.update(|cx| settled(moves, written, cx));
})
.detach();
}
/// What to write while the last drag is unkept.
fn pending(cx: &App) -> Option<(Store, Value)> {
let panels = cx.global::<Panels>();
let store = panels.store.clone().filter(|_| panels.moves != panels.saved)?;
let n = |w: Option<Pixels>| w.map(|w| f64::from(f32::from(w)));
Some((store, json!({ "left": n(panels.left), "right": n(panels.right) })))
}
/// Records a write; failures show and retry later.
fn settled(moves: u64, written: io::Result<()>, cx: &mut App) {
match written {
Ok(()) => {
cx.global_mut::<Panels>().saved = moves;
super::unsaved(super::PANELS, None, cx);
}
Err(error) => {
log::error!("settings: panel widths not saved: {error}");
super::unsaved(super::PANELS, Some(error.to_string().into()), cx);
}
}
}
/// Writes a pending width now; quitting cannot wait.
fn flush(cx: &mut App) {
if let Some((store, saved)) = pending(cx) {
let moves = cx.global::<Panels>().moves;
let written = store.set(KEY, saved);
settled(moves, written, cx);
}
}
#[cfg(test)]
mod tests {
use gpui::px;
use serde_json::json;
#[test]
fn widths_read_strictly() {
assert_eq!(super::parse(None).unwrap(), (None, None));
assert_eq!(super::parse(Some(&json!({"left": 300, "right": null}))).unwrap(), (Some(px(300.0)), None));
for bad in [json!({"left": "broken", "right": 320}), json!({"left": -4}), json!([1])] {
assert!(super::parse(Some(&bad)).is_err(), "{bad}");
}
}
}
+236
View File
@@ -0,0 +1,236 @@
//! What this app keeps, clearing it, and recovery limits (§18.3).
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
use ely_gpui_component::{
buttons::{Button, SegmentedControl},
feedback::Banner,
overlays::ConfirmDialog,
primitives::Severity,
settings::{SettingsRow, SettingsSection},
};
use ely_platform::storage::{self, Kind, Limits};
use gpui::{AnyElement, App, Context, IntoElement, SharedString, WeakEntity};
use super::window::SettingsView;
use crate::i18n::text;
/// Last read sizes and limits; a clear waiting or failed.
#[derive(Default)]
pub(crate) struct Storage {
usage: Option<Result<Vec<(Kind, u64, bool)>, SharedString>>,
limits: Option<Result<Limits, SharedString>>,
/// A clear waiting for the user's yes.
confirming: Option<Kind>,
failed: Option<SharedString>,
}
/// Limit writes outlive the settings window that asked.
#[derive(Default)]
struct Limiting {
/// The latest commit; older writes are dropped.
asked: Arc<AtomicU64>,
/// Picked but not yet written; a read keeps them.
pending: Option<Limits>,
/// The last write's failure, until one succeeds.
failed: Option<SharedString>,
}
impl gpui::Global for Limiting {}
impl SettingsView {
/// Reads sizes and limits off the window thread.
pub(super) fn read_storage(&mut self, cx: &mut Context<Self>) {
let app = self.app;
let read = cx.background_executor().spawn(async move {
let usage = storage::usage(app).and_then(|usage| {
usage.into_iter().map(|(kind, bytes)| Ok((kind, bytes, storage::clearing(app, kind)?))).collect()
});
(usage, storage::limits(app))
});
cx.spawn(async move |this, cx| {
let (usage, limits) = read.await;
this.update(cx, |this, cx| {
this.storage.usage = Some(usage.map_err(|error| error.to_string().into()));
this.storage.limits = Some(match cx.default_global::<Limiting>().pending {
Some(wanted) => Ok(wanted),
None => limits.map_err(|error| error.to_string().into()),
});
cx.notify();
})
})
.detach();
}
pub(super) fn storage_page(&self, this: WeakEntity<Self>, cx: &App) -> AnyElement {
let mut section = SettingsSection::new(text(cx, "settings.storage", &[]));
match &self.storage.usage {
None => {}
Some(Err(error)) => section = section.row(Banner::new("storage-unread", Severity::Danger, error.clone())),
Some(Ok(usage)) => {
// Only ElyMark keeps web content.
let shown = usage.iter().filter(|(kind, ..)| *kind != Kind::Cache || self.app == ely_platform::AppId::Mark);
for (kind, bytes, clearing) in shown {
section = section.row(row(*kind, *bytes, *clearing, this.clone(), cx));
}
}
}
match &self.storage.limits {
None => {}
Some(Err(error)) => section = section.row(Banner::new("storage-limits", Severity::Danger, error.clone())),
Some(Ok(limits)) => {
let keep = choices("storage-keep", &[10, 20, 50], limits.keep as u64, this.clone(), |limits, n| Limits {
keep: n as usize,
..limits
});
let space = choices("storage-space", &[32, 64, 256], limits.megabytes, this, |limits, n| Limits {
megabytes: n,
..limits
});
section = section
.row(SettingsRow::new(text(cx, "settings.storage.keep", &[])).control(keep))
.row(SettingsRow::new(text(cx, "settings.storage.space", &[])).control(space));
}
}
let unsaved = cx.try_global::<Limiting>().and_then(|limiting| limiting.failed.clone());
if let Some(failed) = unsaved {
section = section.row(Banner::new("storage-unsaved", Severity::Danger, failed));
}
if let Some(failed) = self.storage.failed.clone() {
section = section.row(Banner::new("storage-failed", Severity::Danger, failed));
}
section.into_any_element()
}
/// The yes-or-no before data is deleted (§4.2).
pub(super) fn storage_dialog(&self, this: WeakEntity<Self>, cx: &App) -> Option<AnyElement> {
let kind = self.storage.confirming?;
let what = name(kind, cx);
let message = match kind.at_next_start() {
true => text(cx, "settings.storage.ask_later", &[("what", &what)]),
false => text(cx, "settings.storage.ask_now", &[("what", &what)]),
};
let close = this.clone();
let dialog = ConfirmDialog::new("storage-ask", text(cx, "settings.storage.ask_title", &[("what", &what)]), message, move |_, cx| {
close.update(cx, |this, cx| {
this.storage.confirming = None;
cx.notify();
})
.ok();
})
.confirm(text(cx, "settings.storage.delete", &[]))
.destructive()
.on_confirm(move |_, cx| {
this.update(cx, |this, cx| this.clear_storage(kind, cx)).ok();
});
Some(dialog.into_any_element())
}
fn clear_storage(&mut self, kind: Kind, cx: &mut Context<Self>) {
let app = self.app;
let cleared = cx.background_executor().spawn(async move { storage::clear(app, kind) });
cx.spawn(async move |this, cx| {
let done = cleared.await;
this.update(cx, |this, cx| this.storage_done(done.map(|_| ()), cx))
})
.detach();
}
fn storage_done(&mut self, done: std::io::Result<()>, cx: &mut Context<Self>) {
self.storage.failed = done.err().map(|error| text(cx, "settings.storage.failed", &[("error", &error.to_string())]));
self.read_storage(cx);
}
/// Writes the shown limits with one changed; latest wins.
fn pick_limits(&mut self, change: impl FnOnce(Limits) -> Limits, cx: &mut Context<Self>) {
let Some(Ok(shown)) = self.storage.limits.clone() else {
return;
};
let wanted = change(shown);
self.storage.limits = Some(Ok(wanted));
let limiting = cx.default_global::<Limiting>();
limiting.pending = Some(wanted);
let asked = limiting.asked.fetch_add(1, Ordering::SeqCst) + 1;
let (app, latest) = (self.app, Arc::clone(&limiting.asked));
let kept = cx
.background_executor()
.spawn(async move { storage::set_limits(app, wanted, || latest.load(Ordering::SeqCst) == asked) });
// The global settles even when this window has closed.
cx.spawn(async move |this, cx| {
let done = kept.await;
let latest = cx.update(|cx| {
let failed = done.as_ref().err().map(|error| text(cx, "settings.storage.failed", &[("error", &error.to_string())]));
let limiting = cx.global_mut::<Limiting>();
let latest = limiting.asked.load(Ordering::SeqCst) == asked;
if latest {
limiting.failed = failed;
if done.is_ok() {
limiting.pending = None;
}
}
latest
});
if latest {
this.update(cx, |this, cx| this.read_storage(cx)).ok();
}
})
.detach();
cx.notify();
}
}
fn name(kind: Kind, cx: &App) -> SharedString {
match kind {
Kind::Log => text(cx, "settings.storage.log", &[]),
Kind::Cache => text(cx, "settings.storage.cache", &[]),
Kind::Recovery => text(cx, "settings.storage.recovery", &[]),
}
}
/// One kind: its size, cleared after a yes.
fn row(kind: Kind, bytes: u64, clearing: bool, this: WeakEntity<SettingsView>, cx: &App) -> AnyElement {
let size = text(cx, "settings.storage.size", &[("size", &bytes.div_ceil(1024).to_string())]);
let row = SettingsRow::new(name(kind, cx)).description(size);
if clearing {
let waiting = Button::new(SharedString::from(format!("storage-waiting-{kind:?}")), text(cx, "settings.storage.next_start", &[]));
return row.control(waiting.disabled(true)).into_any_element();
}
let label = match kind.at_next_start() {
true => text(cx, "settings.storage.clear_later", &[]),
false => text(cx, "settings.storage.clear", &[]),
};
let button = Button::new(SharedString::from(format!("storage-clear-{kind:?}")), label)
.disabled(bytes == 0)
.on_click(move |_, _, cx| {
this.update(cx, |this, cx| {
this.storage.confirming = Some(kind);
cx.notify();
})
.ok();
});
row.control(button).into_any_element()
}
/// A few sizes to pick from; picking writes the limits.
fn choices(
id: &'static str,
options: &[u64],
chosen: u64,
this: WeakEntity<SettingsView>,
change: fn(Limits, u64) -> Limits,
) -> SegmentedControl {
options
.iter()
.fold(SegmentedControl::new(id, chosen.to_string()), |control, n| {
control.segment(n.to_string(), n.to_string(), None)
})
.on_change(move |picked, _, cx| {
let Ok(n) = picked.parse::<u64>() else {
return log::error!("settings: {id} offered {picked}");
};
this.update(cx, |this, cx| this.pick_limits(|limits| change(limits, n), cx)).ok();
})
}
+50
View File
@@ -0,0 +1,50 @@
use ely_gpui_component::theme::{ActiveTheme as _, Mode};
use ely_platform::{
AppId,
settings::{Appearance, Shared},
};
use gpui::TestAppContext;
use super::{changed, window::SettingsView};
fn installed(cx: &mut TestAppContext) {
cx.update(|cx| {
ely_gpui_component::init_for_tests(cx);
super::install_defaults(cx);
crate::theme::install(cx);
crate::i18n::install(cx);
});
}
#[gpui::test]
fn a_change_on_disk_turns_the_theme_at_once(cx: &mut TestAppContext) {
installed(cx);
let dark = Shared { appearance: Appearance::Dark, ..Shared::default() };
cx.update(|cx| changed((dark, None), cx));
cx.run_until_parked();
cx.update(|cx| {
assert_eq!(cx.global::<crate::theme::ThemeSettings>().appearance, Appearance::Dark);
assert_eq!(cx.theme().mode(), Mode::Dark);
});
let light = Shared { appearance: Appearance::Light, ..Shared::default() };
cx.update(|cx| changed((light, None), cx));
cx.run_until_parked();
cx.update(|cx| assert_eq!(cx.theme().mode(), Mode::Light));
}
#[gpui::test]
fn each_app_shows_its_own_sections(cx: &mut TestAppContext) {
installed(cx);
for (app, keys) in [
(AppId::Doc, &["general", "documents", "storage", "diagnostics"][..]),
(AppId::Pdf, &["general", "documents", "storage", "diagnostics"]),
(AppId::Tree, &["general", "git", "storage", "diagnostics"]),
(AppId::Mark, &["general", "storage", "diagnostics"]),
] {
let window = cx.add_window(|window, cx| SettingsView::new(app, window, cx));
let shown = window
.update(cx, |view, _, cx| view.sections(cx).into_iter().map(|(key, ..)| key).collect::<Vec<_>>())
.unwrap();
assert_eq!(shown, keys, "{app:?}");
}
}
+351
View File
@@ -0,0 +1,351 @@
//! The settings window: shared settings and this app's own.
use ely_gpui_component::{
buttons::SegmentedControl,
feedback::Banner,
forms::{Input, InputEvent, Switch, TextInput},
primitives::{IconName, Severity},
settings::{self as kit, SettingsLayout, SettingsRow, SettingsSection, ThemeSelector},
};
use ely_platform::{
AppId,
settings::{APPEARANCE, AUTHOR, Appearance, LANGUAGE, Language, MOTION, Motion, OMARCHY_SYNC, Store, word},
};
use gpui::{
AppContext as _, Context, Entity, FocusHandle, Global, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Window, WindowHandle, div,
};
use serde_json::Value;
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
use super::{current, set};
use crate::{i18n::text, shell::Shell};
/// The open settings window, one per app.
#[derive(Clone, Copy)]
struct Opened(Option<WindowHandle<SettingsView>>);
impl Global for Opened {}
/// Opens the settings window, or brings it forward.
pub fn open(app: AppId, cx: &mut gpui::App) {
let shown = cx.try_global::<Opened>().and_then(|opened| opened.0);
if let Some(handle) = shown
&& handle.update(cx, |_, window, _| window.activate_window()).is_ok()
{
return;
}
let mut options = crate::run::window_options(app, gpui::size(gpui::px(720.0), gpui::px(520.0)), cx);
if let Some(bar) = options.titlebar.as_mut() {
bar.title = Some(text(cx, "settings.title", &[]));
}
match cx.open_window(options, |window, cx| cx.new(|cx| SettingsView::new(app, window, cx))) {
Ok(handle) => cx.set_global(Opened(Some(handle))),
Err(error) => log::error!("settings: the window did not open: {error:#}"),
}
}
pub struct SettingsView {
pub(super) app: AppId,
focus: FocusHandle,
section: SharedString,
author: Entity<TextInput>,
/// The name last read from or saved to the file.
author_saved: String,
/// The latest author commit; older writes are dropped.
author_asked: Arc<AtomicU64>,
git: Option<Entity<TextInput>>,
git_error: Option<SharedString>,
/// The latest Git commit; older checks are dropped.
git_asked: Arc<AtomicU64>,
pub(super) diagnostics: super::diagnostics::Diagnostics,
pub(super) storage: super::storage::Storage,
_fields: Vec<Subscription>,
}
impl SettingsView {
pub(super) fn new(app: AppId, window: &mut Window, cx: &mut Context<Self>) -> Self {
let author = cx.new(|cx| TextInput::new(window, cx).label(text(cx, "settings.author", &[])));
let account = ely_platform::user::account().unwrap_or_else(|error| {
log::warn!("settings: no account name: {error}");
String::new()
});
let named = current(cx).author.clone();
author.update(cx, |field, cx| {
field.set_placeholder(account, cx);
field.set_text(named.clone(), cx);
});
let mut fields = vec![
cx.subscribe(&author, |this, field, event: &InputEvent, cx| {
if matches!(event, InputEvent::Blur | InputEvent::Submit) {
this.commit_author(&field, cx);
}
}),
cx.observe_global::<super::Current>(|this, cx| this.follow_author(cx)),
];
let git = (app == AppId::Tree).then(|| cx.new(|cx| TextInput::new(window, cx).label(text(cx, "settings.git.path", &[]))));
let mut git_error = None;
if let Some(git) = &git {
let stored = git_setting().unwrap_or_else(|error| {
log::error!("settings: the Git setting is unreadable: {error}");
git_error = Some(text(cx, "settings.unreadable", &[("error", &error.to_string())]));
String::new()
});
git.update(cx, |field, cx| field.set_text(stored, cx));
fields.push(cx.subscribe(git, |this, field, event: &InputEvent, cx| {
if matches!(event, InputEvent::Blur | InputEvent::Submit) {
this.commit_git(&field, cx);
}
}));
}
let focus = cx.focus_handle();
window.focus(&focus, cx);
Self {
app,
focus,
section: "general".into(),
author,
author_saved: named,
author_asked: Arc::default(),
git,
git_error,
git_asked: Arc::default(),
diagnostics: Default::default(),
storage: Default::default(),
_fields: fields,
}
}
/// A name written elsewhere replaces an unedited field.
fn follow_author(&mut self, cx: &mut Context<Self>) {
let named = current(cx).author.clone();
let shown = self.author.read(cx).text().trim().to_string();
if named != self.author_saved && shown == self.author_saved {
self.author.update(cx, |field, cx| field.set_text(named.clone(), cx));
self.author_saved = named;
}
}
/// Writes an edit; a failed one stays for retry.
fn commit_author(&mut self, field: &Entity<TextInput>, cx: &mut Context<Self>) {
let typed = field.read(cx).text().trim().to_string();
// Every commit outdates the writes still waiting.
let asked = self.author_asked.fetch_add(1, Ordering::SeqCst) + 1;
if typed == self.author_saved {
return;
}
let turn = (Arc::clone(&self.author_asked), asked);
let write = super::saved(AUTHOR, Value::String(typed.clone()), Some(turn), cx);
cx.spawn(async move |this, cx| {
if write.await {
this.update(cx, |this, _| this.author_saved = typed).ok();
}
})
.detach();
}
/// Checks a system Git, then keeps it; latest wins.
fn commit_git(&mut self, field: &Entity<TextInput>, cx: &mut Context<Self>) {
let typed = field.read(cx).text().trim().to_string();
// Every commit outdates the checks still running.
let asked = self.git_asked.fetch_add(1, Ordering::SeqCst) + 1;
if git_setting().is_ok_and(|stored| stored == typed) {
self.git_error = None;
return cx.notify();
}
let latest = Arc::clone(&self.git_asked);
let check = cx.background_executor().spawn({
let typed = typed.clone();
async move {
if !typed.is_empty() {
ely_platform::git::system_version(std::path::Path::new(&typed))?;
}
// Under the file lock: a later commit skips this.
let still = || latest.load(Ordering::SeqCst) == asked;
Store::app(AppId::Tree)?.set_if(ely_platform::git::PROGRAM, Value::String(typed), still)
}
});
cx.spawn(async move |this, cx| {
let checked = check.await;
this.update(cx, |this, cx| {
if this.git_asked.load(Ordering::SeqCst) != asked {
return log::info!("settings: Git check {asked} was overtaken");
}
this.git_error = checked.err().map(|error| {
log::warn!("settings: Git setting refused: {error}");
text(cx, "settings.git.bad", &[("path", &typed), ("error", &error.to_string())])
});
cx.notify();
})
})
.detach();
}
pub(super) fn sections(&self, cx: &gpui::App) -> Vec<(&'static str, SharedString, IconName)> {
let mut sections = vec![("general", text(cx, "settings.general", &[]), IconName::Settings)];
if matches!(self.app, AppId::Doc | AppId::Table | AppId::Show | AppId::Pdf) {
sections.push(("documents", text(cx, "settings.documents", &[]), IconName::FileText));
}
if self.git.is_some() {
sections.push(("git", text(cx, "settings.git", &[]), IconName::GitBranch));
}
sections.push(("storage", text(cx, "settings.storage", &[]), IconName::HardDrive));
sections.push(("diagnostics", text(cx, "settings.diagnostics", &[]), IconName::Bug));
sections
}
fn general(&self, cx: &gpui::App) -> SettingsSection {
let shared = current(cx);
let appearance = match shared.appearance {
Appearance::System => kit::Appearance::System,
Appearance::Light => kit::Appearance::Light,
Appearance::Dark => kit::Appearance::Dark,
};
let theme = ThemeSelector::new("settings-appearance", appearance, |chosen, _, cx| {
let chosen = match chosen {
kit::Appearance::System => Appearance::System,
kit::Appearance::Light => Appearance::Light,
kit::Appearance::Dark => Appearance::Dark,
};
set(APPEARANCE, word(&Appearance::ALL, &chosen).into(), cx);
});
let language = choices("settings-language", &Language::ALL, &shared.language, |choice, cx| match choice {
Language::System => text(cx, "settings.language.system", &[]),
Language::English => "English".into(),
Language::Chinese => "简体中文".into(),
}, LANGUAGE, cx);
let motion = choices("settings-motion", &Motion::ALL, &shared.motion, |choice, cx| match choice {
Motion::System => text(cx, "settings.motion.system", &[]),
Motion::Reduced => text(cx, "settings.motion.reduce", &[]),
Motion::Full => text(cx, "settings.motion.full", &[]),
}, MOTION, cx);
let mut section = SettingsSection::new(text(cx, "settings.general", &[]))
.row(SettingsRow::new(text(cx, "settings.appearance", &[])).control(theme))
.row(
SettingsRow::new(text(cx, "settings.language", &[]))
.description(text(cx, "settings.language.next", &[]))
.control(language),
)
.row(SettingsRow::new(text(cx, "settings.motion", &[])).control(motion));
// Only where Omarchy is installed.
if ely_platform::omarchy_current_dir().is_some() {
let follow = Switch::new("settings-omarchy", shared.omarchy_sync)
.name(text(cx, "settings.omarchy", &[]))
.on_change(|on, _, cx| set(OMARCHY_SYNC, Value::Bool(on), cx));
section = section.row(SettingsRow::new(text(cx, "settings.omarchy", &[])).control(follow));
}
section
}
fn page(&self, this: gpui::WeakEntity<Self>, cx: &gpui::App) -> gpui::AnyElement {
match self.section.as_ref() {
"documents" => {
let autosave = Switch::new("settings-autosave", super::autosave::on(cx))
.name(text(cx, "settings.autosave", &[]))
.on_change(|on, _, cx| super::autosave::set(on, cx));
SettingsSection::new(text(cx, "settings.documents", &[]))
.row(
SettingsRow::new(text(cx, "settings.author", &[]))
.description(text(cx, "settings.author.hint", &[]))
.control(div().w_full().max_w_64().child(Input::new(&self.author))),
)
.row(
SettingsRow::new(text(cx, "settings.autosave", &[]))
.description(text(cx, "settings.autosave.hint", &[]))
.control(autosave),
)
.into_any_element()
}
"git" => {
let field = self.git.clone().expect("the git section is ElyTree's");
let section = SettingsSection::new(text(cx, "settings.git", &[])).row(
SettingsRow::new(text(cx, "settings.git.path", &[]))
.description(text(cx, "settings.git.hint", &[]))
.control(div().w_full().max_w_64().child(Input::new(&field).invalid(self.git_error.is_some()))),
);
match self.git_error.clone() {
Some(error) => section.row(Banner::new("settings-git-error", Severity::Danger, error)),
None => section,
}
.into_any_element()
}
"storage" => self.storage_page(this, cx),
"diagnostics" => self.diagnostics_page(this, cx),
_ => self.general(cx).into_any_element(),
}
}
}
/// A segmented choice that writes `key` when picked.
fn choices<T: Copy + PartialEq + 'static>(
id: &'static str,
all: &'static [(T, &'static str)],
chosen: &T,
label: impl Fn(&T, &gpui::App) -> SharedString,
key: &'static str,
cx: &gpui::App,
) -> SegmentedControl {
all.iter()
.fold(SegmentedControl::new(id, word(all, chosen)), |control, (choice, word)| {
control.segment(*word, label(choice, cx), None)
})
.on_change(move |picked, _, cx| set(key, Value::String(picked.to_string()), cx))
}
/// ElyTree's Git program; empty is its own.
fn git_setting() -> std::io::Result<String> {
match Store::app(AppId::Tree)?.read()?.get(ely_platform::git::PROGRAM) {
None => Ok(String::new()),
Some(Value::String(path)) => Ok(path.clone()),
Some(other) => Err(std::io::Error::other(format!("settings: git cannot be {other}"))),
}
}
impl Render for SettingsView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let sections = self.sections(cx);
let this = cx.entity().downgrade();
let page = self.page(this.clone(), cx);
let dialog = self.storage_dialog(this.clone(), cx);
let layout = SettingsLayout::new("settings", sections, self.section.clone(), move |key, _, cx| {
let key = key.clone();
this.update(cx, |this, cx| {
if key == "diagnostics" {
this.read_diagnostics(cx);
}
if key == "storage" {
this.read_storage(cx);
}
this.section = key;
cx.notify();
})
.ok();
})
.page(page);
let current = cx.global::<super::Current>();
let unreadable = current.error.clone().map(|error| {
Banner::new("settings-unreadable", Severity::Danger, text(cx, "settings.unreadable", &[("error", &error)]))
});
let app_error: Vec<_> = current
.app_error
.iter()
.map(|(kind, error)| {
let id = SharedString::from(format!("settings-app-unreadable-{kind}"));
Banner::new(id, Severity::Danger, text(cx, "settings.app_unreadable", &[("error", error)]))
})
.collect();
let unsaved: Vec<_> = current
.unsaved
.iter()
.map(|(kind, error)| {
Banner::new(SharedString::from(format!("settings-unsaved-{kind}")), Severity::Danger, text(cx, "settings.unsaved", &[("error", error)]))
})
.collect();
Shell::new(text(cx, "settings.title", &[]), &self.focus)
.child(div().size_full().p_6().flex().flex_col().gap_4().children(unreadable).children(app_error).children(unsaved).child(layout))
.children(dialog)
}
}
+165
View File
@@ -0,0 +1,165 @@
use std::time::Instant;
use ely_gpui_component::{
layout::AppShell,
menus::MenuBar,
motion,
navigation::EditorTabs,
primitives::FocusScope,
shell::{ResizeBorder, StatusBar, TitleBar},
theme::{ActiveTheme, TextSize},
};
use gpui::{
AnyElement, AnyView, App, ElementId, FocusHandle, IntoElement, ParentElement, RenderOnce,
SharedString, Styled, Window, div,
};
mod page;
mod panel;
mod root;
use page::Fade;
use panel::{Side, SidePanel};
pub use root::{Root, blank, document_root, root, set_document_menus, tool_root};
/// The window frame; empty panels take no room.
#[derive(IntoElement)]
pub struct Shell {
focus: FocusHandle,
title: SharedString,
tabs: Option<EditorTabs>,
left: Option<AnyView>,
right: Option<AnyView>,
status: Option<StatusBar>,
page: Option<ElementId>,
body: Vec<AnyElement>,
}
impl Shell {
pub fn new(title: impl Into<SharedString>, focus: &FocusHandle) -> Self {
Self {
focus: focus.clone(),
title: title.into(),
tabs: None,
left: None,
right: None,
status: None,
page: None,
body: Vec::new(),
}
}
pub fn tabs(mut self, tabs: EditorTabs) -> Self {
self.tabs = Some(tabs);
self
}
/// Navigation: outline, pages, folders. None leaves no strip.
pub fn left(mut self, view: Option<AnyView>) -> Self {
self.left = view;
self
}
/// The selection's properties. None leaves no strip.
pub fn right(mut self, view: Option<AnyView>) -> Self {
self.right = view;
self
}
pub fn status(mut self, bar: StatusBar) -> Self {
self.status = Some(bar);
self
}
/// The content fades in each time this key changes.
pub fn page(mut self, key: impl Into<ElementId>) -> Self {
self.page = Some(key.into());
self
}
}
#[cfg(target_os = "macos")]
fn menu_bar(_: &App) -> Option<MenuBar> {
None
}
#[cfg(not(target_os = "macos"))]
fn menu_bar(cx: &App) -> Option<MenuBar> {
cx.get_menus().map(|menus| MenuBar::new("menu-bar", menus))
}
impl ParentElement for Shell {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.body.extend(elements);
}
}
impl RenderOnce for Shell {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let length = motion::duration(motion::BASE, cx);
let content = div()
.flex_1()
.min_w_0()
.h_full()
.flex()
.flex_col()
.children(self.body);
let content = match self.page {
Some(page) => {
let now = Instant::now();
let state =
window.use_keyed_state("page-fade", cx, |_, _| Fade::new(page.clone(), now));
let (opacity, running) = state.update(cx, |fade, _| {
fade.show(&page, now);
(fade.opacity(now, length), fade.running(now, length))
});
if running {
window.request_animation_frame();
}
content.opacity(opacity).into_any_element()
}
None => content.into_any_element(),
};
let header = div()
.flex()
.flex_col()
.child(TitleBar::new("titlebar").title(self.title))
.children(menu_bar(cx))
.children(self.tabs);
// Panels give way so the content keeps its room.
let (left, right) = (panel::fitted(Side::Left, window, cx), panel::fitted(Side::Right, window, cx));
let (shown_left, shown_right) = panel::room(
window.viewport_size().width,
self.left.as_ref().map(|_| left),
self.right.as_ref().map(|_| right),
);
let row = div().flex_1().min_h_0().flex().child(content).child(SidePanel::new(
"right-panel",
Side::Right,
self.right.filter(|_| shown_right.is_some()),
shown_right.unwrap_or(right),
));
let frame = AppShell::new()
.title_bar(header)
.sidebar(SidePanel::new(
"left-panel",
Side::Left,
self.left.filter(|_| shown_left.is_some()),
shown_left.unwrap_or(left),
))
.child(row);
let frame = match self.status {
Some(bar) => frame.status_bar(bar),
None => frame,
};
let theme = cx.theme();
ResizeBorder::new().child(
FocusScope::new(&self.focus)
.root()
.size_full()
.font_family(theme.font_family.clone())
.text_size(theme.text_size(TextSize::Base))
.child(frame),
)
}
}
+97
View File
@@ -0,0 +1,97 @@
use std::time::{Duration, Instant};
use ely_gpui_component::motion;
use gpui::ElementId;
/// The current page and when its fade-in began.
pub(super) struct Fade {
page: ElementId,
since: Instant,
}
impl Fade {
pub(super) fn new(page: ElementId, now: Instant) -> Self {
Self { page, since: now }
}
/// Restarts the fade when the page changes.
pub(super) fn show(&mut self, page: &ElementId, now: Instant) {
if self.page != *page {
self.page = page.clone();
self.since = now;
}
}
pub(super) fn opacity(&self, now: Instant, length: Duration) -> f32 {
let elapsed = now.saturating_duration_since(self.since).as_secs_f32();
let t = (elapsed / length.as_secs_f32().max(f32::EPSILON)).min(1.0);
0.4 + 0.6 * motion::ease_out_cubic(t)
}
pub(super) fn running(&self, now: Instant, length: Duration) -> bool {
now.saturating_duration_since(self.since) < length
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use gpui::ElementId;
use super::Fade;
const LENGTH: Duration = Duration::from_millis(200);
fn after(start: Instant, millis: u64) -> Instant {
start + Duration::from_millis(millis)
}
#[test]
fn a_fade_rises_from_dim_to_full_and_stays() {
let t0 = Instant::now();
let fade = Fade::new("a".into(), t0);
let first: Vec<f32> = (0..=20)
.map(|i| fade.opacity(after(t0, i * 10), LENGTH))
.collect();
assert_eq!(first[0], 0.4);
assert!(first.windows(2).all(|pair| pair[1] >= pair[0]), "{first:?}");
assert_eq!(first[20], 1.0);
assert!(!fade.running(after(t0, 200), LENGTH));
assert_eq!(fade.opacity(after(t0, 900), LENGTH), 1.0);
}
#[test]
fn the_same_page_keeps_its_fade() {
let t0 = Instant::now();
let mut fade = Fade::new("a".into(), t0);
let page = ElementId::Name("a".into());
fade.show(&page, after(t0, 150));
assert_eq!(fade.opacity(after(t0, 200), LENGTH), 1.0);
}
#[test]
fn a_new_page_restarts_dim() {
let t0 = Instant::now();
let mut fade = Fade::new("a".into(), t0);
assert_eq!(fade.opacity(after(t0, 500), LENGTH), 1.0);
fade.show(&"b".into(), after(t0, 500));
assert_eq!(fade.opacity(after(t0, 500), LENGTH), 0.4);
assert_eq!(fade.opacity(after(t0, 700), LENGTH), 1.0);
}
#[test]
fn reduced_motion_shows_the_page_at_once() {
let t0 = Instant::now();
let fade = Fade::new("a".into(), t0);
let length = Duration::from_millis(1);
assert_eq!(fade.opacity(after(t0, 1), length), 1.0);
assert!(!fade.running(after(t0, 1), length));
}
}
+321
View File
@@ -0,0 +1,321 @@
use std::time::{Duration, Instant};
use std::{cell::Cell, ops::Not as _, rc::Rc};
use ely_gpui_component::{layout::resize_edge, motion, theme::ActiveTheme};
use gpui::{
AnyView, App, AppContext as _, Axis, DragMoveEvent, ElementId, EmptyView, InteractiveElement as _, IntoElement,
ParentElement, Pixels, RenderOnce, StatefulInteractiveElement as _, Styled, Window, div, px,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Side {
Left,
Right,
}
/// Openness of a panel; mid-flight flips never jump.
#[derive(Clone, Copy, Debug)]
struct Slide {
from: f32,
to: f32,
since: Instant,
}
impl Slide {
fn new(open: bool, now: Instant) -> Self {
let share = if open { 1.0 } else { 0.0 };
Self {
from: share,
to: share,
since: now,
}
}
fn share(&self, now: Instant, length: Duration) -> f32 {
let elapsed = now.saturating_duration_since(self.since).as_secs_f32();
let t = (elapsed / length.as_secs_f32().max(f32::EPSILON)).min(1.0);
self.from + (self.to - self.from) * motion::ease_in_out_cubic(t)
}
fn aim(&mut self, open: bool, now: Instant, length: Duration) {
let to = if open { 1.0 } else { 0.0 };
if to != self.to {
*self = Self {
from: self.share(now, length),
to,
since: now,
};
}
}
fn running(&self, now: Instant, length: Duration) -> bool {
self.from != self.to && now.saturating_duration_since(self.since) < length
}
}
/// The slide, plus the view kept while closing.
struct Slot {
slide: Slide,
view: Option<AnyView>,
}
/// A sliding side column; no view, no room.
#[derive(IntoElement)]
pub(super) struct SidePanel {
id: ElementId,
side: Side,
view: Option<AnyView>,
width: Pixels,
}
impl SidePanel {
/// `width` is what the shell left; no view, no room.
pub(super) fn new(id: impl Into<ElementId>, side: Side, view: Option<AnyView>, width: Pixels) -> Self {
Self {
id: id.into(),
side,
view,
width,
}
}
}
impl RenderOnce for SidePanel {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let open = self.view.is_some();
let length = motion::duration(motion::BASE, cx);
let now = Instant::now();
let slot = window.use_keyed_state(self.id.clone(), cx, |_, _| Slot {
slide: Slide::new(open, now),
view: None,
});
let (share, running, view) = slot.update(cx, |slot, _| {
slot.slide.aim(open, now, length);
if open {
slot.view = self.view;
}
let share = slot.slide.share(now, length);
if !open && !slot.slide.running(now, length) {
slot.view = None;
}
(share, slot.slide.running(now, length), slot.view.clone())
});
let Some(view) = view else {
return div().into_any_element();
};
if running {
window.request_animation_frame();
}
let full = self.width;
let shown = full * share;
let theme = cx.theme();
let inner = div()
.w(full)
.h_full()
.flex_none()
.border_color(theme.colors.border)
.child(view);
let inner = match self.side {
Side::Left => inner.border_r_1().ml(shown - full),
Side::Right => inner.border_l_1(),
};
// The inner edge drags the width.
let grip = resize_edge((self.id.clone(), "edge"), Axis::Horizontal, cx)
.absolute()
.top_0()
.bottom_0()
.on_drag(Grab { side: self.side, start: full, anchor: Rc::new(Cell::new(None)) }, |_, _, _, cx| {
cx.new(|_| EmptyView)
});
let grip = match self.side {
Side::Left => grip.right_0(),
Side::Right => grip.left_0(),
};
let side = self.side;
div()
.id(self.id)
.relative()
.flex_none()
.h_full()
.w(shown)
.overflow_hidden()
.bg(theme.colors.bg)
.on_drag_move(move |event: &DragMoveEvent<Grab>, window, cx| {
let grab = event.drag(cx);
if grab.side != side {
return;
}
let at = event.event.position.x;
let anchor = grab.anchor.get().unwrap_or_else(|| {
grab.anchor.set(Some(at));
at
});
let moved = if side == Side::Left { at - anchor } else { anchor - at };
let wanted = clamp(side, grab.start + moved, window);
crate::settings::panels::set_width(side == Side::Left, wanted, cx);
window.refresh();
})
.child(inner)
.children(running.not().then_some(grip))
.into_any_element()
}
}
/// A drag on a panel's edge, from the pressed width.
struct Grab {
side: Side,
start: Pixels,
anchor: Rc<Cell<Option<Pixels>>>,
}
/// Defaults leave the page most of the window.
fn default_width(side: Side) -> Pixels {
match side {
Side::Left => px(220.0),
// A name column and an editor beside it.
Side::Right => px(320.0),
}
}
/// The user's width, else the default, within bounds.
pub(super) fn fitted(side: Side, window: &Window, cx: &App) -> Pixels {
let chosen = crate::settings::panels::width(side == Side::Left, cx);
clamp(side, chosen.unwrap_or(default_width(side)), window)
}
/// The narrowest a panel works: a list, or properties.
fn least(side: Side) -> Pixels {
match side {
Side::Left => px(160.0),
Side::Right => px(280.0),
}
}
/// At least its least, under half the window.
fn clamp(side: Side, wanted: Pixels, window: &Window) -> Pixels {
let most = (window.viewport_size().width * 0.45).max(least(side));
wanted.clamp(least(side), most)
}
/// The content keeps this much; panels give way first.
const CONTENT_LEAST: f32 = 360.0;
/// Shown widths: shrink both, then hide right, then left.
pub(super) fn room(window: Pixels, left: Option<Pixels>, right: Option<Pixels>) -> (Option<Pixels>, Option<Pixels>) {
let spare = window - px(CONTENT_LEAST);
if let (Some(l), Some(r)) = (left, right) {
let r = r.min((spare - l).max(least(Side::Right)));
let l = l.min((spare - r).max(least(Side::Left)));
if l + r <= spare {
return (Some(l), Some(r));
}
return (alone(Side::Left, l.max(left.unwrap_or(l)), spare), None);
}
(left.and_then(|l| alone(Side::Left, l, spare)), right.and_then(|r| alone(Side::Right, r, spare)))
}
/// One panel in `spare`, if its least fits.
fn alone(side: Side, wanted: Pixels, spare: Pixels) -> Option<Pixels> {
let shown = wanted.min(spare);
(shown >= least(side)).then_some(shown)
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use gpui::px;
use super::{Slide, room};
const LENGTH: Duration = Duration::from_millis(200);
fn after(start: Instant, millis: u64) -> Instant {
start + Duration::from_millis(millis)
}
#[test]
fn a_panel_starts_where_it_is_aimed_and_stays() {
let t0 = Instant::now();
assert_eq!(Slide::new(true, t0).share(after(t0, 5000), LENGTH), 1.0);
assert_eq!(Slide::new(false, t0).share(after(t0, 5000), LENGTH), 0.0);
assert!(!Slide::new(true, t0).running(t0, LENGTH));
}
#[test]
fn opening_rises_without_stepping_back_and_ends_at_one() {
let t0 = Instant::now();
let mut slide = Slide::new(false, t0);
slide.aim(true, t0, LENGTH);
let shares: Vec<f32> = (0..=20)
.map(|i| slide.share(after(t0, i * 10), LENGTH))
.collect();
assert_eq!(shares[0], 0.0);
assert_eq!(shares[20], 1.0);
assert!(
shares.windows(2).all(|pair| pair[1] >= pair[0]),
"{shares:?}"
);
assert!(slide.running(after(t0, 100), LENGTH));
assert!(!slide.running(after(t0, 200), LENGTH));
assert_eq!(slide.share(after(t0, 900), LENGTH), 1.0);
}
#[test]
fn flipping_the_aim_mid_flight_continues_from_where_it_was() {
let t0 = Instant::now();
let mut slide = Slide::new(false, t0);
slide.aim(true, t0, LENGTH);
let mid = after(t0, 80);
let before = slide.share(mid, LENGTH);
assert!(before > 0.0 && before < 1.0);
slide.aim(false, mid, LENGTH);
assert_eq!(slide.share(mid, LENGTH), before);
assert!(slide.share(after(mid, 40), LENGTH) < before);
assert_eq!(slide.share(after(mid, 400), LENGTH), 0.0);
}
#[test]
fn aiming_again_at_the_same_target_does_not_restart() {
let t0 = Instant::now();
let mut slide = Slide::new(false, t0);
slide.aim(true, t0, LENGTH);
let at = after(t0, 120);
let share = slide.share(at, LENGTH);
slide.aim(true, at, LENGTH);
assert_eq!(slide.share(at, LENGTH), share);
assert_eq!(slide.share(after(t0, 200), LENGTH), 1.0);
}
#[test]
fn reduced_motion_settles_in_a_millisecond() {
let t0 = Instant::now();
let mut slide = Slide::new(false, t0);
slide.aim(true, t0, Duration::from_millis(1));
assert_eq!(slide.share(after(t0, 1), Duration::from_millis(1)), 1.0);
assert!(!slide.running(after(t0, 1), Duration::from_millis(1)));
}
#[test]
fn panels_give_way_so_the_content_keeps_its_room() {
let (left, right) = (Some(px(220.0)), Some(px(320.0)));
assert_eq!(room(px(1100.0), left, right), (left, right), "wide: as chosen");
// 800 leaves 440: both shrink to their least.
assert_eq!(room(px(800.0), left, right), (Some(px(160.0)), Some(px(280.0))));
// 640 leaves 280: the right goes, the left stays.
assert_eq!(room(px(640.0), left, right), (left, None));
// A wider left alone shrinks to the spare room.
assert_eq!(room(px(640.0), Some(px(400.0)), None), (Some(px(280.0)), None));
// Too narrow for even the least: hidden.
assert_eq!(room(px(500.0), left, None), (None, None));
assert_eq!(room(px(640.0), None, right), (None, Some(px(280.0))));
}
}
+302
View File
@@ -0,0 +1,302 @@
use ely_gpui_component::{
feedback::{Toast, ToastViewport, Toaster},
navigation::{Command as PaletteCommand, CommandPalette},
primitives::{Severity, raise},
};
use ely_platform::AppId;
use std::path::{Path, PathBuf};
use gpui::{
App, AppContext as _, Context, Entity, FocusHandle, InteractiveElement as _, IntoElement,
Keystroke, ParentElement, Render, SharedString, Styled as _, Window, div,
prelude::FluentBuilder as _,
};
use super::Shell;
use crate::commands::{
self, Export, New, Open, OpenElyAgent, OpenElyDoc, OpenElyMail, OpenElyMark, OpenElyPdf,
OpenElyShow, OpenElyTable, OpenElyTree, Print, Save, SaveAs, ToggleCommandPalette,
};
/// The shared root: shell, file commands, command palette.
pub struct Root {
app: AppId,
focus: FocusHandle,
palette: bool,
recent: Vec<SharedString>,
last: Option<SharedString>,
toaster: Entity<Toaster>,
content: Option<gpui::AnyView>,
document: bool,
file: &'static [&'static commands::Command],
}
/// A window with only its frame.
pub fn blank(app: AppId, window: &mut Window, cx: &mut App) -> Entity<Root> {
root(app, None, window, cx)
}
pub fn root(
app: AppId,
content: Option<gpui::AnyView>,
window: &mut Window,
cx: &mut App,
) -> Entity<Root> {
build(app, content, false, &commands::FILE, Vec::new(), window, cx)
}
/// A tool window: File has New and Open; `menus` follow.
pub fn tool_root(
app: AppId,
content: gpui::AnyView,
menus: Vec<gpui::Menu>,
window: &mut Window,
cx: &mut App,
) -> Entity<Root> {
build(
app,
Some(content),
false,
&commands::OPENING,
menus,
window,
cx,
)
}
/// A document window: File holds `file`; `menus` follow.
pub fn document_root(
app: AppId,
document: gpui::AnyView,
file: &'static [&'static commands::Command],
menus: Vec<gpui::Menu>,
window: &mut Window,
cx: &mut App,
) -> Entity<Root> {
build(app, Some(document), true, file, menus, window, cx)
}
/// Replaces a document window's menus.
pub fn set_document_menus(
app: AppId,
file: &'static [&'static commands::Command],
menus: Vec<gpui::Menu>,
cx: &mut App,
) {
cx.set_menus(commands::menus(app, file, true, menus, cx));
}
fn build(
app: AppId,
content: Option<gpui::AnyView>,
document: bool,
file: &'static [&'static commands::Command],
menus: Vec<gpui::Menu>,
window: &mut Window,
cx: &mut App,
) -> Entity<Root> {
cx.set_menus(commands::menus(app, file, document, menus, cx));
cx.new(|cx| {
let focus = cx.focus_handle();
// A document takes focus itself.
if !document {
window.focus(&focus, cx);
}
Root {
app,
focus,
palette: false,
recent: Vec::new(),
last: None,
toaster: cx.new(|_| Toaster::default()),
content,
document,
file,
}
})
}
impl Root {
pub fn palette_open(&self) -> bool {
self.palette
}
pub fn recent(&self) -> &[SharedString] {
&self.recent
}
pub fn last(&self) -> Option<&SharedString> {
self.last.as_ref()
}
fn attempt(&mut self, id: &'static str, cx: &mut Context<Self>) {
if let Some(reason) = commands::blocked(id, self.document, cx) {
log::info!("command: {id} blocked: {reason}");
self.last = Some(format!("{id} blocked").into());
self.toaster.update(cx, |toaster, cx| {
toaster.push(Toast::new(reason).severity(Severity::Warning), cx);
});
cx.notify();
return;
}
self.ran(id, cx);
}
/// Shows `text` in the window's toaster.
pub fn toast(
&mut self,
text: impl Into<SharedString>,
severity: Severity,
cx: &mut Context<Self>,
) {
self.toaster.update(cx, |toaster, cx| {
toaster.push(Toast::new(text.into()).severity(severity), cx);
});
}
/// A toast whose button opens the output `path` (§16).
pub fn toast_output(
&mut self,
text: impl Into<SharedString>,
severity: Severity,
path: PathBuf,
cx: &mut Context<Self>,
) {
let root = cx.weak_entity();
let open = crate::i18n::text(cx, "toast.open", &[]);
let toast = Toast::new(text.into())
.severity(severity)
.action(open, move |_, cx| {
let (root, path) = (root.clone(), path.clone());
// Opened off the UI thread; a failure comes back.
cx.spawn(async move |cx| {
let opened = cx.background_spawn(async move { view(&path) }).await;
if let Err(error) = opened {
log::error!("apps: the output did not open: {error}");
cx.update(|cx| {
let said = crate::i18n::text(cx, "toast.open_failed", &[("error", &error.to_string())]);
root.update(cx, |root, cx| root.toast(said, Severity::Danger, cx)).ok();
});
}
})
.detach();
});
self.toaster.update(cx, |toaster, cx| {
toaster.push(toast, cx);
});
}
/// Starts another Ely app; a failure says why.
fn switch(&mut self, app: AppId, cx: &mut Context<Self>) {
cx.spawn(async move |root, cx| {
let opened = cx.background_spawn(async move { ely_platform::apps::open(app, &[]) }).await;
if let Err(error) = opened {
log::error!("apps: {} did not open: {error}", app.name());
root.update(cx, |root, cx| {
let said = crate::i18n::text(cx, "menu.app.switch_failed", &[("app", app.name()), ("error", &error.to_string())]);
root.toast(said, Severity::Danger, cx);
})
.ok();
}
})
.detach();
}
pub fn close_palette(&mut self, cx: &mut Context<Self>) {
self.palette = false;
cx.notify();
}
fn ran(&mut self, id: &'static str, cx: &mut Context<Self>) {
log::info!("command: {id} ran in {}", self.app.name());
self.last = Some(id.into());
self.recent.retain(|value| value != id);
self.recent.insert(0, id.into());
self.recent.truncate(5);
cx.notify();
}
}
impl Render for Root {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let this = cx.entity();
let body = match &self.content {
Some(view) => view.clone().into_any_element(),
None => Shell::new(self.app.name(), &self.focus).into_any_element(),
};
let has = |id| self.file.iter().any(|command| command.id == id);
let frame = div()
.size_full()
.key_context(commands::CONTEXT)
.on_action(cx.listener(|this, _: &New, _, cx| this.attempt("file.new", cx)))
.on_action(cx.listener(|this, _: &Open, _, cx| this.attempt("file.open", cx)))
.when(has("file.save"), |frame| {
frame
.on_action(cx.listener(|this, _: &Save, _, cx| this.attempt("file.save", cx)))
.on_action(
cx.listener(|this, _: &SaveAs, _, cx| this.attempt("file.save_as", cx)),
)
.on_action(
cx.listener(|this, _: &Export, _, cx| this.attempt("file.export", cx)),
)
.on_action(cx.listener(|this, _: &Print, _, cx| this.attempt("file.print", cx)))
})
.on_action(cx.listener(|this, _: &OpenElyDoc, _, cx| this.switch(AppId::Doc, cx)))
.on_action(cx.listener(|this, _: &OpenElyTable, _, cx| this.switch(AppId::Table, cx)))
.on_action(cx.listener(|this, _: &OpenElyShow, _, cx| this.switch(AppId::Show, cx)))
.on_action(cx.listener(|this, _: &OpenElyPdf, _, cx| this.switch(AppId::Pdf, cx)))
.on_action(cx.listener(|this, _: &OpenElyMail, _, cx| this.switch(AppId::Mail, cx)))
.on_action(cx.listener(|this, _: &OpenElyTree, _, cx| this.switch(AppId::Tree, cx)))
.on_action(cx.listener(|this, _: &OpenElyMark, _, cx| this.switch(AppId::Mark, cx)))
.on_action(cx.listener(|this, _: &OpenElyAgent, _, cx| this.switch(AppId::Agent, cx)))
.on_action(cx.listener(|this, _: &ToggleCommandPalette, window, cx| {
if this.palette {
window.on_next_frame(|window, cx| {
window.dispatch_keystroke(
Keystroke {
key: "escape".into(),
..Default::default()
},
cx,
);
});
} else {
this.palette = true;
cx.notify();
}
}))
.child(body);
let frame = frame.child(raise(
"toasts",
ToastViewport::new("toast-viewport", &self.toaster),
));
if !self.palette {
return frame.into_any_element();
}
let palette = CommandPalette::new("command-palette", move |_, cx| {
this.update(cx, |this, cx| this.close_palette(cx));
})
.group(
crate::i18n::text(cx, "palette.group.file", &[]),
self.file.iter().map(|c| {
PaletteCommand::new(
c.id,
crate::i18n::text(cx, &format!("command.{}", c.id), &[]),
)
.keys(c.keys)
}),
)
.recent(self.recent.clone())
.on_run(move |value, window, cx| commands::run(value, window, cx));
frame.child(raise("palette", palette)).into_any_element()
}
}
/// PDFs open in ElyPDF if installed; others, the system's app.
fn view(path: &Path) -> std::io::Result<()> {
let pdf = path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("pdf"));
if pdf && ely_platform::apps::installed(AppId::Pdf) {
return ely_platform::apps::open(AppId::Pdf, &[path.to_path_buf()]);
}
ely_platform::apps::open_default(path)
}
+138
View File
@@ -0,0 +1,138 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use gpui::{App, AppContext as _, Context, Entity, SharedString};
/// Where a long task stands: range, state, error.
pub enum TaskState {
Running { done: u32, total: u32 },
Succeeded,
Failed(SharedString),
Cancelled,
}
/// A beat of work; the flag stops it early.
type Work = dyn Fn(u32, &AtomicBool) -> Result<(), SharedString> + Send + Sync;
/// A named background task with progress, retry, and a cancel.
pub struct LongTask {
name: SharedString,
state: TaskState,
total: u32,
work: Arc<Work>,
stop: Arc<AtomicBool>,
halt: Option<gpui::Task<()>>,
}
impl LongTask {
pub fn name(&self) -> &SharedString {
&self.name
}
pub fn state(&self) -> &TaskState {
&self.state
}
/// Runs `work` off-thread; cancel lands at the next checkpoint.
pub fn start(
name: impl Into<SharedString>,
total: u32,
work: impl Fn(u32, &AtomicBool) -> Result<(), SharedString> + Send + Sync + 'static,
cx: &mut App,
) -> Entity<Self> {
let entity = cx.new(|_| LongTask {
name: name.into(),
state: TaskState::Running { done: 0, total },
total,
work: Arc::new(work),
stop: Arc::new(AtomicBool::new(false)),
halt: None,
});
entity.update(cx, |this, cx| this.spawn_loop(cx));
entity
}
/// Asks the loop to stop; it reports Cancelled once stopped.
pub fn cancel(&mut self, _: &mut Context<Self>) {
if matches!(self.state, TaskState::Running { .. }) {
self.stop.store(true, Ordering::Relaxed);
}
}
/// Runs the same work again after a failure.
pub fn retry(&mut self, cx: &mut Context<Self>) {
if !matches!(self.state, TaskState::Failed(_)) {
return;
}
log::info!("task: {} retried", self.name);
self.stop.store(false, Ordering::Relaxed);
self.state = TaskState::Running {
done: 0,
total: self.total,
};
cx.notify();
self.spawn_loop(cx);
}
fn spawn_loop(&mut self, cx: &mut Context<Self>) {
let name = self.name.clone();
let total = self.total;
let work = self.work.clone();
let stop = self.stop.clone();
self.halt = Some(cx.spawn(async move |this, cx| {
for done in 1..=total {
if stop.load(Ordering::Relaxed) {
return Self::finish(&this, cx, TaskState::Cancelled, &name);
}
let result = {
let work = work.clone();
let stop = stop.clone();
cx.background_executor()
.spawn(async move { work(done, &stop) })
.await
};
match result {
Err(error) => {
return Self::finish(&this, cx, TaskState::Failed(error), &name);
}
Ok(()) if stop.load(Ordering::Relaxed) => {
return Self::finish(&this, cx, TaskState::Cancelled, &name);
}
Ok(()) => {
let step = this.update(cx, |this, cx| {
this.state = TaskState::Running { done, total };
log::info!("task: {} {done}/{total}", this.name);
cx.notify();
});
if let Err(error) = step {
log::error!("task: {name} lost its owner: {error:#}");
return;
}
}
}
}
Self::finish(&this, cx, TaskState::Succeeded, &name);
}));
}
fn finish(
this: &gpui::WeakEntity<Self>,
cx: &mut gpui::AsyncApp,
state: TaskState,
name: &SharedString,
) {
match &state {
TaskState::Succeeded => log::info!("task: {name} done"),
TaskState::Cancelled => log::info!("task: {name} cancelled"),
TaskState::Failed(error) => log::info!("task: {name} failed: {error}"),
TaskState::Running { .. } => unreachable!("a finish has no progress"),
}
let done = this.update(cx, |this, cx| {
this.state = state;
cx.notify();
});
if let Err(error) = done {
log::error!("task: {name} lost its owner: {error:#}");
}
}
}
+134
View File
@@ -0,0 +1,134 @@
use ely_gpui_component::theme::{Mode, Theme};
use ely_platform::settings::{Appearance, Shared};
use ely_platform::{accessibility, env, omarchy_current_dir};
use gpui::{App, Global, Window, WindowAppearance};
pub mod omarchy;
mod watch;
use omarchy::OmarchyTheme;
/// `omarchy_sync` lets System follow Omarchy where present.
#[derive(Clone, Debug, PartialEq)]
pub struct ThemeSettings {
pub appearance: Appearance,
pub omarchy_sync: bool,
}
impl ThemeSettings {
/// The settings, then `ELY_APPEARANCE` and `ELY_OMARCHY_SYNC` over them.
fn of(shared: &Shared) -> Self {
let appearance = match env::value("ELY_APPEARANCE").as_deref() {
Some("light") => Appearance::Light,
Some("dark") => Appearance::Dark,
Some("system") => Appearance::System,
None => shared.appearance,
Some(other) => {
log::warn!("theme: ELY_APPEARANCE={other} is not system, light or dark, ignored");
shared.appearance
}
};
Self {
appearance,
omarchy_sync: env::flag("ELY_OMARCHY_SYNC").unwrap_or(shared.omarchy_sync),
}
}
}
impl Global for ThemeSettings {}
/// The theme Omarchy last gave us.
#[derive(Default)]
struct OmarchyState(Option<OmarchyTheme>);
impl Global for OmarchyState {}
/// Applies motion and appearance settings, and starts following Omarchy.
pub fn install(cx: &mut App) {
motion(cx);
cx.set_global(ThemeSettings::of(crate::settings::current(cx)));
cx.set_global(OmarchyState::default());
match omarchy_current_dir() {
Some(dir) => watch::start(dir, cx),
None => log::info!("theme: no Omarchy on this machine"),
}
apply(cx, true);
}
/// Follows changed settings: motion, appearance and Omarchy sync.
pub(crate) fn settings_changed(cx: &mut App) {
motion(cx);
cx.set_global(ThemeSettings::of(crate::settings::current(cx)));
apply(cx, false);
}
fn motion(cx: &mut App) {
let reduce = accessibility::reduce_motion(crate::settings::current(cx).motion);
cx.set_reduce_motion(reduce);
Theme::update(cx, |theme| theme.reduced_motion = reduce);
log::info!("theme: reduced motion {}", if reduce { "on" } else { "off" });
}
/// Re-applies the theme when the system appearance changes.
pub fn follow_system(window: &mut Window) {
window
.observe_window_appearance(|_, cx| apply(cx, false))
.detach();
}
fn apply(cx: &mut App, at_once: bool) {
let settings = cx.global::<ThemeSettings>().clone();
let omarchy = cx.global::<OmarchyState>().0.clone();
let set_mode = |mode: Mode, cx: &mut App| {
if at_once {
Theme::set_mode_now(mode, cx);
} else {
Theme::set_mode(mode, cx);
}
};
if let Some(theme) =
omarchy.filter(|_| settings.appearance == Appearance::System && settings.omarchy_sync)
{
let other = if theme.mode == Mode::Dark {
Mode::Light
} else {
Mode::Dark
};
Theme::set_palette(theme.mode, Some(theme.palette.clone()), cx);
Theme::set_palette(other, None, cx);
cx.set_window_appearance(Some(window_appearance(theme.mode)));
set_mode(theme.mode, cx);
log::info!(
"theme: omarchy {} applied, {:?}",
theme.name.as_deref().unwrap_or("unnamed"),
theme.mode
);
return;
}
Theme::set_palette(Mode::Light, None, cx);
Theme::set_palette(Mode::Dark, None, cx);
let mode = match settings.appearance {
Appearance::System => {
cx.set_window_appearance(None);
Mode::from(cx.window_appearance())
}
Appearance::Light => {
cx.set_window_appearance(Some(WindowAppearance::Light));
Mode::Light
}
Appearance::Dark => {
cx.set_window_appearance(Some(WindowAppearance::Dark));
Mode::Dark
}
};
set_mode(mode, cx);
}
fn window_appearance(mode: Mode) -> WindowAppearance {
match mode {
Mode::Light => WindowAppearance::Light,
Mode::Dark => WindowAppearance::Dark,
}
}
+164
View File
@@ -0,0 +1,164 @@
use std::collections::BTreeMap;
use anyhow::{Context, Result, bail};
use ely_gpui_component::theme::{Mix, Mode};
use gpui::{Hsla, hsla, rgb};
const WHITE: Hsla = hsla(0.0, 0.0, 1.0, 1.0);
pub(super) const HUE_NAMES: [&str; 6] = ["red", "green", "yellow", "blue", "magenta", "cyan"];
const HUE_KEYS: [&[&str]; 6] = [
&["red", "color1"],
&["green", "color2"],
&["yellow", "color3"],
&["blue", "color4"],
&["magenta", "color5", "purple"],
&["cyan", "color6"],
];
const BRIGHT_KEYS: [&[&str]; 6] = [
&["bright_red", "color9"],
&["bright_green", "color10"],
&["bright_yellow", "color11"],
&["bright_blue", "color12"],
&["bright_magenta", "color13", "bright_purple"],
&["bright_cyan", "color14"],
];
/// A theme's colors, resolved as `omarchy-theme-color` does.
pub(super) struct Colors {
pub mode: Mode,
pub bg: Hsla,
pub fg: Hsla,
pub bright_fg: Hsla,
pub accent: Option<Hsla>,
pub selection: Hsla,
/// In `HUE_NAMES` order; none if the theme lacks it.
pub hues: [Option<Hsla>; 6],
pub bright: [Option<Hsla>; 6],
pub orange: Option<Hsla>,
}
struct Raw(BTreeMap<String, String>);
impl Raw {
/// Reads lines the way Omarchy does.
fn parse(text: &str) -> Self {
let mut pairs = BTreeMap::new();
for line in text.lines() {
let (key, value) = line.split_once('=').unwrap_or((line, ""));
let key = key.replace(['"', '\'', ' '], "");
if key.is_empty() || key.starts_with('#') {
continue;
}
if !key
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
log::warn!(
"theme: omarchy colors skip '{key}', its name has unsupported characters"
);
continue;
}
let value = match value.find(['"', '\'']) {
Some(open) => {
let rest = &value[open + 1..];
rest[..rest.find(['"', '\'']).unwrap_or(rest.len())].to_string()
}
None => value.trim().to_string(),
};
if !value
.chars()
.all(|c| c.is_ascii_alphanumeric() || "#(),._+/% -".contains(c))
{
log::warn!(
"theme: omarchy colors skip '{key}', its value has unsupported characters"
);
continue;
}
pairs.insert(key, value);
}
Self(pairs)
}
fn text(&self, key: &str) -> Option<&str> {
self.0
.get(key)
.map(String::as_str)
.filter(|value| !value.is_empty())
}
/// The first of `keys` the theme sets, as a color.
fn pick(&self, keys: &[&str]) -> Result<Option<Hsla>> {
let Some(key) = keys.iter().find(|key| self.text(key).is_some()) else {
return Ok(None);
};
hex(self.text(key).unwrap_or_default())
.map(Some)
.with_context(|| format!("key '{key}'"))
}
fn need(&self, keys: &[&str]) -> Result<Hsla> {
self.pick(keys)?
.with_context(|| format!("'{}' is missing", keys[0]))
}
}
/// `light_file`: the theme directory holds a `light.mode` file.
pub(super) fn read(text: &str, light_file: bool) -> Result<Colors> {
let raw = Raw::parse(text);
let bg = raw.need(&["background", "bg", "color0"])?;
let fg = raw.need(&["foreground", "fg", "color7"])?;
let mut hues = [None; 6];
let mut bright = [None; 6];
for i in 0..6 {
hues[i] = raw.pick(HUE_KEYS[i])?;
bright[i] = match raw.pick(BRIGHT_KEYS[i])? {
Some(color) => Some(color),
None => hues[i].map(|hue| hue.mix(&WHITE, 0.2)),
};
}
Ok(Colors {
mode: mode(&raw, bg, light_file)?,
bg,
fg,
bright_fg: raw
.pick(&["bright_foreground", "bright_fg", "color15"])?
.unwrap_or(fg),
accent: raw.pick(&["accent"])?,
selection: raw
.pick(&["selection", "selection_background", "color8"])?
.unwrap_or(bg),
orange: raw.pick(&["orange"])?.or(hues[2]),
hues,
bright,
})
}
/// `mode`, then `theme_type`, then `light.mode`, then the background's brightness.
fn mode(raw: &Raw, bg: Hsla, light_file: bool) -> Result<Mode> {
match raw.text("mode").or(raw.text("theme_type")) {
Some("dark") => Ok(Mode::Dark),
Some("light") => Ok(Mode::Light),
Some(other) => bail!("mode is '{other}', not dark or light"),
None if light_file => Ok(Mode::Light),
None => {
let c = bg.to_rgb();
let sum: u32 = [c.r, c.g, c.b]
.iter()
.map(|v| (v * 255.0).round() as u32)
.sum();
Ok(if sum > 382 { Mode::Light } else { Mode::Dark })
}
}
}
pub(super) fn hex(value: &str) -> Result<Hsla> {
match value.strip_prefix('#') {
Some(digits) if digits.len() == 6 && digits.bytes().all(|b| b.is_ascii_hexdigit()) => {
Ok(rgb(u32::from_str_radix(digits, 16)?).into())
}
_ => bail!("'{value}' is not #rrggbb"),
}
}
+66
View File
@@ -0,0 +1,66 @@
use std::{fs, io, path::Path};
use anyhow::{Context, Result};
use ely_gpui_component::theme::{Mode, Palette};
use futures::channel::mpsc::{self, UnboundedReceiver};
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
mod colors;
mod palette;
#[cfg(test)]
mod tests;
/// An Omarchy theme as Ely's palette.
#[derive(Clone, Debug, PartialEq)]
pub struct OmarchyTheme {
pub name: Option<String>,
pub mode: Mode,
pub palette: Palette,
/// Hues the theme lacks, which Ely's own colors fill.
pub standing_in: Vec<&'static str>,
}
/// Reads `<current>/theme/colors.toml`. None when Omarchy has not written one.
pub fn load(current: &Path) -> Result<Option<OmarchyTheme>> {
let theme = current.join("theme");
let file = theme.join("colors.toml");
let text = match fs::read_to_string(&file) {
Ok(text) => text,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error).with_context(|| format!("cannot read {}", file.display())),
};
let name = fs::read_to_string(current.join("theme.name"))
.ok()
.map(|name| name.trim().to_string());
parse(&text, name, theme.join("light.mode").exists())
.with_context(|| format!("{} is not a usable theme", file.display()))
.map(Some)
}
pub fn parse(text: &str, name: Option<String>, light_file: bool) -> Result<OmarchyTheme> {
let colors = colors::read(text, light_file)?;
let (palette, standing_in) = palette::build(&colors)?;
Ok(OmarchyTheme {
name,
mode: colors.mode,
palette,
standing_in,
})
}
/// One `()` per event under `current`; keep the watcher alive.
pub fn changes(current: &Path) -> Result<(RecommendedWatcher, UnboundedReceiver<()>)> {
let (sender, receiver) = mpsc::unbounded();
let mut watcher =
notify::recommended_watcher(move |event: notify::Result<notify::Event>| match event {
Ok(_) => {
let _ = sender.unbounded_send(());
}
Err(error) => log::warn!("theme: watching omarchy failed: {error}"),
})
.context("cannot start the file watcher")?;
watcher
.watch(current, RecursiveMode::NonRecursive)
.with_context(|| format!("cannot watch {}", current.display()))?;
Ok((watcher, receiver))
}
+210
View File
@@ -0,0 +1,210 @@
use anyhow::{Result, bail};
use ely_gpui_component::{
devtools::{contrast, over},
theme::{Mix, Mode, Palette, Syntax},
};
use gpui::{Hsla, hsla};
use super::colors::{Colors, HUE_NAMES};
const TEXT: f32 = 4.5;
const MARK: f32 = 3.0;
const BLACK: Hsla = hsla(0.0, 0.0, 0.0, 1.0);
const WHITE: Hsla = hsla(0.0, 0.0, 1.0, 1.0);
const SURFACE: f32 = 0.04;
const OVERLAY: f32 = 0.06;
const SUNKEN: f32 = 0.08;
const HOVER: f32 = 0.08;
const ACTIVE: f32 = 0.14;
const SUBTLE: f32 = 0.16;
const SURFACES: &[&str] = &["bg", "surface", "sunken", "overlay", "hover", "active"];
/// Mirrors the library's A11yChecker: text colors over their surfaces.
const TEXT_PAIRS: &[(&str, &[&str])] = &[
("fg", SURFACES),
("fg_muted", SURFACES),
("fg_subtle", SURFACES),
("link", &["bg", "surface", "sunken"]),
("on_accent", &["accent", "accent_hover"]),
("tooltip_fg", &["tooltip_bg"]),
("success", &["bg", "success_subtle"]),
("warning", &["bg", "warning_subtle"]),
("danger", &["bg", "danger_subtle"]),
("info", &["bg", "info_subtle"]),
];
const CODE_SURFACES: [&str; 3] = ["surface", "sunken", "hover"];
/// `color` via `via` to `extreme`, until it reads `need`:1.
fn lift(color: Hsla, grounds: &[Hsla], via: Hsla, extreme: Hsla, need: f32) -> Hsla {
let toward = |from: Hsla, to: Hsla, from_step: u32| {
(from_step..=20).map(move |step| from.mix(&to, step as f32 / 20.0))
};
toward(color, via, 0)
.chain(toward(via, extreme, 1))
.find(|candidate| grounds.iter().all(|g| contrast(*candidate, *g) >= need))
.unwrap_or(extreme)
}
/// The palette, and the hues Ely's colors replace.
pub(super) fn build(c: &Colors) -> Result<(Palette, Vec<&'static str>)> {
let mut p = match c.mode {
Mode::Dark => Palette::dark(false),
Mode::Light => Palette::light(false),
};
let bg = c.bg;
let tint = |t: f32| bg.mix(&c.fg, t);
let grounds = [
bg,
tint(SURFACE),
tint(OVERLAY),
tint(SUNKEN),
tint(HOVER),
tint(ACTIVE),
];
let worst = |color: Hsla| {
grounds
.iter()
.map(|g| contrast(color, *g))
.fold(f32::MAX, f32::min)
};
let extreme = if worst(WHITE) > worst(BLACK) {
WHITE
} else {
BLACK
};
let fg = lift(c.fg, &grounds, extreme, extreme, TEXT);
let lifted = |color: Hsla, grounds: &[Hsla], need: f32| lift(color, grounds, fg, extreme, need);
let text = |color: Hsla| lifted(color, &grounds, TEXT);
let mut standing_in = Vec::new();
let mut hue = |i: usize, ely: Hsla| {
c.hues[i].unwrap_or_else(|| {
standing_in.push(HUE_NAMES[i]);
ely
})
};
let (red, green, yellow, blue) = (
hue(0, p.danger),
hue(1, p.success),
hue(2, p.warning),
hue(3, p.info),
);
let (magenta, cyan) = (hue(4, p.chart[4]), hue(5, p.chart[1]));
let orange = c.orange.unwrap_or(yellow);
let accent = c.accent.or(c.hues[3]).unwrap_or_else(|| {
standing_in.push("accent");
p.accent
});
let bright = |i: usize, hue: Hsla| c.bright[i].unwrap_or(hue);
p.bg = bg;
(p.surface, p.overlay, p.sunken, p.hover, p.active) =
(grounds[1], grounds[2], grounds[3], grounds[4], grounds[5]);
p.border = tint(0.16);
p.border_strong = tint(0.32);
p.fg = fg;
p.fg_muted = text(fg.mix(&bg, 0.25));
p.fg_subtle = text(fg.mix(&bg, 0.45));
p.fg_disabled = fg.mix(&bg, 0.65);
p.accent = accent;
p.on_accent = if contrast(WHITE, accent) > contrast(BLACK, accent) {
WHITE
} else {
BLACK
};
let away = if p.on_accent == WHITE { BLACK } else { WHITE };
p.accent_hover = [accent.mix(&fg, 0.12), accent.mix(&away, 0.12)]
.into_iter()
.find(|hover| contrast(p.on_accent, *hover) >= TEXT)
.unwrap_or(accent);
p.focus = lifted(accent, &grounds[..2], MARK);
p.link = text(accent);
p.selection = c.selection;
p.success_subtle = bg.mix(&green, SUBTLE);
p.warning_subtle = bg.mix(&yellow, SUBTLE);
p.danger_subtle = bg.mix(&red, SUBTLE);
p.info_subtle = bg.mix(&blue, SUBTLE);
p.success = lifted(green, &[bg, p.success_subtle], TEXT);
p.warning = lifted(yellow, &[bg, p.warning_subtle], TEXT);
p.danger = lifted(red, &[bg, p.danger_subtle], TEXT);
p.info = lifted(blue, &[bg, p.info_subtle], TEXT);
p.tooltip_bg = fg;
p.tooltip_fg = bg;
p.chart = [
blue,
cyan,
yellow,
red,
magenta,
green,
orange,
bright(5, cyan),
]
.map(|hue| lifted(hue, &[bg], MARK));
p.ansi = [
bg,
red,
green,
yellow,
blue,
magenta,
cyan,
fg,
p.fg_subtle,
bright(0, red),
bright(1, green),
bright(2, yellow),
bright(3, blue),
bright(4, magenta),
bright(5, cyan),
c.bright_fg,
];
let s = &mut p.syntax;
s.keyword = text(magenta);
s.string = text(green);
s.number = text(orange);
s.comment = p.fg_subtle;
s.function = text(blue);
s.type_name = text(yellow);
s.constant = text(orange);
s.property = text(cyan);
s.tag = text(red);
s.attribute = text(yellow);
s.operator = text(cyan);
s.punctuation = p.fg_muted;
s.variable = fg;
verify(&p)?;
Ok((p, standing_in))
}
/// Fails on the first pair the library's audit fails.
fn verify(p: &Palette) -> Result<()> {
let check = |front: &str, color: Hsla, back: &str, need: f32| -> Result<()> {
let ground = p.token(back);
let ratio = contrast(over(color, ground), ground);
if ratio < need {
bail!("{front} on {back} reads {ratio:.2}:1, needs {need}:1");
}
Ok(())
};
for (front, backs) in TEXT_PAIRS {
for back in *backs {
check(front, p.token(front), back, TEXT)?;
}
}
for name in Syntax::NAMES {
for back in CODE_SURFACES {
check(&format!("syntax.{name}"), p.syntax.token(name), back, TEXT)?;
}
}
for back in ["bg", "surface"] {
check("focus", p.focus, back, MARK)?;
}
for (ix, hue) in p.chart.iter().enumerate() {
check(&format!("chart hue {ix}"), *hue, "bg", MARK)?;
}
Ok(())
}
+316
View File
@@ -0,0 +1,316 @@
use std::{fs, thread, time::Duration};
use ely_gpui_component::{
devtools::contrast,
theme::{Mix, Mode, Palette},
};
use futures::channel::mpsc::UnboundedReceiver;
use super::{OmarchyTheme, changes, colors::hex, load, parse};
use gpui::Hsla;
const BUNDLED: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/omarchy");
const GENERATED: &str = include_str!("../../../tests/fixtures/omarchy-generated/tokyo-night.toml");
const TOKYO_NIGHT: &str = include_str!("../../../tests/fixtures/omarchy/tokyo-night.toml");
const LATTE: &str = include_str!("../../../tests/fixtures/omarchy/catppuccin-latte.toml");
fn bundled() -> Vec<(String, String)> {
let mut themes: Vec<_> = fs::read_dir(BUNDLED)
.unwrap()
.map(|entry| entry.unwrap().path())
.map(|path| {
let name = path.file_stem().unwrap().to_string_lossy().into_owned();
(name, fs::read_to_string(path).unwrap())
})
.collect();
themes.sort();
themes
}
fn value_of(text: &str, key: &str) -> String {
let line = text
.lines()
.find(|line| line.starts_with(&format!("{key} =")))
.unwrap();
line.split('"').nth(1).unwrap().to_string()
}
/// The library's contrast rules, written out apart from `verify`.
fn assert_contract(name: &str, theme: &OmarchyTheme) {
let p = &theme.palette;
let s = &p.syntax;
let surfaces = [p.bg, p.surface, p.overlay, p.sunken, p.hover, p.active];
let on = |token: &str, color: Hsla, grounds: &[Hsla], need: f32| {
for (ix, ground) in grounds.iter().enumerate() {
let ratio = contrast(color, *ground);
assert!(
ratio >= need,
"{name}: {token} on ground {ix} reads {ratio:.2}"
);
}
};
for (token, color) in [
("fg", p.fg),
("fg_muted", p.fg_muted),
("fg_subtle", p.fg_subtle),
("link", p.link),
("keyword", s.keyword),
("string", s.string),
("number", s.number),
("comment", s.comment),
("function", s.function),
("type_name", s.type_name),
("constant", s.constant),
("property", s.property),
("tag", s.tag),
("attribute", s.attribute),
("operator", s.operator),
("punctuation", s.punctuation),
("variable", s.variable),
] {
on(token, color, &surfaces, 4.5);
}
for (token, color, subtle) in [
("success", p.success, p.success_subtle),
("warning", p.warning, p.warning_subtle),
("danger", p.danger, p.danger_subtle),
("info", p.info, p.info_subtle),
] {
on(token, color, &[p.bg, subtle], 4.5);
}
on("on_accent", p.on_accent, &[p.accent, p.accent_hover], 4.5);
on("tooltip_fg", p.tooltip_fg, &[p.tooltip_bg], 4.5);
on("focus", p.focus, &[p.bg, p.surface], 3.0);
for (ix, hue) in p.chart.iter().enumerate() {
on(&format!("chart {ix}"), *hue, &[p.bg], 3.0);
}
}
#[test]
fn every_bundled_theme_maps_with_its_colors_and_text_that_reads_on_every_surface() {
let themes = bundled();
assert!(themes.len() >= 22, "only {} fixtures", themes.len());
for (name, text) in themes {
let theme =
parse(&text, Some(name.clone()), false).unwrap_or_else(|e| panic!("{name}: {e:#}"));
let mode = if value_of(&text, "mode") == "dark" {
Mode::Dark
} else {
Mode::Light
};
assert_eq!(theme.mode, mode, "{name}");
assert!(
theme.standing_in.is_empty(),
"{name}: {:?}",
theme.standing_in
);
assert_eq!(
theme.palette.bg,
hex(&value_of(&text, "background")).unwrap(),
"{name}"
);
assert_eq!(
theme.palette.accent,
hex(&value_of(&text, "accent")).unwrap(),
"{name}"
);
assert_eq!(
theme.palette.selection,
hex(&value_of(&text, "selection")).unwrap(),
"{name}"
);
let red = hex(&value_of(&text, "red")).unwrap();
let reads = |ground| contrast(red, ground) >= 4.5;
if reads(theme.palette.bg) && reads(theme.palette.danger_subtle) {
assert_eq!(
theme.palette.danger, red,
"{name}: a red that reads is kept"
);
}
assert_contract(&name, &theme);
}
}
#[test]
fn a_theme_whose_text_already_reads_keeps_its_foreground() {
let theme = parse(TOKYO_NIGHT, None, false).unwrap();
assert_eq!(theme.palette.fg, hex("#a9b1d6").unwrap());
}
#[test]
fn a_generated_theme_with_only_colorn_keys_gets_its_hues_through_omarchys_aliases() {
let theme = parse(GENERATED, None, false).unwrap();
let p = &theme.palette;
assert_eq!(theme.mode, Mode::Dark);
assert!(theme.standing_in.is_empty(), "{:?}", theme.standing_in);
assert_eq!(p.danger, hex("#f7768e").unwrap());
assert_eq!(p.success, hex("#9ece6a").unwrap());
assert_eq!(p.warning, hex("#e0af68").unwrap());
assert_eq!(p.info, hex("#7aa2f7").unwrap());
assert_eq!(p.chart[4], hex("#ad8ee6").unwrap());
assert_eq!(p.chart[1], hex("#449dab").unwrap());
assert_eq!(p.chart[7], hex("#0db9d7").unwrap());
assert_eq!(p.ansi[9], hex("#ff7a93").unwrap());
assert_eq!(p.ansi[15], hex("#c0caf5").unwrap());
assert_contract("generated", &theme);
}
#[test]
fn bright_hues_and_orange_follow_omarchys_derivations_when_absent() {
let bare = "background = \"#101010\"\nforeground = \"#e0e0e0\"\nred = \"#c04040\"\nyellow = \"#c0a040\"\n";
let theme = parse(bare, None, false).unwrap();
let p = &theme.palette;
assert_eq!(
p.ansi[9],
hex("#c04040").unwrap().mix(&hex("#ffffff").unwrap(), 0.2)
);
assert_eq!(p.chart[6], hex("#c0a040").unwrap());
}
#[test]
fn a_theme_that_lacks_a_hue_is_told_apart_from_one_that_has_it() {
let no_red = TOKYO_NIGHT.replace("red = \"#f7768e\"", "");
let theme = parse(&no_red, None, false).unwrap();
assert_eq!(theme.standing_in, ["red"]);
assert_eq!(theme.palette.danger, Palette::dark(false).danger);
let bare = parse(
"background = \"#101010\"\nforeground = \"#e0e0e0\"",
None,
false,
)
.unwrap();
assert_eq!(
bare.standing_in,
[
"red", "green", "yellow", "blue", "magenta", "cyan", "accent"
]
);
}
#[test]
fn the_reader_takes_omarchys_loose_syntax_and_skips_what_omarchy_skips() {
let loose = "# a comment\n\"background\" = '#1a1b26'\nforeground=#a9b1d6\n accent = \"#7aa2f7\" # note\nmode = \"dark\"\nodd key! = \"#ffffff\"\nname = \"a;b\"\n[table]\n";
let theme = parse(loose, None, false).unwrap();
assert_eq!(theme.palette.bg, hex("#1a1b26").unwrap());
assert_eq!(theme.palette.accent, hex("#7aa2f7").unwrap());
assert_eq!(theme.palette.fg, hex("#a9b1d6").unwrap());
}
#[test]
fn mode_comes_from_mode_then_theme_type_then_the_light_file_then_brightness() {
let colors =
|extra: &str, bg: &str| format!("{extra}\nbackground = \"{bg}\"\nforeground = \"#808080\"");
let mode = |text: String, light_file: bool| parse(&text, None, light_file).unwrap().mode;
assert_eq!(
mode(colors("mode = \"light\"", "#000000"), false),
Mode::Light
);
assert_eq!(
mode(colors("theme_type = \"light\"", "#000000"), false),
Mode::Light
);
assert_eq!(
mode(
colors("mode = \"dark\"\ntheme_type = \"light\"", "#ffffff"),
false
),
Mode::Dark
);
assert_eq!(mode(colors("", "#000000"), true), Mode::Light);
assert_eq!(mode(colors("mode = \"dark\"", "#ffffff"), true), Mode::Dark);
assert_eq!(mode(colors("", "#7f8080"), false), Mode::Light);
assert_eq!(mode(colors("", "#7f7f80"), false), Mode::Dark);
}
#[test]
fn the_cause_is_named_for_a_missing_color_a_bad_color_a_bad_mode_and_unreadable_text() {
let say = |text: &str| format!("{:#}", parse(text, None, false).unwrap_err());
let without = |key: &str| {
TOKYO_NIGHT
.lines()
.filter(|line| !line.starts_with(key))
.collect::<Vec<_>>()
.join("\n")
};
assert!(say(&without("foreground")).contains("'foreground' is missing"));
assert!(say(&without("background")).contains("'background' is missing"));
assert!(
say(&TOKYO_NIGHT.replace("#7aa2f7\"\nselection", "blue\"\nselection")).contains("'accent'")
);
assert!(say(&TOKYO_NIGHT.replace("#f7768e", "#+12345")).contains("'#+12345' is not #rrggbb"));
assert!(
say(&TOKYO_NIGHT.replace("mode = \"dark\"", "mode = \"dim\""))
.contains("not dark or light")
);
assert!(say("not toml at all").contains("'background' is missing"));
assert!(say("background = \"#777777\"\nforeground = \"#000000\"").contains("4.5"));
}
#[test]
fn load_reads_the_theme_its_name_and_its_light_marker_and_a_missing_file_is_none() {
let current = tempfile::tempdir().unwrap();
assert!(load(current.path()).unwrap().is_none());
let theme_dir = current.path().join("theme");
fs::create_dir(&theme_dir).unwrap();
fs::write(theme_dir.join("colors.toml"), LATTE).unwrap();
fs::write(current.path().join("theme.name"), "catppuccin-latte\n").unwrap();
let theme = load(current.path()).unwrap().expect("a theme");
assert_eq!(theme.name.as_deref(), Some("catppuccin-latte"));
assert_eq!(theme.mode, Mode::Light);
fs::write(
theme_dir.join("colors.toml"),
"background = \"#000000\"\nforeground = \"#ffffff\"",
)
.unwrap();
assert_eq!(load(current.path()).unwrap().unwrap().mode, Mode::Dark);
fs::write(theme_dir.join("light.mode"), "").unwrap();
assert_eq!(load(current.path()).unwrap().unwrap().mode, Mode::Light);
fs::write(theme_dir.join("colors.toml"), "mode = \"dark\"").unwrap();
let error = format!("{:#}", load(current.path()).unwrap_err());
assert!(
error.contains("colors.toml") && error.contains("'background' is missing"),
"{error}"
);
}
fn event_within(changes: &mut UnboundedReceiver<()>, tries: u32) -> bool {
(0..tries).any(|_| {
thread::sleep(Duration::from_millis(50));
changes.try_recv().is_ok()
})
}
#[test]
fn changes_sees_a_theme_swap_in_the_order_omarchy_does_it() {
let current = tempfile::tempdir().unwrap();
let (_watcher, mut events) = changes(current.path()).unwrap();
fs::create_dir(current.path().join("theme")).unwrap();
assert!(event_within(&mut events, 60), "no event for a new theme");
while events.try_recv().is_ok() {}
fs::create_dir(current.path().join("next-theme")).unwrap();
fs::remove_dir_all(current.path().join("theme")).unwrap();
fs::rename(
current.path().join("next-theme"),
current.path().join("theme"),
)
.unwrap();
fs::write(current.path().join("theme.name"), "nord\n").unwrap();
assert!(
event_within(&mut events, 60),
"no event for a swapped theme"
);
}
+57
View File
@@ -0,0 +1,57 @@
use std::{
path::{Path, PathBuf},
time::Duration,
};
use futures::StreamExt;
use gpui::{App, AsyncApp};
use super::{OmarchyState, apply, omarchy};
const SETTLE: Duration = Duration::from_millis(250);
/// Loads the Omarchy theme now and after each change.
pub(super) fn start(dir: PathBuf, cx: &mut App) {
reload(&dir, cx);
let (watcher, mut changes) = match omarchy::changes(&dir) {
Ok(watch) => watch,
Err(error) => {
log::error!("theme: omarchy sync does not follow changes: {error:#}");
return;
}
};
cx.spawn(async move |cx: &mut AsyncApp| {
let _watcher = watcher;
while changes.next().await.is_some() {
cx.background_executor().timer(SETTLE).await;
while changes.try_recv().is_ok() {}
cx.update(|cx| reload(&dir, cx));
}
})
.detach();
}
fn reload(dir: &Path, cx: &mut App) {
match omarchy::load(dir) {
Ok(Some(theme)) => {
let name = theme.name.as_deref().unwrap_or("unnamed");
if !theme.standing_in.is_empty() {
log::warn!(
"theme: omarchy {name} lacks {}, Ely's colors stand in",
theme.standing_in.join(", ")
);
}
cx.global_mut::<OmarchyState>().0 = Some(theme);
apply(cx, false);
}
Ok(None) => {
if cx.global_mut::<OmarchyState>().0.take().is_some() {
log::info!("theme: omarchy theme has no colors.toml, Ely's own colors again");
apply(cx, false);
}
}
Err(error) => {
log::error!("theme: omarchy theme ignored, the current colors stay: {error:#}");
}
}
}