Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Repository Guidelines

## Project Structure & Module Organization
- `src/`: core Rust crates for the editor (`bin/edit` for the terminal app, `buffer`, `tui`, `framebuffer`, etc.). Start from `src/bin/edit/main.rs` for entry flow, and `src/bin/edit/state.rs` for global state/preferences.
- `i18n/`: localization sources (`edit.toml`) compiled into `src/bin/edit/localization.rs`.
- `assets/`: icons, branding, and misc resources bundled with releases.
- `scripts/`: helper executables; prefer `scripts/*.sh` over raw cargo commands to match CI settings.

## Build, Test, and Development Commands
- `scripts/build-debug.sh` – debug build (`cargo build`).
- `scripts/build-release.sh` – release build with nightly config (`cargo build --config .cargo/release-nightly.toml --release`).
- `scripts/test.sh [-- <args>]` – run `cargo test`.
- `scripts/check.sh` – `cargo check` (fast validation).
- `scripts/install.sh` – `cargo install --path . --locked`; installs `edit` into `~/.cargo/bin`.

## Coding Style & Naming Conventions
- Use `cargo fmt` (already configured via `rustfmt.toml`); check formatting before committing.
- Modules follow snake_case; types use UpperCamelCase; constants use SCREAMING_SNAKE.
- Prefer `arena_format!` for UI strings needing formatting; keep UI `classname`s stable for the TUI diffing algorithm.

## Testing Guidelines
- Tests leverage Rust’s standard `cargo test` runner; integration tests live alongside modules.
- Add `#[test]` functions near the functionality they cover; name as `test_<feature>_<case>`.
- For rendering or UI changes, add snapshot/debug logs where possible and describe manual test steps in PRs.

