diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | README_zh_CN.md | 2 | ||||
| -rw-r--r-- | build.rs | 64 | ||||
| -rw-r--r-- | docs/images/Yizi.ico | bin | 0 -> 270622 bytes | |||
| -rw-r--r-- | export.ps1 | 13 | ||||
| -rwxr-xr-x | export.sh | 4 | ||||
| -rw-r--r-- | setup/linux/inst.sh (renamed from scripts/inst.sh) | 0 | ||||
| -rw-r--r-- | setup/windows/inst.ps1 | 8 | ||||
| -rw-r--r-- | setup/windows/setup_jv_cli_template.iss | 42 | ||||
| -rw-r--r-- | setup/windows/uninst.ps1 | 39 | ||||
| -rw-r--r-- | src/bin/jvii.rs | 130 |
13 files changed, 303 insertions, 6 deletions
@@ -22,3 +22,6 @@ # Compile info /src/data/compile_info.rs + +# Setup script +/setup/windows/setup_jv_cli.iss @@ -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"] @@ -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 ``` @@ -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 Binary files differnew file mode 100644 index 0000000..24a2d00 --- /dev/null +++ b/docs/images/Yizi.ico 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 + } +} @@ -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(¤t_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 |
