summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2025-12-10 13:02:08 +0800
committer魏曹先生 <1992414357@qq.com>2025-12-10 13:02:08 +0800
commit04010f6cffa34253d1883229c9f0d831b16e3367 (patch)
tree591ff3306a1ee12e78ba2e4973a71f89b58be6a3
parentc740e28bd851221f32dc3f48cd94ee78352bba93 (diff)
parent15b508b7931aacd0c07ad6f52d4cefa6eef69fa1 (diff)
Merge remote-tracking branch 'origin/main'
-rw-r--r--.gitignore3
-rw-r--r--Cargo.toml2
-rw-r--r--README.md2
-rw-r--r--README_zh_CN.md2
-rw-r--r--build.rs64
-rw-r--r--docs/images/Yizi.icobin0 -> 270622 bytes
-rw-r--r--export.ps113
-rwxr-xr-xexport.sh4
-rw-r--r--setup/linux/inst.sh (renamed from scripts/inst.sh)0
-rw-r--r--setup/windows/inst.ps18
-rw-r--r--setup/windows/setup_jv_cli_template.iss42
-rw-r--r--setup/windows/uninst.ps139
-rw-r--r--src/bin/jvii.rs130
13 files changed, 303 insertions, 6 deletions
diff --git a/.gitignore b/.gitignore
index a3e2496..e00561c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,6 @@
# Compile info
/src/data/compile_info.rs
+
+# Setup script
+/setup/windows/setup_jv_cli.iss
diff --git a/Cargo.toml b/Cargo.toml
index 92ed9ed..524c96a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,8 @@
name = "just_enough_vcs_cli"
edition = "2024"
build = "build.rs"
+authors = ["JustEnoughVCS Team"]
+homepage = "http://jvcs.cc/"
[workspace]
members = ["crates/build_helper"]
diff --git a/README.md b/README.md
index 4545a99..0770896 100644
--- a/README.md
+++ b/README.md
@@ -20,5 +20,5 @@
```bash
# Linux
-curl -s https://raw.githubusercontent.com/JustEnoughVCS/CommandLine/main/scripts/inst.sh | bash
+curl -s https://raw.githubusercontent.com/JustEnoughVCS/CommandLine/main/setup/linux/inst.sh | bash
```
diff --git a/README_zh_CN.md b/README_zh_CN.md
index dcd9537..e129640 100644
--- a/README_zh_CN.md
+++ b/README_zh_CN.md
@@ -20,5 +20,5 @@
```bash
# Linux
-curl -s https://raw.githubusercontent.com/JustEnoughVCS/CommandLine/main/scripts/inst.sh | bash
+curl -s https://raw.githubusercontent.com/JustEnoughVCS/CommandLine/main/setup/linux/inst.sh | bash
```
diff --git a/build.rs b/build.rs
index 04fcd71..e215418 100644
--- a/build.rs
+++ b/build.rs
@@ -7,12 +7,76 @@ const COMPILE_INFO_RS_TEMPLATE: &str = "./src/data/compile_info.rs.template";
fn main() {
let repo_root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+ // Only generate installer script on Windows
+ if cfg!(target_os = "windows") {
+ if let Err(e) = generate_installer_script(&repo_root) {
+ eprintln!("Failed to generate installer script: {}", e);
+ std::process::exit(1);
+ }
+ }
+
if let Err(e) = generate_compile_info(&repo_root) {
eprintln!("Failed to generate compile info: {}", e);
std::process::exit(1);
}
}
+/// Generate Inno Setup installer script (Windows only)
+fn generate_installer_script(repo_root: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
+ let template_path = repo_root.join("setup/windows/setup_jv_cli_template.iss");
+ let output_path = repo_root.join("setup/windows/setup_jv_cli.iss");
+
+ let template = std::fs::read_to_string(&template_path)?;
+
+ let author = get_author()?;
+ let version = get_version();
+ let site = get_site()?;
+
+ let generated = template
+ .replace("<<<AUTHOR>>>", &author)
+ .replace("<<<VERSION>>>", &version)
+ .replace("<<<SITE>>>", &site);
+
+ std::fs::write(output_path, generated)?;
+ Ok(())
+}
+
+fn get_author() -> Result<String, Box<dyn std::error::Error>> {
+ let cargo_toml_path = std::path::Path::new("Cargo.toml");
+ let cargo_toml_content = std::fs::read_to_string(cargo_toml_path)?;
+ let cargo_toml: toml::Value = toml::from_str(&cargo_toml_content)?;
+
+ if let Some(package) = cargo_toml.get("package") {
+ if let Some(authors) = package.get("authors") {
+ if let Some(authors_array) = authors.as_array() {
+ if let Some(first_author) = authors_array.get(0) {
+ if let Some(author_str) = first_author.as_str() {
+ return Ok(author_str.to_string());
+ }
+ }
+ }
+ }
+ }
+
+ Err("Author not found in Cargo.toml".into())
+}
+
+fn get_site() -> Result<String, Box<dyn std::error::Error>> {
+ let cargo_toml_path = std::path::Path::new("Cargo.toml");
+ let cargo_toml_content = std::fs::read_to_string(cargo_toml_path)?;
+ let cargo_toml: toml::Value = toml::from_str(&cargo_toml_content)?;
+
+ if let Some(package) = cargo_toml.get("package") {
+ if let Some(homepage) = package.get("homepage") {
+ if let Some(site_str) = homepage.as_str() {
+ return Ok(site_str.to_string());
+ }
+ }
+ }
+
+ Err("Homepage not found in Cargo.toml".into())
+}
+
/// Generate compile info
fn generate_compile_info(repo_root: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
// Read the template code
diff --git a/docs/images/Yizi.ico b/docs/images/Yizi.ico
new file mode 100644
index 0000000..24a2d00
--- /dev/null
+++ b/docs/images/Yizi.ico
Binary files differ
diff --git a/export.ps1 b/export.ps1
new file mode 100644
index 0000000..d24c37e
--- /dev/null
+++ b/export.ps1
@@ -0,0 +1,13 @@
+# Require : Cargo (Rust), ISCC (Inno Setup)
+
+# Build
+cargo build --workspace --release
+if ($LASTEXITCODE -ne 0) {
+ # Build failed
+} else {
+ # Build succeeded
+ # Export
+ if (cargo run --manifest-path crates/build_helper/Cargo.toml --bin exporter) {
+ ISCC /Q .\setup\windows\setup_jv_cli.iss
+ }
+}
diff --git a/export.sh b/export.sh
index 5428107..52215ba 100755
--- a/export.sh
+++ b/export.sh
@@ -1,7 +1,9 @@
#!/bin/bash
+# Require : Cargo (Rust)
+
# Build
-if cargo build --workspace --release; then
+if cargo build --workspace --release >/dev/null 2>&1; then
# Export
cargo run --manifest-path crates/build_helper/Cargo.toml --bin exporter
fi
diff --git a/scripts/inst.sh b/setup/linux/inst.sh
index bbebeb8..bbebeb8 100644
--- a/scripts/inst.sh
+++ b/setup/linux/inst.sh
diff --git a/setup/windows/inst.ps1 b/setup/windows/inst.ps1
new file mode 100644
index 0000000..2e15dc3
--- /dev/null
+++ b/setup/windows/inst.ps1
@@ -0,0 +1,8 @@
+. ".\uninst.ps1"
+
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+
+$parentDir = Split-Path -Parent $scriptDir
+Add-Content -Path $PROFILE -Value "`n# JustEnoughVCS - Begin #"
+Add-Content -Path $PROFILE -Value ". `"$parentDir\jv_cli.ps1`""
+Add-Content -Path $PROFILE -Value "# JustEnoughVCS - End #"
diff --git a/setup/windows/setup_jv_cli_template.iss b/setup/windows/setup_jv_cli_template.iss
new file mode 100644
index 0000000..124d897
--- /dev/null
+++ b/setup/windows/setup_jv_cli_template.iss
@@ -0,0 +1,42 @@
+#define MyAppName "JustEnoughVCS"
+#define MyAppVersion "<<<VERSION>>>"
+#define MyAppPublisher "<<<AUTHOR>>>"
+#define MyAppURL "<<<SITE>>>"
+
+[Setup]
+AppId={{8265DF21-F290-487E-9403-C2730EC31A03}
+AppName={#MyAppName}
+AppVersion={#MyAppVersion}
+AppPublisher={#MyAppPublisher}
+AppPublisherURL={#MyAppURL}
+AppSupportURL={#MyAppURL}
+AppUpdatesURL={#MyAppURL}
+DefaultDirName={autopf}\{#MyAppName}
+DefaultGroupName={#MyAppName}
+AllowNoIcons=yes
+LicenseFile=..\..\LICENSE
+PrivilegesRequired=lowest
+OutputDir=..\..\export\setup
+OutputBaseFilename=JustEnoughVCS For Windows
+SetupIconFile=..\..\docs\images\Yizi.ico
+SolidCompression=yes
+WizardStyle=modern dynamic
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Files]
+Source: "..\..\export\*"; Excludes: "setup"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+Source: "inst.ps1"; DestDir: "{app}\scripts\"; Flags: ignoreversion
+Source: "uninst.ps1"; DestDir: "{app}\scripts\"; Flags: ignoreversion
+
+[Run]
+Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\scripts\inst.ps1"""; Flags: runhidden; Description: "Running post-installation script..."; StatusMsg: "Running post-installation script..."; AfterInstall: RunPostInstall
+
+[UninstallRun]
+Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\scripts\uninst.ps1"""; Flags: runhidden; RunOnceId: "UninstallScript"
+
+[Code]
+procedure RunPostInstall;
+begin
+end;
diff --git a/setup/windows/uninst.ps1 b/setup/windows/uninst.ps1
new file mode 100644
index 0000000..d5c898d
--- /dev/null
+++ b/setup/windows/uninst.ps1
@@ -0,0 +1,39 @@
+$profileContent = Get-Content $PROFILE -ErrorAction SilentlyContinue
+if ($profileContent) {
+ $startMarker = "# JustEnoughVCS - Begin #"
+ $endMarker = "# JustEnoughVCS - End #"
+ $newContent = @()
+ $insideBlock = $false
+ $foundStart = $false
+
+ foreach ($line in $profileContent) {
+ if ($line.Trim() -eq $startMarker) {
+ $insideBlock = $true
+ $foundStart = $true
+ continue
+ }
+ if ($line.Trim() -eq $endMarker) {
+ $insideBlock = $false
+ continue
+ }
+ if (-not $insideBlock) {
+ $newContent += $line
+ }
+ }
+
+ if ($foundStart -and $insideBlock) {
+ $newContent = @()
+ $insideBlock = $false
+ foreach ($line in $profileContent) {
+ if ($line.Trim() -eq $startMarker) {
+ $insideBlock = $true
+ continue
+ }
+ if (-not $insideBlock) {
+ $newContent += $line
+ }
+ }
+ }
+
+ $newContent | Set-Content $PROFILE
+}
diff --git a/src/bin/jvii.rs b/src/bin/jvii.rs
index aacd371..83d6162 100644
--- a/src/bin/jvii.rs
+++ b/src/bin/jvii.rs
@@ -2,12 +2,13 @@ use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
+use std::time::{Duration, Instant};
use clap::{Parser, command};
use crossterm::{
QueueableCommand,
cursor::MoveTo,
- event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
+ event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
execute,
style::{self, Color, Print, SetForegroundColor},
terminal::{
@@ -46,6 +47,10 @@ struct Editor {
modified: bool,
terminal_size: (u16, u16),
should_exit: bool,
+ #[cfg(windows)]
+ last_key_event: Option<(KeyCode, KeyModifiers, Instant)>,
+ #[cfg(windows)]
+ ime_composing: bool,
}
impl Editor {
@@ -53,7 +58,7 @@ impl Editor {
let content = if file_path.exists() {
fs::read_to_string(&file_path)?
.lines()
- .map(|s| s.to_string())
+ .map(|line| line.to_string())
.collect()
} else {
vec![String::new()]
@@ -71,6 +76,10 @@ impl Editor {
modified: false,
terminal_size: (width, height),
should_exit: false,
+ #[cfg(windows)]
+ last_key_event: None,
+ #[cfg(windows)]
+ ime_composing: false,
})
}
@@ -367,6 +376,9 @@ impl Editor {
return Err(e);
}
+ // Clear input buffer to avoid leftover keystrokes from command execution
+ self.clear_input_buffer()?;
+
// Initial render
if let Err(e) = self.render(&mut stdout) {
self.cleanup_terminal(&mut stdout)?;
@@ -377,6 +389,25 @@ impl Editor {
let result = loop {
match event::read() {
Ok(Event::Key(key_event)) => {
+ // Windows-specific input handling for IME and duplicate events
+ #[cfg(windows)]
+ {
+ // Skip key release events (we only care about presses)
+ if matches!(key_event.kind, KeyEventKind::Release) {
+ continue;
+ }
+
+ // Handle IME composition
+ if self.should_skip_ime_event(&key_event) {
+ continue;
+ }
+
+ // Skip duplicate events
+ if self.is_duplicate_event(&key_event) {
+ continue;
+ }
+ }
+
if let Err(e) = self.handle_key_event(key_event, &mut stdout) {
break Err(e);
}
@@ -409,6 +440,84 @@ impl Editor {
Ok(())
}
+ fn clear_input_buffer(&self) -> io::Result<()> {
+ // Try to read and discard any pending events in the buffer
+ while event::poll(Duration::from_millis(0))? {
+ let _ = event::read()?;
+ }
+ Ok(())
+ }
+
+ #[cfg(windows)]
+ fn is_duplicate_event(&mut self, key_event: &KeyEvent) -> bool {
+ let now = Instant::now();
+ let current_event = (key_event.code.clone(), key_event.modifiers, now);
+
+ // Check if this is the same event that just happened
+ if let Some((last_code, last_modifiers, last_time)) = &self.last_key_event {
+ if *last_code == key_event.code
+ && *last_modifiers == key_event.modifiers
+ && now.duration_since(*last_time) < Duration::from_millis(20)
+ // Reduced to 20ms for better responsiveness
+ {
+ // This is likely a duplicate event from IME or Windows input handling
+ return true;
+ }
+ }
+
+ // Update last event
+ self.last_key_event = Some(current_event);
+ false
+ }
+
+ #[cfg(not(windows))]
+ fn is_duplicate_event(&mut self, _key_event: &KeyEvent) -> bool {
+ false
+ }
+
+ #[cfg(windows)]
+ fn should_skip_ime_event(&mut self, key_event: &KeyEvent) -> bool {
+ // Check for IME composition markers
+ match &key_event.code {
+ KeyCode::Char(c) => {
+ // IME composition often produces control characters or special sequences
+ let c_u32 = *c as u32;
+
+ // Check for IME composition start/end markers
+ // Some IMEs use special characters or sequences
+ if c_u32 == 0x16 || c_u32 == 0x17 || c_u32 == 0x18 {
+ // These are common IME control characters
+ self.ime_composing = true;
+ return true;
+ }
+
+ // Check for dead keys or composition characters
+ if c_u32 < 0x20 || (c_u32 >= 0x80 && c_u32 < 0xA0) {
+ // Control characters or C1 control codes
+ return true;
+ }
+
+ // If we were composing and get a normal character, check if it's part of composition
+ if self.ime_composing {
+ // Reset composition state when we get a printable character
+ if c.is_ascii_graphic() || c.is_alphanumeric() {
+ self.ime_composing = false;
+ } else {
+ return true;
+ }
+ }
+
+ false
+ }
+ _ => false,
+ }
+ }
+
+ #[cfg(not(windows))]
+ fn should_skip_ime_event(&mut self, _key_event: &KeyEvent) -> bool {
+ false
+ }
+
fn handle_key_event(&mut self, key_event: KeyEvent, stdout: &mut io::Stdout) -> io::Result<()> {
match key_event.code {
KeyCode::Char('s') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
@@ -419,6 +528,15 @@ impl Editor {
self.show_message(&t!("jvii.messages.file_saved"), stdout)?;
}
}
+ KeyCode::Char('v') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
+ // Handle Ctrl+V paste - skip normal character insertion
+ // On Windows, Ctrl+V might also generate a 'v' character event
+ // We'll handle paste separately if needed
+ self.is_selecting = false;
+ self.selection_start = None;
+ // For now, just ignore Ctrl+V to prevent extra 'v' character
+ return Ok(());
+ }
KeyCode::Char(c) => {
if key_event.modifiers.contains(KeyModifiers::SHIFT) {
self.is_selecting = true;
@@ -433,7 +551,9 @@ impl Editor {
// Handle special characters
match c {
'\n' | '\r' => self.new_line(),
- _ => self.insert_char(c),
+ _ => {
+ self.insert_char(c);
+ }
}
}
KeyCode::Backspace => {
@@ -518,6 +638,10 @@ async fn main() {
// Init i18n
set_locale(&current_locales());
+ // Windows specific initialization for colored output
+ #[cfg(windows)]
+ let _ = colored::control::set_virtual_terminal(true);
+
let args = JustEnoughVcsInputer::parse();
// Check if a file argument was provided