## Commit & Pull Request Guidelines
- Commit messages mirror existing history: concise imperative summary (e.g., “Add preferences dialog with colorschemes”).
- PRs should include: purpose/summary, key changes, testing performed (`cargo check`, `scripts/test.sh`, manual steps), and screenshots for UI shifts.
- Reference related issues (`Fixes #123`) when applicable, and keep PRs focused; split large features into iterative commits when possible.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ codegen-units = 16 # Make compiling criterion faster (16 is the default
lto = "thin" # Similarly, speed up linking by a ton

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[target.'cfg(unix)'.dependencies]
libc = "0.2"
Expand Down Expand Up @@ -63,6 +65,4 @@ features = [

[dev-dependencies]
criterion = { version = "0.7", features = ["html_reports"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
zstd = { version = "0.13", default-features = false }
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ winget install Microsoft.Edit
* Rust 1.90 or earlier: `cargo build --config .cargo/release.toml --release`
* otherwise: `cargo build --config .cargo/release-nightly.toml --release`

### Helper Scripts

Instead of memorizing full cargo invocations, you can run the helper scripts in `scripts/`:

* `scripts/build-release.sh` – runs `cargo build --config .cargo/release-nightly.toml --release`
* `scripts/build-debug.sh` – runs `cargo build`
* `scripts/test.sh` – runs `cargo test`
* `scripts/check.sh` – runs `cargo check`
* `scripts/install.sh` – runs `cargo install --path . --locked`

All scripts forward additional CLI arguments to cargo (e.g., `scripts/test.sh -- --ignored`).

### Build Configuration

During compilation you can set various environment variables to configure the build. The following table lists the available configuration options:
Expand Down
144 changes: 144 additions & 0 deletions i18n/edit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ __default__ = [
"ja",
"ko",
"pt_br",
"ro",
"pl",
"ru",
"zh_hans",
"zh_hant",
Expand All @@ -20,10 +22,12 @@ zh = "zh_hans"
[Ctrl]
en = "Ctrl"
de = "Strg"
ro = "Ctrl"

# The keyboard key
[Alt]
en = "Alt"
ro = "Alt"

# The keyboard key
[Shift]
Expand All @@ -34,6 +38,7 @@ es = "Mayús"
fi = "Vaihto"
fr = "Maj"
it = "Maiusc"
ro = "Shift"

# Used as a common dialog button
[Ok]
Expand All @@ -53,6 +58,7 @@ it = "OK"
ja = "OK"
ko = "확인"
nl = "OK"
pl = "OK"
pt_br = "OK"
pt_pt = "OK"
ro = "OK"
Expand Down Expand Up @@ -286,6 +292,11 @@ vi = "Mở tệp…"
zh_hans = "打开文件…"
zh_hant = "開啟檔案…"

[FileOpenRecent]
en = "Open Recent…"
pl = "Otwórz ostatnie…"
ro = "Deschide recent…"

[FileSave]
en = "Save"
bn = "সংরক্ষণ"
Expand Down Expand Up @@ -710,6 +721,10 @@ vi = "Chọn tất cả"
zh_hans = "全选"
zh_hant = "全選"

[EditPreferences]
en = "Preferences"
ro = "Preferințe"

# A menu bar item
[View]
en = "View"
Expand Down Expand Up @@ -1767,3 +1782,132 @@ uk = "Файл вже існує. Перезаписати?"
vi = "Tệp đã tồn tại. Bạn có muốn ghi đè không?"
zh_hans = "文件已存在。要覆盖它吗?"
zh_hant = "檔案已存在。要覆蓋它嗎?"
[PreferencesDialogTitle]
en = "Preferences"
pl = "Preferencje"
ro = "Preferințe"

[PreferencesAutoClose]
en = "Auto close brackets"
pl = "Automatycznie domykaj nawiasy"
ro = "Închide automat parantezele"

[PreferencesGeneral]
en = "General"
pl = "Ogólne"
ro = "General"

[PreferencesLineHighlight]
en = "Highlight current line"
pl = "Podświetlaj bieżący wiersz"
ro = "Evidențiază linia curentă"

[PreferencesColorscheme]
en = "Color scheme"
pl = "Motyw kolorów"
ro = "Schemat de culori"

[PreferencesShowLineNumbers]
en = "Show line numbers"
pl = "Pokaż numery wierszy"
ro = "Afișează numerele liniilor"

[PreferencesWordWrap]
en = "Wrap long lines"
pl = "Zawijaj długie wiersze"
ro = "Încadrează liniile lungi"

[PreferencesIndentWithTabs]
en = "Use tabs for indentation"
pl = "Używaj tabulatorów do wcięć"
ro = "Folosește tabulatori pentru indentare"

[PreferencesTabWidth]
en = "Tab width"
pl = "Szerokość tabulatora"
ro = "Lățimea tabulatorului"

[PreferencesLanguage]
en = "Language"
pl = "Język"
ro = "Limba"

[PreferencesLanguageSystem]
en = "Match system language"
pl = "Język systemu"
ro = "Potrivește limba sistemului"

[PreferencesSchemeSystem]
en = "System"
pl = "Systemowy"
ro = "Sistem"

[PreferencesSchemeMidnight]
en = "Midnight"
pl = "Północ"
ro = "Miez de noapte"

[PreferencesSchemeDaylight]
en = "Daylight"
pl = "Światło dzienne"
ro = "Lumina zilei"

[PreferencesSchemeNord]
en = "Nord"
pl = "Nord"
ro = "Nord"

[PreferencesSchemeHighContrast]
en = "High Contrast"
pl = "Wysoki kontrast"
ro = "Contrast ridicat"

[PreferencesSchemeGruvboxDark]
en = "Gruvbox (Dark)"
pl = "Gruvbox (ciemny)"
ro = "Gruvbox (întunecat)"

[PreferencesSchemeGruvboxLight]
en = "Gruvbox (Light)"
pl = "Gruvbox (jasny)"
ro = "Gruvbox (luminos)"

[PreferencesSchemeDracula]
en = "Dracula"
pl = "Dracula"
ro = "Dracula"

[PreferencesSchemeKanagawa]
en = "Kanagawa"
pl = "Kanagawa"
ro = "Kanagawa"

[PreferencesSchemeTokyonight]
en = "Tokyo Night"
pl = "Tokyo Night"
ro = "Tokyo Night"

[PreferencesSchemeMonokai]
en = "Monokai"
pl = "Monokai"
ro = "Monokai"

[PreferencesSchemeAtom]
en = "Atom One Dark"
pl = "Atom One Dark"
ro = "Atom One Dark"

[RecentFilesDialogTitle]
en = "Recent Files"
pl = "Ostatnie pliki"
ro = "Fișiere recente"

[CommandPaletteTitle]
en = "Command Palette"
pl = "Paleta poleceń"
ro = "Paleta de comenzi"

[CommandPaletteNoResults]
en = "No commands found"
pl = "Brak wyników"
ro = "Nicio comandă găsită"
7 changes: 7 additions & 0 deletions scripts/build-debug.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

cargo build "$@"
7 changes: 7 additions & 0 deletions scripts/build-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

cargo build --config .cargo/release-nightly.toml --release "$@"
7 changes: 7 additions & 0 deletions scripts/check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

cargo check "$@"
9 changes: 9 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

# Installs the edit binary into Cargo's bin dir.
# Additional arguments are forwarded to `cargo install`.
cargo install --path . --locked "$@"
7 changes: 7 additions & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

cargo test "$@"
79 changes: 79 additions & 0 deletions src/base64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ use crate::arena::ArenaString;

const CHARSET: [u8; 64] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

fn decode_value(byte: u8) -> Option<u8> {
match byte {
b'A'..=b'Z' => Some(byte - b'A'),
b'a'..=b'z' => Some(byte - b'a' + 26),
b'0'..=b'9' => Some(byte - b'0' + 52),
b'+' => Some(62),
b'/' => Some(63),
b'=' => Some(64),
b'\r' | b'\n' => None,
_ => Some(0xff),
}
}

/// One aspect of base64 is that the encoded length can be
/// calculated accurately in advance, which is what this returns.
#[inline]
Expand Down Expand Up @@ -77,6 +90,63 @@ pub fn encode(dst: &mut ArenaString, src: &[u8]) {
}
}

/// Decodes a base64 string into raw bytes.
pub fn decode(src: &str) -> Option<Vec<u8>> {
let mut chunk = [0u8; 4];
let mut chunk_len = 0;
let mut out = Vec::with_capacity(src.len().saturating_sub(3) / 4 * 3);

for &byte in src.as_bytes() {
let Some(val) = decode_value(byte) else {
continue;
};
if val == 0xff {
return None;
}
chunk[chunk_len] = val;
chunk_len += 1;

if chunk_len == 4 {
if chunk[0] == 64 || chunk[1] == 64 {
return None;
}

out.push((chunk[0] << 2) | (chunk[1] >> 4));

match chunk[2] {
64 => {
if chunk[3] != 64 {
return None;
}
}
val => {
out.push((chunk[1] << 4) | (val >> 2));
}
}

if let (Some(c), Some(d)) =
((chunk[2] != 64).then_some(chunk[2]), (chunk[3] != 64).then_some(chunk[3]))
{
out.push((c << 6) | d);
} else if chunk[3] != 64 {
return None;
}

if chunk[2] == 64 && chunk[3] != 64 {
return None;
}

chunk_len = 0;
}
}

if chunk_len != 0 {
return None;
}

Some(out)
}

#[cfg(test)]
mod tests {
use super::encode;
Expand Down Expand Up @@ -118,4 +188,13 @@ mod tests {
assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWXY"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldYWQ==");
assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWXYZ"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldYWVo=");
}

#[test]
fn roundtrip_decode() {
let arena = Arena::new(4 * 1024).unwrap();
let mut dst = ArenaString::new_in(&arena);
encode(&mut dst, b"hello clipboard");
let decoded = super::decode(&dst).unwrap();
assert_eq!(decoded, b"hello clipboard");
}
}
Loading