release: v0.1.5 — Event Sourcing, Flatpak build fixes, icons
This commit is contained in:
parent
769d84aa80
commit
a714dca0be
35 changed files with 652 additions and 193 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -15,4 +15,5 @@ status
|
|||
vietc-xrecord
|
||||
packaging/flatpak/build-dir
|
||||
packaging/flatpak/vietc-repo
|
||||
packaging/flatpak/repo
|
||||
packaging/flatpak/VietPlus-*
|
||||
|
|
|
|||
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -1,5 +1,36 @@
|
|||
# Changelog
|
||||
|
||||
## v0.1.5 (2026-06-28)
|
||||
|
||||
### Event Sourcing (privacy-safe architecture)
|
||||
- **EventStore** replaces `Vec<char>` keystroke history — typed `InputEvent`s (`KeyTyped`, `Backspace`, `Flush`, `Paste`) with `push/pop/clear/raw_keystrokes/pattern_hash`
|
||||
- **`Engine::replay_events()`** — stateless replay through fresh BambooEngine (replaces `replay_keystrokes()`)
|
||||
- **`Engine::replay_events_to_commands()`** — computes diff commands (`Type`, `Backspace`) comparing expected vs screen output
|
||||
- **`EventStore::pattern_hash()`** — sha256 of event type sequence; privacy-safe pattern detection without text recovery
|
||||
- **Daemon updated** — all `keystroke_history` references migrated to `event_store`; `replay_and_inject()`, `replay_backspace()`, `word_to_commit()`, `replay_reset()` use new Event Sourcing API
|
||||
|
||||
### Flatpak Build Fixes
|
||||
- **Fixed SDK/RUNTIME swap**: `flatpak build-init` arg order is `SDK` then `RUNTIME`; previous `org.gnome.Platform` as SDK meant `/usr/lib/sdk/` was never mounted
|
||||
- **Rust SDK extension** now auto-mounts at `/usr/lib/sdk/rust-stable/` — no symlinks or file copies needed
|
||||
- **Icons**: renamed to `io.github.vietc.VietPlus.*` prefix (Flatpak export requires app ID prefix for all icon files)
|
||||
- **Desktop file**: removed unregistered `InputMethod` category
|
||||
- **Tray**: `icon_name()` returns Flatpak-prefixed names when running inside Flatpak sandbox (detected via `/app/bin/vietc-daemon`); `icon_pixmap()` programmatic fallback unchanged
|
||||
- **Bundle**: `VietPlus-0.1.5.flatpak` (46 MB, runtime `org.gnome.Platform//50`)
|
||||
|
||||
### Documentation
|
||||
- `packaging/flatpak/FLATPAK_BUILD.md` — detailed build instructions (prerequisites, manual step-by-step, installation)
|
||||
- `RELEASE_CHECKLIST.md` — step-by-step release process (bump version, build, test, push, create release)
|
||||
|
||||
### Licenses
|
||||
- MIT license headers (`// SPDX-License-Identifier: MIT`) on all 22 `.rs` files across 6 crates
|
||||
|
||||
### Icons
|
||||
- `packaging/icons/vietc.svg` — app icon (keyboard + VN badge)
|
||||
- `packaging/icons/vietc-vn.svg` — tray icon (red VN)
|
||||
- `packaging/icons/vietc-en.svg` — tray icon (gray EN)
|
||||
|
||||
---
|
||||
|
||||
## v0.1.4 (2026-06-28)
|
||||
|
||||
### Flatpak Packaging
|
||||
|
|
|
|||
33
README.md
33
README.md
|
|
@ -2,8 +2,9 @@
|
|||
<img src="https://img.shields.io/badge/Platform-Linux-blue?style=for-the-badge" alt="Platform">
|
||||
<img src="https://img.shields.io/badge/Language-Rust-orange?style=for-the-badge" alt="Rust">
|
||||
<img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License">
|
||||
<img src="https://img.shields.io/badge/Version-0.1.4-purple?style=for-the-badge" alt="Version">
|
||||
<img src="https://img.shields.io/badge/Version-0.1.5-purple?style=for-the-badge" alt="Version">
|
||||
<img src="https://img.shields.io/badge/Tests-106_passing-brightgreen?style=for-the-badge" alt="Tests">
|
||||
<img src="https://img.shields.io/badge/Event_Sourcing-✓-blueviolet?style=for-the-badge" alt="Event Sourcing">
|
||||
</p>
|
||||
|
||||
<h1 align="center">
|
||||
|
|
@ -94,31 +95,32 @@ Physical Keyboard
|
|||
and renders Vietnamese text on screen
|
||||
```
|
||||
|
||||
### The Backspace-Replay Pattern
|
||||
### Event Sourcing + Backspace-Replay
|
||||
|
||||
This is Viet+'s core innovation. Traditional IMEs track state incrementally — each keystroke updates an internal buffer. But this buffer can **desync** from what's actually on screen (due to focus changes, external pastes, etc.).
|
||||
|
||||
Viet+ solves this by **never tracking incremental state**:
|
||||
Viet+ uses **Event Sourcing**: every input action is recorded as a typed `InputEvent` (`KeyTyped`, `Backspace`, `Flush`, `Paste`) in an `EventStore`. On every keystroke, the entire event history is **replayed from scratch** through a fresh engine to compute the correct diff — no incremental state to desync.
|
||||
|
||||
```
|
||||
Traditional IME:
|
||||
keystroke → update buffer → emit event → hope it matches screen
|
||||
|
||||
Viet+ (Backspace-Replay):
|
||||
keystroke → add to history → replay ALL history in fresh engine → compute diff
|
||||
Viet+ (Event Sourcing):
|
||||
keystroke → append InputEvent → replay ALL events in fresh engine → compute diff
|
||||
```
|
||||
|
||||
On every keystroke:
|
||||
|
||||
1. The keystroke is appended to `keystroke_history`
|
||||
1. The keystroke is appended as an `InputEvent` to the `EventStore`
|
||||
2. A **brand new** `Engine` is created
|
||||
3. The **entire** history is replayed through it
|
||||
3. The **entire** event history is replayed through it via `Engine::replay_events()`
|
||||
4. The engine's buffer is the **correct** screen output
|
||||
5. Viet+ computes the diff: how many backspaces to erase old text, what new text to type
|
||||
5. Viet+ computes the diff: `Engine::replay_events_to_commands()` returns Type/Backspace commands
|
||||
|
||||
This means:
|
||||
- **Zero state desync** — always recomputed from scratch
|
||||
- **Self-healing** — if anything goes wrong, the next keystroke fixes it
|
||||
- **Privacy-safe** — `EventStore::pattern_hash()` provides a sha256 of the event type sequence for pattern detection without any ability to recover original text
|
||||
- **Simple** — no complex state tracking or synchronization
|
||||
|
||||
---
|
||||
|
|
@ -128,7 +130,8 @@ This means:
|
|||
```
|
||||
vietc/
|
||||
├── engine/ # Vietnamese composition engine (bamboo-core Rust port)
|
||||
│ ├── engine.rs # Orchestrator + replay_keystrokes()
|
||||
│ ├── engine.rs # Orchestrator + replay_events(), replay_events_to_commands()
|
||||
│ ├── event.rs # Event Sourcing: InputEvent, EventStore, Command
|
||||
│ ├── bamboo.rs # Bamboo engine: transformation model, composition, tone placement
|
||||
│ ├── input_method.rs # Telex/VNI rule definitions
|
||||
│ └── spelling.rs # Vietnamese syllable validation
|
||||
|
|
@ -280,10 +283,18 @@ Includes daemon + CLI + system tray + uinput daemon. Sandboxed — no system lib
|
|||
```bash
|
||||
git clone https://github.com/vndangkhoa/vietc.git
|
||||
cd vietc/packaging/flatpak
|
||||
bash build-flatpak.sh
|
||||
bash build-flatpak.sh [version]
|
||||
```
|
||||
|
||||
Requires Flatpak runtime `org.gnome.Platform//50` and Rust SDK extension (installed automatically).
|
||||
Requires Flatpak runtimes: `org.gnome.Platform//50`, `org.gnome.Sdk//50`, `org.freedesktop.Sdk.Extension.rust-stable//25.08`
|
||||
|
||||
```bash
|
||||
flatpak install --user flathub org.gnome.Platform//50
|
||||
flatpak install --user flathub org.gnome.Sdk//50
|
||||
flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
|
||||
```
|
||||
|
||||
See `packaging/flatpak/FLATPAK_BUILD.md` for detailed build instructions.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
88
RELEASE_CHECKLIST.md
Normal file
88
RELEASE_CHECKLIST.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Release Checklist
|
||||
|
||||
## When to release
|
||||
|
||||
- New feature or bugfix that should be distributed to users
|
||||
- Flatpak build changes validated
|
||||
- All tests passing (`cargo test`)
|
||||
|
||||
---
|
||||
|
||||
## Step-by-step
|
||||
|
||||
### 1. Bump version
|
||||
|
||||
Update version in:
|
||||
- `daemon/Cargo.toml`
|
||||
- `cli/Cargo.toml`
|
||||
- `engine/Cargo.toml`
|
||||
- `protocol/Cargo.toml`
|
||||
- `ui/Cargo.toml`
|
||||
- `uinputd/Cargo.toml`
|
||||
- `README.md` version badge
|
||||
|
||||
### 2. Update CHANGELOG.md
|
||||
|
||||
Add a new entry under the version heading:
|
||||
|
||||
```markdown
|
||||
## vX.Y.Z (YYYY-MM-DD)
|
||||
|
||||
### Added
|
||||
- new features...
|
||||
|
||||
### Fixed
|
||||
- bug fixes...
|
||||
|
||||
### Changed
|
||||
- behavior changes...
|
||||
```
|
||||
|
||||
### 3. Build the Flatpak
|
||||
|
||||
```bash
|
||||
cd packaging/flatpak
|
||||
bash build-flatpak.sh X.Y.Z
|
||||
```
|
||||
|
||||
Verify the bundle was created:
|
||||
```bash
|
||||
ls -lh VietPlus-X.Y.Z.flatpak
|
||||
```
|
||||
|
||||
### 4. Test the Flatpak
|
||||
|
||||
```bash
|
||||
flatpak install --user --bundle VietPlus-X.Y.Z.flatpak
|
||||
flatpak run io.github.vietc.VietPlus
|
||||
```
|
||||
|
||||
### 5. Commit and push
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "release: vX.Y.Z — <summary>"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 6. Create a release on Forgejo/GitHub
|
||||
|
||||
Attach the Flatpak bundle (`VietPlus-X.Y.Z.flatpak`) as a release asset.
|
||||
|
||||
```bash
|
||||
# Using forgejo-release (if configured)
|
||||
# Or manually upload via the web UI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick command
|
||||
|
||||
```bash
|
||||
VERSION=X.Y.Z && \
|
||||
sed -i "s/^version = .*/version = \"$VERSION\"/" \
|
||||
daemon/Cargo.toml cli/Cargo.toml engine/Cargo.toml \
|
||||
protocol/Cargo.toml ui/Cargo.toml uinputd/Cargo.toml && \
|
||||
sed -i "s/Version-[0-9.]*-purple/Version-$VERSION-purple/" README.md && \
|
||||
echo "Version bumped to $VERSION"
|
||||
```
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::io::{self, Write};
|
||||
use vietc_engine::{Engine, EngineEvent, InputMethod};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -6,7 +7,7 @@ use std::sync::{Arc, Mutex};
|
|||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use vietc_engine::{Engine, EngineEvent, InputMethod};
|
||||
use vietc_engine::{Engine, EngineEvent, EventStore, InputEvent, InputMethod};
|
||||
|
||||
/// Pin current thread to performance cores (0-3) and boost priority.
|
||||
/// Inspired by VMK's approach to minimize input latency on Intel hybrid CPUs.
|
||||
|
|
@ -110,10 +111,11 @@ struct Daemon {
|
|||
app_state: AppStateManager,
|
||||
engine_enabled: Arc<AtomicBool>,
|
||||
grab_enabled: bool,
|
||||
/// Backspace-Replay: all keystrokes in the current word being composed.
|
||||
/// On each keypress, we replay the entire history through a fresh engine
|
||||
/// to compute the correct screen output, eliminating state desync.
|
||||
keystroke_history: Vec<char>,
|
||||
/// Event Store: append-only log of typed input events.
|
||||
/// On each input, we replay the entire event log through a fresh engine
|
||||
/// to compute the expected screen output, eliminating state desync.
|
||||
/// KHÔNG lưu nội dung nhạy cảm — chỉ lưu event sequence.
|
||||
event_store: EventStore,
|
||||
/// What's currently displayed on screen for the current word.
|
||||
/// Used to calculate how many backspaces we need before retyping.
|
||||
screen_output: String,
|
||||
|
|
@ -154,7 +156,7 @@ impl Daemon {
|
|||
config_modified,
|
||||
app_state,
|
||||
engine_enabled,
|
||||
keystroke_history: Vec::new(),
|
||||
event_store: EventStore::new(),
|
||||
screen_output: String::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -279,11 +281,16 @@ impl Daemon {
|
|||
self.app_state.is_current_app_bypassed()
|
||||
}
|
||||
|
||||
/// Backspace-Replay: replay the entire keystroke history through a fresh
|
||||
/// engine, compute what should be on screen, and return the commands
|
||||
/// Event Sourcing: replay the entire event store through a fresh engine,
|
||||
/// compute what should be on screen, and return the commands
|
||||
/// (backspaces to erase old + new text to type).
|
||||
/// KHÔNG đọc DOM, chỉ dựa trên event sequence.
|
||||
fn replay_and_inject(&mut self, ch: char) -> Vec<OutputCommand> {
|
||||
let mut commands = Vec::new();
|
||||
let method = match self.config.input_method.as_str() {
|
||||
"vni" => InputMethod::Vni,
|
||||
_ => InputMethod::Telex,
|
||||
};
|
||||
|
||||
// Flush characters: commit the current word and type the flush char.
|
||||
// Only backspace + retype when auto-restore actually CHANGES the word
|
||||
|
|
@ -291,6 +298,7 @@ impl Daemon {
|
|||
// already correctly on screen, so retyping it would eat the spacing and
|
||||
// shift the finished word left.
|
||||
if is_flush_char(ch) {
|
||||
self.event_store.push(InputEvent::Flush(ch));
|
||||
let to_commit = self.word_to_commit();
|
||||
if !self.screen_output.is_empty() && to_commit != self.screen_output {
|
||||
let backspaces = self.screen_output.chars().count();
|
||||
|
|
@ -299,37 +307,29 @@ impl Daemon {
|
|||
}
|
||||
// Type the flush character itself
|
||||
commands.push(OutputCommand::Type(ch.to_string()));
|
||||
self.keystroke_history.clear();
|
||||
self.event_store.clear();
|
||||
self.screen_output.clear();
|
||||
return commands;
|
||||
}
|
||||
|
||||
// Add the new keystroke to history
|
||||
self.keystroke_history.push(ch);
|
||||
// Record the typed key as an event
|
||||
self.event_store.push(InputEvent::KeyTyped(ch));
|
||||
|
||||
// Replay through fresh engine
|
||||
let method = match self.config.input_method.as_str() {
|
||||
"vni" => InputMethod::Vni,
|
||||
_ => InputMethod::Telex,
|
||||
};
|
||||
let (new_output, did_flush) = Engine::replay_keystrokes(
|
||||
// Replay entire event log through fresh engine
|
||||
let (new_output, did_flush) = Engine::replay_events(
|
||||
method,
|
||||
&self.config.macros,
|
||||
&self.keystroke_history,
|
||||
&self.event_store,
|
||||
);
|
||||
|
||||
if did_flush {
|
||||
// Engine flushed a word. Only backspace + retype when auto-restore
|
||||
// actually CHANGES the word; otherwise the composed word is already
|
||||
// correct on screen and retyping it eats spacing and shifts the
|
||||
// finished word left.
|
||||
let to_commit = self.word_to_commit();
|
||||
if !self.screen_output.is_empty() && to_commit != self.screen_output {
|
||||
let backspaces = self.screen_output.chars().count();
|
||||
commands.push(OutputCommand::Backspace(backspaces));
|
||||
commands.push(OutputCommand::Type(to_commit));
|
||||
}
|
||||
self.keystroke_history.clear();
|
||||
self.event_store.clear();
|
||||
self.screen_output.clear();
|
||||
return commands;
|
||||
}
|
||||
|
|
@ -348,31 +348,43 @@ impl Daemon {
|
|||
commands
|
||||
}
|
||||
|
||||
/// Backspace-Replay: pop from history, replay, and return commands to fix screen.
|
||||
/// Event Sourcing: pop last event, replay, and return commands to fix screen.
|
||||
fn replay_backspace(&mut self) -> Vec<OutputCommand> {
|
||||
let mut commands = Vec::new();
|
||||
let method = match self.config.input_method.as_str() {
|
||||
"vni" => InputMethod::Vni,
|
||||
_ => InputMethod::Telex,
|
||||
};
|
||||
|
||||
if self.keystroke_history.is_empty() {
|
||||
if self.event_store.is_empty() {
|
||||
// Nothing in history — just forward the backspace
|
||||
commands.push(OutputCommand::Backspace(1));
|
||||
return commands;
|
||||
}
|
||||
|
||||
// Remove last keystroke from history
|
||||
self.keystroke_history.pop();
|
||||
// Record backspace event
|
||||
self.event_store.push(InputEvent::Backspace);
|
||||
|
||||
// Remove the last key-typed event for replay (unless it was already a backspace)
|
||||
match self.event_store.pop() {
|
||||
Some(InputEvent::Backspace) => {
|
||||
// Pop again to remove the preceding event
|
||||
self.event_store.pop();
|
||||
}
|
||||
Some(_) => {
|
||||
// Already popped the last event (KeyTyped or Flush)
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
// Replay through fresh engine
|
||||
let method = match self.config.input_method.as_str() {
|
||||
"vni" => InputMethod::Vni,
|
||||
_ => InputMethod::Telex,
|
||||
};
|
||||
let (new_output, _) = if self.keystroke_history.is_empty() {
|
||||
let (new_output, _) = if self.event_store.is_empty() {
|
||||
(String::new(), false)
|
||||
} else {
|
||||
Engine::replay_keystrokes(
|
||||
Engine::replay_events(
|
||||
method,
|
||||
&self.config.macros,
|
||||
&self.keystroke_history,
|
||||
&self.event_store,
|
||||
)
|
||||
};
|
||||
|
||||
|
|
@ -394,7 +406,7 @@ impl Daemon {
|
|||
/// word is English / not valid Vietnamese — the raw keystrokes typed.
|
||||
fn word_to_commit(&self) -> String {
|
||||
if self.config.auto_restore.enabled {
|
||||
let raw: String = self.keystroke_history.iter().collect();
|
||||
let raw = self.event_store.raw_keystrokes();
|
||||
if Engine::should_restore_word(&self.screen_output, &raw) {
|
||||
return raw;
|
||||
}
|
||||
|
|
@ -404,7 +416,7 @@ impl Daemon {
|
|||
|
||||
/// Reset the replay state (on flush, focus loss, modifier key, etc.)
|
||||
fn replay_reset(&mut self) {
|
||||
self.keystroke_history.clear();
|
||||
self.event_store.clear();
|
||||
self.screen_output.clear();
|
||||
}
|
||||
|
||||
|
|
@ -731,7 +743,7 @@ fn run_with_x11(
|
|||
pressed_keys.remove(&event.keycode);
|
||||
SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed);
|
||||
execute_commands(&*injector, &commands, true);
|
||||
if daemon.keystroke_history.is_empty() && commands.is_empty() {
|
||||
if daemon.event_store.is_empty() && commands.is_empty() {
|
||||
let _ = injector.send_backspace();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ description = "Viet+ Vietnamese IME Core Engine"
|
|||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { version = "1.34", features = ["yaml"] }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use crate::input_method::{InputMethod, InputMethodRules, get_rules};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use crate::bamboo::BambooEngine;
|
||||
use crate::english::EnglishDict;
|
||||
use crate::event::{Command, EventStore};
|
||||
use crate::input_method::InputMethod;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
|
@ -148,6 +150,88 @@ impl Engine {
|
|||
(if did_flush { String::new() } else { last_output }, did_flush)
|
||||
}
|
||||
|
||||
/// Replay events through a fresh engine, returning (expected_output, did_flush).
|
||||
/// This is the Event Sourcing equivalent of replay_keystrokes.
|
||||
pub fn replay_events(
|
||||
method: InputMethod,
|
||||
macros: &HashMap<String, String>,
|
||||
events: &EventStore,
|
||||
) -> (String, bool) {
|
||||
let mut engine = Engine::new(method);
|
||||
for (shortcut, expansion) in macros {
|
||||
engine.add_macro(shortcut.clone(), expansion.clone());
|
||||
}
|
||||
|
||||
let mut last_output = String::new();
|
||||
let mut composing = String::new();
|
||||
|
||||
for event in events.iter() {
|
||||
match event {
|
||||
crate::event::InputEvent::KeyTyped(ch) => {
|
||||
if let Some(out) = engine.bamboo.process_key(*ch) {
|
||||
composing = out.clone();
|
||||
last_output = out;
|
||||
} else {
|
||||
composing = engine.bamboo.get_output();
|
||||
last_output = composing.clone();
|
||||
}
|
||||
}
|
||||
crate::event::InputEvent::Backspace => {
|
||||
let _ = engine.bamboo.pop_last();
|
||||
composing = engine.bamboo.get_output();
|
||||
last_output = composing.clone();
|
||||
}
|
||||
crate::event::InputEvent::Flush(_) => {
|
||||
if !composing.is_empty() {
|
||||
last_output = composing.clone();
|
||||
}
|
||||
composing.clear();
|
||||
engine.bamboo.reset();
|
||||
}
|
||||
crate::event::InputEvent::Paste(text) => {
|
||||
for ch in text.chars() {
|
||||
if let Some(out) = engine.bamboo.process_key(ch) {
|
||||
composing = out;
|
||||
}
|
||||
}
|
||||
last_output = composing.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = engine.bamboo.get_output();
|
||||
let output_is_empty = output.is_empty();
|
||||
if !output.is_empty() {
|
||||
last_output = output;
|
||||
}
|
||||
|
||||
let did_flush = output_is_empty && composing.is_empty();
|
||||
(if did_flush { String::new() } else { last_output }, did_flush)
|
||||
}
|
||||
|
||||
/// Event Sourcing + Command Pattern: replay events and return diff commands.
|
||||
/// Compares expected output against screen_output and generates backspace/type commands.
|
||||
pub fn replay_events_to_commands(
|
||||
method: InputMethod,
|
||||
macros: &HashMap<String, String>,
|
||||
events: &EventStore,
|
||||
screen_output: &str,
|
||||
) -> Vec<Command> {
|
||||
let (new_output, _) = Engine::replay_events(method, macros, events);
|
||||
|
||||
let mut commands = Vec::new();
|
||||
if new_output != screen_output {
|
||||
let backspaces = screen_output.chars().count();
|
||||
if backspaces > 0 {
|
||||
commands.push(Command::Backspace(backspaces));
|
||||
}
|
||||
if !new_output.is_empty() {
|
||||
commands.push(Command::Type(new_output));
|
||||
}
|
||||
}
|
||||
commands
|
||||
}
|
||||
|
||||
pub fn update_with_pasted_text(&mut self, text: &str) {
|
||||
self.raw_buffer.clear();
|
||||
self.raw_buffer.push_str(text);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct EnglishDict {
|
||||
|
|
|
|||
104
engine/src/event.rs
Normal file
104
engine/src/event.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Typed input event - the core of Event Sourcing.
|
||||
/// KHÔNG lưu nội dung nhạy cảm, chỉ lưu event sequence.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InputEvent {
|
||||
/// A character key was typed
|
||||
KeyTyped(char),
|
||||
/// Backspace was pressed
|
||||
Backspace,
|
||||
/// A flush character (space, punctuation, enter, tab)
|
||||
Flush(char),
|
||||
/// Text was pasted
|
||||
Paste(String),
|
||||
}
|
||||
|
||||
/// Append-only event store.
|
||||
/// Source of truth for all user input.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EventStore {
|
||||
events: Vec<InputEvent>,
|
||||
}
|
||||
|
||||
impl EventStore {
|
||||
pub fn new() -> Self {
|
||||
Self { events: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, event: InputEvent) {
|
||||
self.events.push(event);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<InputEvent> {
|
||||
self.events.pop()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.events.clear();
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.events.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.events.len()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &InputEvent> {
|
||||
self.events.iter()
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[InputEvent] {
|
||||
&self.events
|
||||
}
|
||||
|
||||
/// Extract raw keystrokes from event log (for auto-restore comparison).
|
||||
/// Only reconstructs the literal characters typed, excluding backspaces.
|
||||
pub fn raw_keystrokes(&self) -> String {
|
||||
let mut s = String::new();
|
||||
for event in &self.events {
|
||||
match event {
|
||||
InputEvent::KeyTyped(c) => s.push(*c),
|
||||
InputEvent::Backspace => { s.pop(); }
|
||||
InputEvent::Flush(_) => {}
|
||||
InputEvent::Paste(text) => s.push_str(text),
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Hash the event type sequence (not content) for privacy-safe pattern detection.
|
||||
/// Output: sha256 hex of event type characters (K=KeyTyped, B=Backspace, F=Flush, P=Paste).
|
||||
/// Không thể recover text gốc — chỉ biết "có X events với pattern Y".
|
||||
pub fn pattern_hash(&self) -> String {
|
||||
let types: String = self.events.iter().map(|e| match e {
|
||||
InputEvent::KeyTyped(_) => 'K',
|
||||
InputEvent::Backspace => 'B',
|
||||
InputEvent::Flush(_) => 'F',
|
||||
InputEvent::Paste(_) => 'P',
|
||||
}).collect();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(types.as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EventStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Formalized output commands (Command Pattern).
|
||||
/// Chỉ chứa diff instruction, không chứa text nhạy cảm.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Command {
|
||||
/// Type a string of characters
|
||||
Type(String),
|
||||
/// Backspace N times
|
||||
Backspace(usize),
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
mod bamboo;
|
||||
mod engine;
|
||||
mod english;
|
||||
pub mod event;
|
||||
mod input_method;
|
||||
pub mod spelling;
|
||||
|
||||
|
|
@ -9,4 +11,5 @@ mod tests;
|
|||
|
||||
pub use engine::Engine;
|
||||
pub use engine::EngineEvent;
|
||||
pub use event::{Command, EventStore, InputEvent};
|
||||
pub use input_method::InputMethod;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
const FIRST_CONSONANT_SEQS: &[&str] = &[
|
||||
"b d đ g gh m n nh p ph r s t tr v z",
|
||||
"c h k kh qu th",
|
||||
|
|
|
|||
112
packaging/flatpak/FLATPAK_BUILD.md
Normal file
112
packaging/flatpak/FLATPAK_BUILD.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# Building the Viet+ Flatpak
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Flatpak installed with Flathub remote configured
|
||||
- `org.gnome.Platform//50` runtime installed
|
||||
- `org.gnome.Sdk//50` SDK installed
|
||||
- `org.freedesktop.Sdk.Extension.rust-stable//25.08` installed
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
flatpak install --user flathub org.gnome.Platform//50
|
||||
flatpak install --user flathub org.gnome.Sdk//50
|
||||
flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method 1: Quick build script
|
||||
|
||||
```bash
|
||||
cd packaging/flatpak
|
||||
bash build-flatpak.sh [version]
|
||||
# e.g. bash build-flatpak.sh 0.1.5
|
||||
```
|
||||
|
||||
Output: `packaging/flatpak/VietPlus-<version>.flatpak`
|
||||
|
||||
---
|
||||
|
||||
## Method 2: Manual step-by-step
|
||||
|
||||
```bash
|
||||
cd packaging/flatpak
|
||||
|
||||
# 1. Clean previous artifacts
|
||||
rm -rf build-dir repo VietPlus-*.flatpak
|
||||
|
||||
# 2. Initialize build directory
|
||||
# NOTE: arg order is flatpak build-init DIR APPNAME SDK RUNTIME
|
||||
flatpak build-init build-dir io.github.vietc.VietPlus \
|
||||
org.gnome.Sdk//50 org.gnome.Platform//50
|
||||
|
||||
# 3. Copy source code
|
||||
mkdir -p build-dir/files/src/vietc
|
||||
rsync -a /path/to/vietc/ build-dir/files/src/vietc/ --exclude=target --exclude=.git
|
||||
|
||||
# 4. Build Rust binaries
|
||||
flatpak build --share=network build-dir sh -c '
|
||||
export PATH=/usr/lib/sdk/rust-stable/bin:$PATH
|
||||
export CARGO_HOME=/app/cargo
|
||||
cd /app/src/vietc
|
||||
cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd
|
||||
'
|
||||
|
||||
# 5. Install binaries and icons
|
||||
flatpak build build-dir sh -c '
|
||||
install -Dm755 /app/src/vietc/target/release/vietc /app/bin/vietc-daemon
|
||||
install -Dm755 /app/src/vietc/target/release/vietc-cli /app/bin/vietc-cli
|
||||
install -Dm755 /app/src/vietc/target/release/vietc-uinputd /app/bin/vietc-uinputd
|
||||
|
||||
install -Dm644 /app/src/vietc/packaging/icons/vietc.svg \
|
||||
/app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.svg
|
||||
install -Dm644 /app/src/vietc/packaging/icons/vietc-vn.svg \
|
||||
/app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-vn.svg
|
||||
install -Dm644 /app/src/vietc/packaging/icons/vietc-en.svg \
|
||||
/app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-en.svg
|
||||
'
|
||||
|
||||
# 6. Finish (set permissions + command)
|
||||
flatpak build-finish build-dir \
|
||||
--socket=x11 \
|
||||
--socket=wayland \
|
||||
--filesystem=home \
|
||||
--share=ipc \
|
||||
--talk-name=org.freedesktop.Notifications \
|
||||
--talk-name=org.a11y.Bus \
|
||||
--command=vietc-daemon
|
||||
|
||||
# 7. Export to local repo
|
||||
flatpak build-export repo build-dir
|
||||
|
||||
# 8. Create bundle
|
||||
flatpak build-bundle repo VietPlus-0.1.5.flatpak io.github.vietc.VietPlus
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# From bundle
|
||||
flatpak install --user --bundle VietPlus-0.1.5.flatpak
|
||||
|
||||
# From local repo
|
||||
flatpak --user remote-add --no-gpg-verify vietc-repo repo
|
||||
flatpak --user install vietc-repo io.github.vietc.VietPlus
|
||||
|
||||
# Run
|
||||
flatpak run io.github.vietc.VietPlus
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Notes
|
||||
|
||||
- **SDK/RUNTIME order**: `flatpak build-init` takes `SDK` first, then `RUNTIME` (counterintuitive but important — getting this wrong means `/usr/lib/sdk/` won't be mounted)
|
||||
- **Rust SDK**: must be installed as `org.freedesktop.Sdk.Extension.rust-stable//25.08`; it mounts automatically at `/usr/lib/sdk/rust-stable/`
|
||||
- **Icons**: all icon files in Flatpak must be prefixed with the app ID (`io.github.vietc.VietPlus.*`) or `flatpak build-export` will skip them
|
||||
- **Daemon binary name**: Cargo builds the daemon binary as `vietc` (not `vietc-daemon`) in `target/release/`; rename on install to match the desktop file
|
||||
- **Desktop Categories**: only use registered categories (`Utility`); `InputMethod` is not registered
|
||||
|
|
@ -6,78 +6,57 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|||
VERSION="${1:-0.1.4}"
|
||||
|
||||
echo "=== Building Viet+ Flatpak v${VERSION} ==="
|
||||
|
||||
# Install required runtimes
|
||||
flatpak install -y flathub org.gnome.Platform//50 org.gnome.Sdk//50 2>/dev/null || true
|
||||
flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 2>/dev/null || true
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Clean previous build
|
||||
rm -rf build-dir vietc-repo VietPlus-*.flatpak
|
||||
rm -rf build-dir repo VietPlus-*.flatpak
|
||||
|
||||
# Initialize build directory
|
||||
# NOTE: arg order is flatpak build-init DIR APPNAME SDK RUNTIME
|
||||
flatpak build-init build-dir io.github.vietc.VietPlus \
|
||||
org.gnome.Platform//50 org.gnome.Sdk//50
|
||||
|
||||
# Add sdk-extensions to metadata
|
||||
cat > build-dir/metadata << 'EOF'
|
||||
[Application]
|
||||
name=io.github.vietc.VietPlus
|
||||
runtime=org.gnome.Platform/x86_64/50
|
||||
sdk=org.gnome.Sdk/x86_64/50
|
||||
sdk-extensions=org.freedesktop.Sdk.Extension.rust-stable
|
||||
EOF
|
||||
org.gnome.Sdk//50 org.gnome.Platform//50
|
||||
|
||||
# Copy source code
|
||||
mkdir -p build-dir/files/src/vietc
|
||||
rsync -a "$PROJECT_ROOT/" build-dir/files/src/vietc/ --exclude=target --exclude=.git
|
||||
|
||||
# Symlink Rust SDK extension
|
||||
RUST_FILES=$(find /var/lib/flatpak/runtime/org.freedesktop.Sdk.Extension.rust-stable \
|
||||
-name "rustc" -type f 2>/dev/null | head -1 | sed 's|/bin/rustc||')
|
||||
mkdir -p build-dir/files/usr/lib/sdk
|
||||
ln -s "$RUST_FILES" build-dir/files/usr/lib/sdk/rust-stable
|
||||
BUILD='export PATH=/usr/lib/sdk/rust-stable/bin:$PATH
|
||||
export CARGO_HOME=/app/cargo
|
||||
cd /app/src/vietc'
|
||||
|
||||
# Build all Rust binaries inside sandbox
|
||||
echo "Compiling daemon, CLI, uinputd..."
|
||||
flatpak build --share=network build-dir sh -c '
|
||||
export PATH=/usr/lib/sdk/rust-stable/bin:$PATH
|
||||
export CARGO_HOME=/app/cargo
|
||||
cd /app/src/vietc
|
||||
cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd
|
||||
'
|
||||
# Build daemon + CLI + uinputd
|
||||
echo ""
|
||||
echo "=== Compiling daemon, CLI, uinputd... ==="
|
||||
flatpak build --share=network build-dir sh -c "$BUILD && cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd"
|
||||
|
||||
echo "Compiling system tray..."
|
||||
flatpak build --share=network build-dir sh -c '
|
||||
export PATH=/usr/lib/sdk/rust-stable/bin:$PATH
|
||||
export CARGO_HOME=/app/cargo
|
||||
cd /app/src/vietc
|
||||
cargo build --release --manifest-path ui/Cargo.toml
|
||||
'
|
||||
# Install files
|
||||
echo ""
|
||||
echo "=== Installing files... ==="
|
||||
flatpak build build-dir sh -c "
|
||||
set -e
|
||||
install -Dm755 /app/src/vietc/target/release/vietc /app/bin/vietc-daemon
|
||||
install -Dm755 /app/src/vietc/target/release/vietc-cli /app/bin/vietc-cli
|
||||
install -Dm755 /app/src/vietc/target/release/vietc-uinputd /app/bin/vietc-uinputd
|
||||
|
||||
# Install files into sandbox
|
||||
echo "Installing files..."
|
||||
flatpak build build-dir sh -c '
|
||||
set -e
|
||||
install -Dm755 /app/src/vietc/target/release/vietc /app/bin/vietc
|
||||
install -Dm755 /app/src/vietc/target/release/vietc-cli /app/bin/vietc-cli
|
||||
install -Dm755 /app/src/vietc/target/release/vietc-uinputd /app/bin/vietc-uinputd
|
||||
install -Dm755 /app/src/vietc/ui/target/release/vietc-tray /app/bin/vietc-tray
|
||||
gcc -O2 -o /app/bin/vietc-xrecord /app/src/vietc/packaging/appimage/vietc-xrecord.c -lX11 -lXtst
|
||||
install -Dm755 /app/src/vietc/packaging/flatpak/vietc-wrapper.sh /app/bin/vietc-wrapper.sh
|
||||
install -Dm644 /app/src/vietc/packaging/appimage/vietc.desktop \
|
||||
/app/share/applications/io.github.vietc.VietPlus.desktop
|
||||
sed -i "s/Icon=vietc/Icon=io.github.vietc.VietPlus/g" \
|
||||
/app/share/applications/io.github.vietc.VietPlus.desktop
|
||||
install -Dm644 /app/src/vietc/vietc.toml /app/etc/vietc/config.toml
|
||||
mkdir -p /app/share/icons/hicolor/256x256/apps
|
||||
cp /app/src/vietc/packaging/appimage/AppDir/vietc.svg \
|
||||
/app/share/icons/hicolor/256x256/apps/io.github.vietc.VietPlus.svg 2>/dev/null || true
|
||||
mkdir -p /app/share/metainfo
|
||||
cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << "XML"
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component type="desktop-application">
|
||||
install -Dm644 /app/src/vietc/packaging/icons/vietc.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.svg
|
||||
install -Dm644 /app/src/vietc/packaging/icons/vietc-vn.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-vn.svg
|
||||
install -Dm644 /app/src/vietc/packaging/icons/vietc-en.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-en.svg
|
||||
|
||||
cat > /app/share/applications/io.github.vietc.VietPlus.desktop << END
|
||||
[Desktop Entry]
|
||||
Name=Viet+
|
||||
Comment=Vietnamese Input Method
|
||||
Exec=/app/bin/vietc-daemon
|
||||
Icon=io.github.vietc.VietPlus
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;
|
||||
END
|
||||
|
||||
mkdir -p /app/share/metainfo
|
||||
cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML'
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<component type='desktop-application'>
|
||||
<id>io.github.vietc.VietPlus</id>
|
||||
<name>Viet+</name>
|
||||
<summary>Vietnamese Input Method for Linux</summary>
|
||||
|
|
@ -86,36 +65,39 @@ flatpak build build-dir sh -c '
|
|||
</description>
|
||||
<metadata_license>MIT</metadata_license>
|
||||
<project_license>MIT</project_license>
|
||||
<url type="homepage">https://github.com/vndangkhoa/vietc</url>
|
||||
<provides><binary>vietc</binary></provides>
|
||||
<url type='homepage'>https://github.com/vndangkhoa/vietc</url>
|
||||
<provides><binary>vietc-daemon</binary></provides>
|
||||
<categories><category>Utility</category></categories>
|
||||
</component>
|
||||
XML
|
||||
mkdir -p /app/share/doc/vietc
|
||||
cp /app/src/vietc/README.md /app/share/doc/vietc/ 2>/dev/null || true
|
||||
cp /app/src/vietc/LICENSE /app/share/doc/vietc/ 2>/dev/null || true
|
||||
'
|
||||
"
|
||||
|
||||
# Finish the build
|
||||
echo "Finalizing build..."
|
||||
# Finish
|
||||
echo ""
|
||||
echo "=== Finalizing build... ==="
|
||||
flatpak build-finish build-dir \
|
||||
--socket=x11 \
|
||||
--socket=wayland \
|
||||
--socket=session-bus \
|
||||
--filesystem=home \
|
||||
--share=ipc \
|
||||
--device=all \
|
||||
--command=vietc-wrapper.sh
|
||||
--talk-name=org.freedesktop.Notifications \
|
||||
--talk-name=org.a11y.Bus \
|
||||
--command=vietc-daemon
|
||||
|
||||
# Export to local repository
|
||||
echo "Exporting to repository..."
|
||||
flatpak build-export vietc-repo build-dir
|
||||
# Export
|
||||
echo ""
|
||||
echo "=== Exporting to repository... ==="
|
||||
flatpak build-export repo build-dir
|
||||
|
||||
# Create single-file bundle
|
||||
echo "Creating bundle..."
|
||||
flatpak build-bundle vietc-repo "VietPlus-${VERSION}.flatpak" io.github.vietc.VietPlus
|
||||
# Bundle
|
||||
echo ""
|
||||
echo "=== Creating bundle... ==="
|
||||
flatpak build-bundle repo "VietPlus-${VERSION}.flatpak" io.github.vietc.VietPlus
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Package: $SCRIPT_DIR/VietPlus-${VERSION}.flatpak ($(du -h "$SCRIPT_DIR/VietPlus-${VERSION}.flatpak" | cut -f1))"
|
||||
echo "Package: $SCRIPT_DIR/VietPlus-${VERSION}.flatpak"
|
||||
echo "Size: $(du -h "$SCRIPT_DIR/VietPlus-${VERSION}.flatpak" | cut -f1)"
|
||||
echo ""
|
||||
echo "Install: flatpak install --user --bundle VietPlus-${VERSION}.flatpak"
|
||||
echo "Run: flatpak run io.github.vietc.VietPlus"
|
||||
|
|
@ -1,40 +1,21 @@
|
|||
{
|
||||
"app-id": "io.github.vietc.VietPlus",
|
||||
"runtime": "org.gnome.Platform",
|
||||
"runtime-version": "47",
|
||||
"runtime-version": "50",
|
||||
"sdk": "org.gnome.Sdk",
|
||||
"sdk-extensions": [
|
||||
"org.freedesktop.Sdk.Extension.rust-stable"
|
||||
],
|
||||
"command": "vietc-wrapper.sh",
|
||||
"rename-desktop-file": "vietc.desktop",
|
||||
"rename-icon": "vietc",
|
||||
"command": "vietc-daemon",
|
||||
"finish-args": [
|
||||
"--socket=x11",
|
||||
"--socket=wayland",
|
||||
"--socket=session-bus",
|
||||
"--device=all",
|
||||
"--share=ipc"
|
||||
"--filesystem=home",
|
||||
"--share=ipc",
|
||||
"--talk-name=org.freedesktop.Notifications",
|
||||
"--talk-name=org.a11y.Bus"
|
||||
],
|
||||
"modules": [
|
||||
{
|
||||
"name": "xclip",
|
||||
"no-autogen": true,
|
||||
"make-install-args": [
|
||||
"install"
|
||||
],
|
||||
"builddir": true,
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/astrand/xclip/archive/refs/tags/0.13.tar.gz",
|
||||
"sha256": "ca5b8804e3c910a66423a882d79bf3c9450e8758faa5d2b9deba5342451b140e"
|
||||
}
|
||||
],
|
||||
"cleanup": [
|
||||
"/share/man"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vietc",
|
||||
"buildsystem": "simple",
|
||||
|
|
@ -47,57 +28,25 @@
|
|||
"build-commands": [
|
||||
"cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd --manifest-path /run/build/vietc/Cargo.toml",
|
||||
|
||||
"cd /run/build/vietc/ui && cargo build --release --manifest-path /run/build/vietc/ui/Cargo.toml || echo 'tray build skipped'",
|
||||
|
||||
"install -Dm755 target/release/vietc /app/bin/vietc",
|
||||
"install -Dm755 target/release/vietc /app/bin/vietc-daemon",
|
||||
"install -Dm755 target/release/vietc-cli /app/bin/vietc-cli",
|
||||
"install -Dm755 target/release/vietc-uinputd /app/bin/vietc-uinputd",
|
||||
"[ -f ui/target/release/vietc-tray ] && install -Dm755 ui/target/release/vietc-tray /app/bin/vietc-tray || true",
|
||||
|
||||
"gcc -O2 -o /app/bin/vietc-xrecord /run/build/vietc/packaging/appimage/vietc-xrecord.c -lX11 -lXtst",
|
||||
"install -Dm644 packaging/icons/vietc.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.svg",
|
||||
"install -Dm644 packaging/icons/vietc-vn.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-vn.svg",
|
||||
"install -Dm644 packaging/icons/vietc-en.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-en.svg",
|
||||
|
||||
"install -Dm755 /run/build/vietc/packaging/flatpak/vietc-wrapper.sh /app/bin/vietc-wrapper.sh",
|
||||
|
||||
"install -Dm644 /run/build/vietc/packaging/appimage/vietc.desktop /app/share/applications/vietc.desktop",
|
||||
|
||||
"install -Dm644 /run/build/vietc/vietc.toml /app/etc/vietc/config.toml",
|
||||
|
||||
"mkdir -p /app/share/icons/hicolor/256x256/apps",
|
||||
"cp /run/build/vietc/packaging/appimage/AppDir/vietc.svg /app/share/icons/hicolor/256x256/apps/vietc.svg || true",
|
||||
|
||||
"install -Dm644 /run/build/vietc/vietc.service /app/lib/systemd/user/vietc.service || true",
|
||||
"mkdir -p /app/share/applications",
|
||||
"cat > /app/share/applications/io.github.vietc.VietPlus.desktop << END\n[Desktop Entry]\nName=Viet+\nComment=Vietnamese Input Method\nExec=/app/bin/vietc-daemon\nIcon=io.github.vietc.VietPlus\nTerminal=false\nType=Application\nCategories=Utility;\nEND",
|
||||
|
||||
"mkdir -p /app/share/metainfo",
|
||||
"cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML'",
|
||||
"<?xml version='1.0' encoding='utf-8'?>",
|
||||
"<component type='desktop-application'>",
|
||||
" <id>io.github.vietc.VietPlus</id>",
|
||||
" <name>Viet+</name>",
|
||||
" <summary>Vietnamese Input Method for Linux</summary>",
|
||||
" <description>",
|
||||
" <p>Zero-configuration Vietnamese input method engine supporting Telex and VNI input methods.</p>",
|
||||
" </description>",
|
||||
" <metadata_license>MIT</metadata_license>",
|
||||
" <project_license>MIT</project_license>",
|
||||
" <url type='homepage'>https://github.com/vndangkhoa/vietc</url>",
|
||||
" <provides><binary>vietc</binary></provides>",
|
||||
" <categories><category>Utility</category></categories>",
|
||||
"</component>",
|
||||
"XML",
|
||||
|
||||
"mkdir -p /app/share/doc/vietc",
|
||||
"cp /run/build/vietc/README.md /app/share/doc/vietc/ || true",
|
||||
"cp /run/build/vietc/LICENSE /app/share/doc/vietc/ || true"
|
||||
"cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML'\n<?xml version='1.0' encoding='utf-8'?>\n<component type='desktop-application'>\n <id>io.github.vietc.VietPlus</id>\n <name>Viet+</name>\n <summary>Vietnamese Input Method for Linux</summary>\n <description>\n <p>Zero-configuration Vietnamese input method engine supporting Telex and VNI input methods.</p>\n </description>\n <metadata_license>MIT</metadata_license>\n <project_license>MIT</project_license>\n <url type='homepage'>https://github.com/vndangkhoa/vietc</url>\n <provides><binary>vietc-daemon</binary></provides>\n <categories><category>Utility</category></categories>\n</component>\nXML"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "dir",
|
||||
"path": "../.."
|
||||
}
|
||||
],
|
||||
"cleanup": [
|
||||
"/app/share/doc",
|
||||
"/app/share/icons"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
4
packaging/icons/vietc-en.svg
Normal file
4
packaging/icons/vietc-en.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<rect x="8" y="8" width="112" height="112" rx="24" fill="#4b5563"/>
|
||||
<text x="64" y="96" text-anchor="middle" fill="#ffffff" font-size="48" font-weight="bold" font-family="system-ui, sans-serif">EN</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 278 B |
4
packaging/icons/vietc-vn.svg
Normal file
4
packaging/icons/vietc-vn.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<rect x="8" y="8" width="112" height="112" rx="24" fill="#e02424"/>
|
||||
<text x="64" y="96" text-anchor="middle" fill="#ffffff" font-size="48" font-weight="bold" font-family="system-ui, sans-serif">VN</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 278 B |
32
packaging/icons/vietc.svg
Normal file
32
packaging/icons/vietc.svg
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||
<defs>
|
||||
<linearGradient id="kb-bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#3a3a3a"/>
|
||||
<stop offset="100%" stop-color="#1a1a1a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="vn-badge" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#e74c3c"/>
|
||||
<stop offset="100%" stop-color="#c0392b"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="16" y="56" width="224" height="156" rx="18" fill="url(#kb-bg)" stroke="#555" stroke-width="3"/>
|
||||
<rect x="32" y="72" width="192" height="124" rx="10" fill="#2a2a2a"/>
|
||||
<rect x="44" y="84" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="76" y="84" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="108" y="84" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="140" y="84" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="172" y="84" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="50" y="112" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="82" y="112" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="114" y="112" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="146" y="112" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="178" y="112" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="56" y="140" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="88" y="140" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="120" y="140" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="152" y="140" width="26" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="184" y="140" width="30" height="22" rx="4" fill="#e0e0e0"/>
|
||||
<rect x="68" y="168" width="120" height="16" rx="4" fill="#e0e0e0"/>
|
||||
<circle cx="224" cy="44" r="30" fill="url(#vn-badge)"/>
|
||||
<text x="224" y="52" text-anchor="middle" fill="#fff" font-size="20" font-weight="bold" font-family="sans-serif">VN</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pub mod inject;
|
||||
pub mod monitor;
|
||||
pub mod uinput_monitor;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use crate::inject::KeyEvent;
|
||||
|
||||
pub trait KeyMonitor {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::inject::{InjectResult, KeyInjector};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::io::{Read, BufRead};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use super::inject::{InjectResult, KeyInjector};
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod config;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use crate::config;
|
||||
use ksni::{menu::*, MenuItem, Tray};
|
||||
|
||||
fn is_flatpak() -> bool {
|
||||
std::path::Path::new("/app/bin/vietc-daemon").exists()
|
||||
}
|
||||
|
||||
fn write_status(state: &str) {
|
||||
if let Some(config_dir) = dirs::config_dir() {
|
||||
let _ = std::fs::write(config_dir.join("vietc").join("status"), state);
|
||||
|
|
@ -242,7 +247,13 @@ impl Tray for VietTray {
|
|||
}
|
||||
|
||||
fn icon_name(&self) -> String {
|
||||
if self.mode == "vn" {
|
||||
if is_flatpak() {
|
||||
if self.mode == "vn" {
|
||||
"io.github.vietc.VietPlus.vietc-vn".into()
|
||||
} else {
|
||||
"io.github.vietc.VietPlus.vietc-en".into()
|
||||
}
|
||||
} else if self.mode == "vn" {
|
||||
"vietc-vn".into()
|
||||
} else {
|
||||
"vietc-en".into()
|
||||
|
|
@ -250,9 +261,20 @@ impl Tray for VietTray {
|
|||
}
|
||||
|
||||
fn icon_theme_path(&self) -> String {
|
||||
// Use XDG user theme path for icons
|
||||
dirs::home_dir()
|
||||
.map(|d| d.join(".local/share/icons").to_string_lossy().into_owned())
|
||||
// Use XDG user theme path for icons (works in both native and Flatpak)
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let user_path = home.join(".local/share/icons");
|
||||
if user_path.exists() {
|
||||
return user_path.to_string_lossy().into_owned();
|
||||
}
|
||||
}
|
||||
// Flatpak: icons are in /app/share/icons
|
||||
let flatpak_path = std::path::Path::new("/app/share/icons");
|
||||
if flatpak_path.exists() {
|
||||
return "/app/share/icons".into();
|
||||
}
|
||||
dirs::data_dir()
|
||||
.map(|d| d.join("icons").to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "/usr/share/icons".into())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
use std::fs;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
|
|
|
|||
Loading…
Reference in a new issue