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
|
vietc-xrecord
|
||||||
packaging/flatpak/build-dir
|
packaging/flatpak/build-dir
|
||||||
packaging/flatpak/vietc-repo
|
packaging/flatpak/vietc-repo
|
||||||
|
packaging/flatpak/repo
|
||||||
packaging/flatpak/VietPlus-*
|
packaging/flatpak/VietPlus-*
|
||||||
|
|
|
||||||
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -1,5 +1,36 @@
|
||||||
# Changelog
|
# 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)
|
## v0.1.4 (2026-06-28)
|
||||||
|
|
||||||
### Flatpak Packaging
|
### 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/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/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/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/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>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
|
|
@ -94,31 +95,32 @@ Physical Keyboard
|
||||||
and renders Vietnamese text on screen
|
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.).
|
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:
|
Traditional IME:
|
||||||
keystroke → update buffer → emit event → hope it matches screen
|
keystroke → update buffer → emit event → hope it matches screen
|
||||||
|
|
||||||
Viet+ (Backspace-Replay):
|
Viet+ (Event Sourcing):
|
||||||
keystroke → add to history → replay ALL history in fresh engine → compute diff
|
keystroke → append InputEvent → replay ALL events in fresh engine → compute diff
|
||||||
```
|
```
|
||||||
|
|
||||||
On every keystroke:
|
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
|
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
|
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:
|
This means:
|
||||||
- **Zero state desync** — always recomputed from scratch
|
- **Zero state desync** — always recomputed from scratch
|
||||||
- **Self-healing** — if anything goes wrong, the next keystroke fixes it
|
- **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
|
- **Simple** — no complex state tracking or synchronization
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -128,7 +130,8 @@ This means:
|
||||||
```
|
```
|
||||||
vietc/
|
vietc/
|
||||||
├── engine/ # Vietnamese composition engine (bamboo-core Rust port)
|
├── 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
|
│ ├── bamboo.rs # Bamboo engine: transformation model, composition, tone placement
|
||||||
│ ├── input_method.rs # Telex/VNI rule definitions
|
│ ├── input_method.rs # Telex/VNI rule definitions
|
||||||
│ └── spelling.rs # Vietnamese syllable validation
|
│ └── spelling.rs # Vietnamese syllable validation
|
||||||
|
|
@ -280,10 +283,18 @@ Includes daemon + CLI + system tray + uinput daemon. Sandboxed — no system lib
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/vndangkhoa/vietc.git
|
git clone https://github.com/vndangkhoa/vietc.git
|
||||||
cd vietc/packaging/flatpak
|
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 std::io::{self, Write};
|
||||||
use vietc_engine::{Engine, EngineEvent, InputMethod};
|
use vietc_engine::{Engine, EngineEvent, InputMethod};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -6,7 +7,7 @@ use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
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.
|
/// Pin current thread to performance cores (0-3) and boost priority.
|
||||||
/// Inspired by VMK's approach to minimize input latency on Intel hybrid CPUs.
|
/// Inspired by VMK's approach to minimize input latency on Intel hybrid CPUs.
|
||||||
|
|
@ -110,10 +111,11 @@ struct Daemon {
|
||||||
app_state: AppStateManager,
|
app_state: AppStateManager,
|
||||||
engine_enabled: Arc<AtomicBool>,
|
engine_enabled: Arc<AtomicBool>,
|
||||||
grab_enabled: bool,
|
grab_enabled: bool,
|
||||||
/// Backspace-Replay: all keystrokes in the current word being composed.
|
/// Event Store: append-only log of typed input events.
|
||||||
/// On each keypress, we replay the entire history through a fresh engine
|
/// On each input, we replay the entire event log through a fresh engine
|
||||||
/// to compute the correct screen output, eliminating state desync.
|
/// to compute the expected screen output, eliminating state desync.
|
||||||
keystroke_history: Vec<char>,
|
/// 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.
|
/// What's currently displayed on screen for the current word.
|
||||||
/// Used to calculate how many backspaces we need before retyping.
|
/// Used to calculate how many backspaces we need before retyping.
|
||||||
screen_output: String,
|
screen_output: String,
|
||||||
|
|
@ -154,7 +156,7 @@ impl Daemon {
|
||||||
config_modified,
|
config_modified,
|
||||||
app_state,
|
app_state,
|
||||||
engine_enabled,
|
engine_enabled,
|
||||||
keystroke_history: Vec::new(),
|
event_store: EventStore::new(),
|
||||||
screen_output: String::new(),
|
screen_output: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -279,11 +281,16 @@ impl Daemon {
|
||||||
self.app_state.is_current_app_bypassed()
|
self.app_state.is_current_app_bypassed()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backspace-Replay: replay the entire keystroke history through a fresh
|
/// Event Sourcing: replay the entire event store through a fresh engine,
|
||||||
/// engine, compute what should be on screen, and return the commands
|
/// compute what should be on screen, and return the commands
|
||||||
/// (backspaces to erase old + new text to type).
|
/// (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> {
|
fn replay_and_inject(&mut self, ch: char) -> Vec<OutputCommand> {
|
||||||
let mut commands = Vec::new();
|
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.
|
// Flush characters: commit the current word and type the flush char.
|
||||||
// Only backspace + retype when auto-restore actually CHANGES the word
|
// 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
|
// already correctly on screen, so retyping it would eat the spacing and
|
||||||
// shift the finished word left.
|
// shift the finished word left.
|
||||||
if is_flush_char(ch) {
|
if is_flush_char(ch) {
|
||||||
|
self.event_store.push(InputEvent::Flush(ch));
|
||||||
let to_commit = self.word_to_commit();
|
let to_commit = self.word_to_commit();
|
||||||
if !self.screen_output.is_empty() && to_commit != self.screen_output {
|
if !self.screen_output.is_empty() && to_commit != self.screen_output {
|
||||||
let backspaces = self.screen_output.chars().count();
|
let backspaces = self.screen_output.chars().count();
|
||||||
|
|
@ -299,37 +307,29 @@ impl Daemon {
|
||||||
}
|
}
|
||||||
// Type the flush character itself
|
// Type the flush character itself
|
||||||
commands.push(OutputCommand::Type(ch.to_string()));
|
commands.push(OutputCommand::Type(ch.to_string()));
|
||||||
self.keystroke_history.clear();
|
self.event_store.clear();
|
||||||
self.screen_output.clear();
|
self.screen_output.clear();
|
||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new keystroke to history
|
// Record the typed key as an event
|
||||||
self.keystroke_history.push(ch);
|
self.event_store.push(InputEvent::KeyTyped(ch));
|
||||||
|
|
||||||
// Replay through fresh engine
|
// Replay entire event log through fresh engine
|
||||||
let method = match self.config.input_method.as_str() {
|
let (new_output, did_flush) = Engine::replay_events(
|
||||||
"vni" => InputMethod::Vni,
|
|
||||||
_ => InputMethod::Telex,
|
|
||||||
};
|
|
||||||
let (new_output, did_flush) = Engine::replay_keystrokes(
|
|
||||||
method,
|
method,
|
||||||
&self.config.macros,
|
&self.config.macros,
|
||||||
&self.keystroke_history,
|
&self.event_store,
|
||||||
);
|
);
|
||||||
|
|
||||||
if did_flush {
|
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();
|
let to_commit = self.word_to_commit();
|
||||||
if !self.screen_output.is_empty() && to_commit != self.screen_output {
|
if !self.screen_output.is_empty() && to_commit != self.screen_output {
|
||||||
let backspaces = self.screen_output.chars().count();
|
let backspaces = self.screen_output.chars().count();
|
||||||
commands.push(OutputCommand::Backspace(backspaces));
|
commands.push(OutputCommand::Backspace(backspaces));
|
||||||
commands.push(OutputCommand::Type(to_commit));
|
commands.push(OutputCommand::Type(to_commit));
|
||||||
}
|
}
|
||||||
self.keystroke_history.clear();
|
self.event_store.clear();
|
||||||
self.screen_output.clear();
|
self.screen_output.clear();
|
||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
@ -348,31 +348,43 @@ impl Daemon {
|
||||||
commands
|
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> {
|
fn replay_backspace(&mut self) -> Vec<OutputCommand> {
|
||||||
let mut commands = Vec::new();
|
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
|
// Nothing in history — just forward the backspace
|
||||||
commands.push(OutputCommand::Backspace(1));
|
commands.push(OutputCommand::Backspace(1));
|
||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove last keystroke from history
|
// Record backspace event
|
||||||
self.keystroke_history.pop();
|
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
|
// Replay through fresh engine
|
||||||
let method = match self.config.input_method.as_str() {
|
let (new_output, _) = if self.event_store.is_empty() {
|
||||||
"vni" => InputMethod::Vni,
|
|
||||||
_ => InputMethod::Telex,
|
|
||||||
};
|
|
||||||
let (new_output, _) = if self.keystroke_history.is_empty() {
|
|
||||||
(String::new(), false)
|
(String::new(), false)
|
||||||
} else {
|
} else {
|
||||||
Engine::replay_keystrokes(
|
Engine::replay_events(
|
||||||
method,
|
method,
|
||||||
&self.config.macros,
|
&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.
|
/// word is English / not valid Vietnamese — the raw keystrokes typed.
|
||||||
fn word_to_commit(&self) -> String {
|
fn word_to_commit(&self) -> String {
|
||||||
if self.config.auto_restore.enabled {
|
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) {
|
if Engine::should_restore_word(&self.screen_output, &raw) {
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
@ -404,7 +416,7 @@ impl Daemon {
|
||||||
|
|
||||||
/// Reset the replay state (on flush, focus loss, modifier key, etc.)
|
/// Reset the replay state (on flush, focus loss, modifier key, etc.)
|
||||||
fn replay_reset(&mut self) {
|
fn replay_reset(&mut self) {
|
||||||
self.keystroke_history.clear();
|
self.event_store.clear();
|
||||||
self.screen_output.clear();
|
self.screen_output.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -731,7 +743,7 @@ fn run_with_x11(
|
||||||
pressed_keys.remove(&event.keycode);
|
pressed_keys.remove(&event.keycode);
|
||||||
SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed);
|
SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed);
|
||||||
execute_commands(&*injector, &commands, true);
|
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();
|
let _ = injector.send_backspace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ description = "Viet+ Vietnamese IME Core Engine"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
sha2 = "0.10"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = { version = "1.34", features = ["yaml"] }
|
insta = { version = "1.34", features = ["yaml"] }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use crate::input_method::{InputMethod, InputMethodRules, get_rules};
|
use crate::input_method::{InputMethod, InputMethodRules, get_rules};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use crate::bamboo::BambooEngine;
|
use crate::bamboo::BambooEngine;
|
||||||
use crate::english::EnglishDict;
|
use crate::english::EnglishDict;
|
||||||
|
use crate::event::{Command, EventStore};
|
||||||
use crate::input_method::InputMethod;
|
use crate::input_method::InputMethod;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
@ -148,6 +150,88 @@ impl Engine {
|
||||||
(if did_flush { String::new() } else { last_output }, did_flush)
|
(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) {
|
pub fn update_with_pasted_text(&mut self, text: &str) {
|
||||||
self.raw_buffer.clear();
|
self.raw_buffer.clear();
|
||||||
self.raw_buffer.push_str(text);
|
self.raw_buffer.push_str(text);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
pub struct EnglishDict {
|
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;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
mod bamboo;
|
mod bamboo;
|
||||||
mod engine;
|
mod engine;
|
||||||
mod english;
|
mod english;
|
||||||
|
pub mod event;
|
||||||
mod input_method;
|
mod input_method;
|
||||||
pub mod spelling;
|
pub mod spelling;
|
||||||
|
|
||||||
|
|
@ -9,4 +11,5 @@ mod tests;
|
||||||
|
|
||||||
pub use engine::Engine;
|
pub use engine::Engine;
|
||||||
pub use engine::EngineEvent;
|
pub use engine::EngineEvent;
|
||||||
|
pub use event::{Command, EventStore, InputEvent};
|
||||||
pub use input_method::InputMethod;
|
pub use input_method::InputMethod;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
const FIRST_CONSONANT_SEQS: &[&str] = &[
|
const FIRST_CONSONANT_SEQS: &[&str] = &[
|
||||||
"b d đ g gh m n nh p ph r s t tr v z",
|
"b d đ g gh m n nh p ph r s t tr v z",
|
||||||
"c h k kh qu th",
|
"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}"
|
VERSION="${1:-0.1.4}"
|
||||||
|
|
||||||
echo "=== Building Viet+ Flatpak v${VERSION} ==="
|
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"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
# Clean previous build
|
# Clean previous build
|
||||||
rm -rf build-dir vietc-repo VietPlus-*.flatpak
|
rm -rf build-dir repo VietPlus-*.flatpak
|
||||||
|
|
||||||
# Initialize build directory
|
# Initialize build directory
|
||||||
|
# NOTE: arg order is flatpak build-init DIR APPNAME SDK RUNTIME
|
||||||
flatpak build-init build-dir io.github.vietc.VietPlus \
|
flatpak build-init build-dir io.github.vietc.VietPlus \
|
||||||
org.gnome.Platform//50 org.gnome.Sdk//50
|
org.gnome.Sdk//50 org.gnome.Platform//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
|
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
mkdir -p build-dir/files/src/vietc
|
mkdir -p build-dir/files/src/vietc
|
||||||
rsync -a "$PROJECT_ROOT/" build-dir/files/src/vietc/ --exclude=target --exclude=.git
|
rsync -a "$PROJECT_ROOT/" build-dir/files/src/vietc/ --exclude=target --exclude=.git
|
||||||
|
|
||||||
# Symlink Rust SDK extension
|
BUILD='export PATH=/usr/lib/sdk/rust-stable/bin:$PATH
|
||||||
RUST_FILES=$(find /var/lib/flatpak/runtime/org.freedesktop.Sdk.Extension.rust-stable \
|
export CARGO_HOME=/app/cargo
|
||||||
-name "rustc" -type f 2>/dev/null | head -1 | sed 's|/bin/rustc||')
|
cd /app/src/vietc'
|
||||||
mkdir -p build-dir/files/usr/lib/sdk
|
|
||||||
ln -s "$RUST_FILES" build-dir/files/usr/lib/sdk/rust-stable
|
|
||||||
|
|
||||||
# Build all Rust binaries inside sandbox
|
# Build daemon + CLI + uinputd
|
||||||
echo "Compiling daemon, CLI, uinputd..."
|
echo ""
|
||||||
flatpak build --share=network build-dir sh -c '
|
echo "=== Compiling daemon, CLI, uinputd... ==="
|
||||||
export PATH=/usr/lib/sdk/rust-stable/bin:$PATH
|
flatpak build --share=network build-dir sh -c "$BUILD && cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd"
|
||||||
export CARGO_HOME=/app/cargo
|
|
||||||
cd /app/src/vietc
|
|
||||||
cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd
|
|
||||||
'
|
|
||||||
|
|
||||||
echo "Compiling system tray..."
|
# Install files
|
||||||
flatpak build --share=network build-dir sh -c '
|
echo ""
|
||||||
export PATH=/usr/lib/sdk/rust-stable/bin:$PATH
|
echo "=== Installing files... ==="
|
||||||
export CARGO_HOME=/app/cargo
|
flatpak build build-dir sh -c "
|
||||||
cd /app/src/vietc
|
set -e
|
||||||
cargo build --release --manifest-path ui/Cargo.toml
|
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
|
install -Dm644 /app/src/vietc/packaging/icons/vietc.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.svg
|
||||||
echo "Installing files..."
|
install -Dm644 /app/src/vietc/packaging/icons/vietc-vn.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-vn.svg
|
||||||
flatpak build build-dir sh -c '
|
install -Dm644 /app/src/vietc/packaging/icons/vietc-en.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-en.svg
|
||||||
set -e
|
|
||||||
install -Dm755 /app/src/vietc/target/release/vietc /app/bin/vietc
|
cat > /app/share/applications/io.github.vietc.VietPlus.desktop << END
|
||||||
install -Dm755 /app/src/vietc/target/release/vietc-cli /app/bin/vietc-cli
|
[Desktop Entry]
|
||||||
install -Dm755 /app/src/vietc/target/release/vietc-uinputd /app/bin/vietc-uinputd
|
Name=Viet+
|
||||||
install -Dm755 /app/src/vietc/ui/target/release/vietc-tray /app/bin/vietc-tray
|
Comment=Vietnamese Input Method
|
||||||
gcc -O2 -o /app/bin/vietc-xrecord /app/src/vietc/packaging/appimage/vietc-xrecord.c -lX11 -lXtst
|
Exec=/app/bin/vietc-daemon
|
||||||
install -Dm755 /app/src/vietc/packaging/flatpak/vietc-wrapper.sh /app/bin/vietc-wrapper.sh
|
Icon=io.github.vietc.VietPlus
|
||||||
install -Dm644 /app/src/vietc/packaging/appimage/vietc.desktop \
|
Terminal=false
|
||||||
/app/share/applications/io.github.vietc.VietPlus.desktop
|
Type=Application
|
||||||
sed -i "s/Icon=vietc/Icon=io.github.vietc.VietPlus/g" \
|
Categories=Utility;
|
||||||
/app/share/applications/io.github.vietc.VietPlus.desktop
|
END
|
||||||
install -Dm644 /app/src/vietc/vietc.toml /app/etc/vietc/config.toml
|
|
||||||
mkdir -p /app/share/icons/hicolor/256x256/apps
|
mkdir -p /app/share/metainfo
|
||||||
cp /app/src/vietc/packaging/appimage/AppDir/vietc.svg \
|
cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML'
|
||||||
/app/share/icons/hicolor/256x256/apps/io.github.vietc.VietPlus.svg 2>/dev/null || true
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
mkdir -p /app/share/metainfo
|
<component type='desktop-application'>
|
||||||
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>
|
<id>io.github.vietc.VietPlus</id>
|
||||||
<name>Viet+</name>
|
<name>Viet+</name>
|
||||||
<summary>Vietnamese Input Method for Linux</summary>
|
<summary>Vietnamese Input Method for Linux</summary>
|
||||||
|
|
@ -86,36 +65,39 @@ flatpak build build-dir sh -c '
|
||||||
</description>
|
</description>
|
||||||
<metadata_license>MIT</metadata_license>
|
<metadata_license>MIT</metadata_license>
|
||||||
<project_license>MIT</project_license>
|
<project_license>MIT</project_license>
|
||||||
<url type="homepage">https://github.com/vndangkhoa/vietc</url>
|
<url type='homepage'>https://github.com/vndangkhoa/vietc</url>
|
||||||
<provides><binary>vietc</binary></provides>
|
<provides><binary>vietc-daemon</binary></provides>
|
||||||
<categories><category>Utility</category></categories>
|
<categories><category>Utility</category></categories>
|
||||||
</component>
|
</component>
|
||||||
XML
|
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
|
# Finish
|
||||||
echo "Finalizing build..."
|
echo ""
|
||||||
|
echo "=== Finalizing build... ==="
|
||||||
flatpak build-finish build-dir \
|
flatpak build-finish build-dir \
|
||||||
--socket=x11 \
|
--socket=x11 \
|
||||||
--socket=wayland \
|
--socket=wayland \
|
||||||
--socket=session-bus \
|
--filesystem=home \
|
||||||
--share=ipc \
|
--share=ipc \
|
||||||
--device=all \
|
--talk-name=org.freedesktop.Notifications \
|
||||||
--command=vietc-wrapper.sh
|
--talk-name=org.a11y.Bus \
|
||||||
|
--command=vietc-daemon
|
||||||
|
|
||||||
# Export to local repository
|
# Export
|
||||||
echo "Exporting to repository..."
|
echo ""
|
||||||
flatpak build-export vietc-repo build-dir
|
echo "=== Exporting to repository... ==="
|
||||||
|
flatpak build-export repo build-dir
|
||||||
|
|
||||||
# Create single-file bundle
|
# Bundle
|
||||||
echo "Creating bundle..."
|
echo ""
|
||||||
flatpak build-bundle vietc-repo "VietPlus-${VERSION}.flatpak" io.github.vietc.VietPlus
|
echo "=== Creating bundle... ==="
|
||||||
|
flatpak build-bundle repo "VietPlus-${VERSION}.flatpak" io.github.vietc.VietPlus
|
||||||
|
|
||||||
|
echo ""
|
||||||
echo "=== Done ==="
|
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 ""
|
||||||
echo "Install: flatpak install --user --bundle VietPlus-${VERSION}.flatpak"
|
echo "Install: flatpak install --user --bundle VietPlus-${VERSION}.flatpak"
|
||||||
echo "Run: flatpak run io.github.vietc.VietPlus"
|
echo "Run: flatpak run io.github.vietc.VietPlus"
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,21 @@
|
||||||
{
|
{
|
||||||
"app-id": "io.github.vietc.VietPlus",
|
"app-id": "io.github.vietc.VietPlus",
|
||||||
"runtime": "org.gnome.Platform",
|
"runtime": "org.gnome.Platform",
|
||||||
"runtime-version": "47",
|
"runtime-version": "50",
|
||||||
"sdk": "org.gnome.Sdk",
|
"sdk": "org.gnome.Sdk",
|
||||||
"sdk-extensions": [
|
"sdk-extensions": [
|
||||||
"org.freedesktop.Sdk.Extension.rust-stable"
|
"org.freedesktop.Sdk.Extension.rust-stable"
|
||||||
],
|
],
|
||||||
"command": "vietc-wrapper.sh",
|
"command": "vietc-daemon",
|
||||||
"rename-desktop-file": "vietc.desktop",
|
|
||||||
"rename-icon": "vietc",
|
|
||||||
"finish-args": [
|
"finish-args": [
|
||||||
"--socket=x11",
|
"--socket=x11",
|
||||||
"--socket=wayland",
|
"--socket=wayland",
|
||||||
"--socket=session-bus",
|
"--filesystem=home",
|
||||||
"--device=all",
|
"--share=ipc",
|
||||||
"--share=ipc"
|
"--talk-name=org.freedesktop.Notifications",
|
||||||
|
"--talk-name=org.a11y.Bus"
|
||||||
],
|
],
|
||||||
"modules": [
|
"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",
|
"name": "vietc",
|
||||||
"buildsystem": "simple",
|
"buildsystem": "simple",
|
||||||
|
|
@ -47,57 +28,25 @@
|
||||||
"build-commands": [
|
"build-commands": [
|
||||||
"cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd --manifest-path /run/build/vietc/Cargo.toml",
|
"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-daemon",
|
||||||
|
|
||||||
"install -Dm755 target/release/vietc /app/bin/vietc",
|
|
||||||
"install -Dm755 target/release/vietc-cli /app/bin/vietc-cli",
|
"install -Dm755 target/release/vietc-cli /app/bin/vietc-cli",
|
||||||
"install -Dm755 target/release/vietc-uinputd /app/bin/vietc-uinputd",
|
"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",
|
"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",
|
||||||
"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/metainfo",
|
"mkdir -p /app/share/metainfo",
|
||||||
"cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML'",
|
"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"
|
||||||
"<?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"
|
|
||||||
],
|
],
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"type": "dir",
|
"type": "dir",
|
||||||
"path": "../.."
|
"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;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
pub mod inject;
|
pub mod inject;
|
||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
pub mod uinput_monitor;
|
pub mod uinput_monitor;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use crate::inject::KeyEvent;
|
use crate::inject::KeyEvent;
|
||||||
|
|
||||||
pub trait KeyMonitor {
|
pub trait KeyMonitor {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::os::unix::io::AsRawFd;
|
use std::os::unix::io::AsRawFd;
|
||||||
use std::sync::{Arc, Condvar, Mutex};
|
use std::sync::{Arc, Condvar, Mutex};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::inject::{InjectResult, KeyInjector};
|
use crate::inject::{InjectResult, KeyInjector};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ffi::{c_char, c_int, c_void};
|
use std::ffi::{c_char, c_int, c_void};
|
||||||
use std::io::{Read, BufRead};
|
use std::io::{Read, BufRead};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use super::inject::{InjectResult, KeyInjector};
|
use super::inject::{InjectResult, KeyInjector};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::ffi::{c_char, c_int, c_void};
|
use std::ffi::{c_char, c_int, c_void};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use ksni::{menu::*, MenuItem, Tray};
|
use ksni::{menu::*, MenuItem, Tray};
|
||||||
|
|
||||||
|
fn is_flatpak() -> bool {
|
||||||
|
std::path::Path::new("/app/bin/vietc-daemon").exists()
|
||||||
|
}
|
||||||
|
|
||||||
fn write_status(state: &str) {
|
fn write_status(state: &str) {
|
||||||
if let Some(config_dir) = dirs::config_dir() {
|
if let Some(config_dir) = dirs::config_dir() {
|
||||||
let _ = std::fs::write(config_dir.join("vietc").join("status"), state);
|
let _ = std::fs::write(config_dir.join("vietc").join("status"), state);
|
||||||
|
|
@ -242,7 +247,13 @@ impl Tray for VietTray {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_name(&self) -> String {
|
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()
|
"vietc-vn".into()
|
||||||
} else {
|
} else {
|
||||||
"vietc-en".into()
|
"vietc-en".into()
|
||||||
|
|
@ -250,9 +261,20 @@ impl Tray for VietTray {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_theme_path(&self) -> String {
|
fn icon_theme_path(&self) -> String {
|
||||||
// Use XDG user theme path for icons
|
// Use XDG user theme path for icons (works in both native and Flatpak)
|
||||||
dirs::home_dir()
|
if let Some(home) = dirs::home_dir() {
|
||||||
.map(|d| d.join(".local/share/icons").to_string_lossy().into_owned())
|
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())
|
.unwrap_or_else(|| "/usr/share/icons".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::io::AsRawFd;
|
use std::os::unix::io::AsRawFd;
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue