summaryrefslogtreecommitdiff
path: root/src/bin/jv.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/jv.rs')
-rw-r--r--src/bin/jv.rs6185
1 files changed, 0 insertions, 6185 deletions
diff --git a/src/bin/jv.rs b/src/bin/jv.rs
deleted file mode 100644
index be6d0c1..0000000
--- a/src/bin/jv.rs
+++ /dev/null
@@ -1,6185 +0,0 @@
-use colored::Colorize;
-use just_enough_vcs::{
- data::compile_info::CoreCompileInfo,
- system::action_system::{action::ActionContext, action_pool::ActionPool},
- utils::{
- cfg_file::config::ConfigFile,
- data_struct::data_sort::quick_sort_with_cmp,
- sha1_hash,
- string_proc::{
- self,
- format_path::{format_path, format_path_str},
- snake_case,
- },
- tcp_connection::instance::ConnectionInstance,
- },
- vcs::{
- constants::{
- CLIENT_FILE_TODOLIST, CLIENT_FILE_WORKSPACE, CLIENT_FOLDER_WORKSPACE_ROOT_NAME,
- CLIENT_PATH_WORKSPACE_ROOT, PORT, VAULT_HOST_NAME,
- },
- data::{
- local::{
- LocalWorkspace,
- align_tasks::{AlignTaskName, AlignTasks},
- cached_sheet::CachedSheet,
- latest_file_data::LatestFileData,
- latest_info::LatestInfo,
- modified_status::check_vault_modified,
- workspace_analyzer::{AnalyzeResult, FromRelativePathBuf},
- workspace_config::LocalConfig,
- },
- member::{Member, MemberId},
- sheet::{SheetData, SheetMappingMetadata},
- user::UserDirectory,
- vault::{
- mapping_share::{Share, ShareMergeMode},
- virtual_file::{VirtualFileId, VirtualFileVersion},
- },
- },
- docs::{ASCII_YIZI, document, documents},
- env::{correct_current_dir, current_cfg_dir, current_local_path},
- registry::client_registry,
- remote_actions::{
- content_manage::track_file::{
- CreateTaskResult, NextVersion, SyncTaskResult, TrackFileActionArguments,
- TrackFileActionResult, UpdateDescription, UpdateTaskResult, VerifyFailReason,
- proc_track_file_action,
- },
- edit_right_manage::change_virtual_file_edit_right::{
- ChangeVirtualFileEditRightResult, EditRightChangeBehaviour,
- proc_change_virtual_file_edit_right_action,
- },
- mapping_manage::{
- edit_mapping::{
- EditMappingActionArguments, EditMappingActionResult, EditMappingOperations,
- InvalidMoveReason, OperationArgument, proc_edit_mapping_action,
- },
- merge_share_mapping::{
- MergeShareMappingActionResult, MergeShareMappingArguments,
- proc_merge_share_mapping_action,
- },
- share_mapping::{
- ShareMappingActionResult, ShareMappingArguments, proc_share_mapping_action,
- },
- },
- sheet_manage::{
- drop_sheet::{DropSheetActionResult, proc_drop_sheet_action},
- make_sheet::{MakeSheetActionResult, proc_make_sheet_action},
- },
- workspace_manage::{
- set_upstream_vault::{
- SetUpstreamVaultActionResult, proc_set_upstream_vault_action,
- },
- update_to_latest_info::{
- SyncCachedSheetFailReason, UpdateToLatestInfoResult,
- proc_update_to_latest_info_action,
- },
- },
- },
- },
-};
-use std::{
- collections::{BTreeMap, HashMap, HashSet},
- env::{current_dir, set_current_dir},
- io::Error,
- net::SocketAddr,
- path::PathBuf,
- process::exit,
- str::FromStr,
- sync::Arc,
- time::SystemTime,
-};
-
-use clap::{Parser, Subcommand};
-use just_enough_vcs::utils::tcp_connection::error::TcpTargetError;
-use just_enough_vcs_cli::{
- data::{
- compile_info::CompileInfo,
- ipaddress_history::{get_recent_ip_address, insert_recent_ip_address},
- },
- output::{
- accounts::{AccountItem, AccountListJsonResult},
- align::{AlignJsonResult, AlignTaskMapping},
- analyzer_result::{AnalyzerJsonResult, ModifiedItem, ModifiedType, MovedItem},
- here::{HereJsonResult, HereJsonResultItem},
- info::{InfoHistory, InfoJsonResult},
- share::{SeeShareResult, ShareItem, ShareListResult},
- sheets::{SheetItem, SheetListJsonResult},
- },
- utils::{
- display::{SimpleTable, display_width, md, render_share_path_tree, size_str},
- env::{auto_update_outdate, current_locales, enable_auto_update},
- fs::move_across_partitions,
- globber::{GlobItem, Globber},
- input::{confirm_hint, confirm_hint_or, input_with_editor, show_in_pager},
- push_version::push_version,
- socket_addr_helper,
- },
-};
-use rust_i18n::{set_locale, t};
-use tokio::{
- fs::{self},
- net::TcpSocket,
- process::Command,
- sync::mpsc::{self, Receiver},
-};
-
-// Import i18n files
-rust_i18n::i18n!("resources/locales", fallback = "en");
-
-#[derive(Parser, Debug)]
-#[command(
- disable_help_flag = true,
- disable_version_flag = true,
- disable_help_subcommand = true,
- help_template = "{all-args}"
-)]
-
-struct JustEnoughVcsWorkspace {
- #[command(subcommand)]
- command: JustEnoughVcsWorkspaceCommand,
-}
-
-#[derive(Subcommand, Debug)]
-enum JustEnoughVcsWorkspaceCommand {
- /// Version information
- #[command(alias = "--version", alias = "-v")]
- Version(VersionArgs),
-
- /// Display help information
- #[command(alias = "--help", alias = "-h")]
- Help,
-
- // Member management
- /// Manage your local accounts
- #[command(subcommand, alias = "acc")]
- Account(AccountManage),
-
- /// Create an empty workspace
- Create(CreateWorkspaceArgs),
-
- /// Create an empty workspace in the current directory
- Init(InitWorkspaceArgs),
-
- /// Get workspace information in the current directory
- #[command(alias = "h")]
- Here(HereArgs),
-
- /// Display current sheet status information
- #[command(alias = "s")]
- Status(StatusArgs),
-
- /// Display detailed information about the specified file
- Info(InfoArgs),
-
- // Sheet management
- /// Manage sheets in the workspace
- #[command(subcommand, alias = "sh")]
- Sheet(SheetManage),
-
- // File management
- /// Track files to the upstream vault
- /// First track - Create and upload the "First Version", then hold them
- /// Subsequent tracks - Update files with new versions
- #[command(alias = "t")]
- Track(TrackFileArgs),
-
- /// Hold files for editing
- #[command(alias = "hd")]
- Hold(HoldFileArgs),
-
- /// Throw files, and release edit rights
- #[command(alias = "tr")]
- Throw(ThrowFileArgs),
-
- /// Move or rename files safely
- #[command(alias = "mv")]
- Move(MoveMappingArgs),
-
- /// Share file visibility to other sheets
- Share(ShareMappingArgs),
-
- /// Sync information from upstream vault
- #[command(alias = "u")]
- Update(UpdateArgs),
-
- // Connection management
- /// Direct to an upstream vault and stain this workspace
- Direct(DirectArgs),
-
- /// DANGER ZONE : Unstain this workspace
- Unstain(UnstainArgs),
-
- // Other
- /// Query built-in documentation
- Docs(DocsArgs),
-
- // Lazy commands
- /// Try exit current sheet
- Exit,
-
- /// Try exit current sheet and use another sheet
- Use(UseArgs),
-
- /// List all sheets
- Sheets,
-
- /// List all accounts
- Accounts,
-
- /// Align file structure
- Align(SheetAlignArgs),
-
- /// Set current local workspace account
- As(SetLocalWorkspaceAccountArgs),
-
- /// Make a new sheet
- Make(SheetMakeArgs),
-
- /// Drop a sheet
- Drop(SheetDropArgs),
-
- /// As Member, Direct, and Update
- #[command(alias = "signin")]
- Login(LoginArgs),
-
- // Completion Helpers
- /// Display History IP Address
- #[command(name = "_ip_history")]
- GetHistoryIpAddress,
-
- /// Display Workspace Directory
- #[command(name = "_workspace_dir")]
- GetWorkspaceDir,
-
- /// Display Current Account
- #[command(name = "_account")]
- GetCurrentAccount,
-
- /// Display Current Upstream Vault
- #[command(name = "_upstream")]
- GetCurrentUpstream,
-
- /// Display Current Sheet
- #[command(name = "_sheet")]
- GetCurrentSheet,
-
- // Debug Tools
- #[command(name = "_glob")]
- DebugGlob(DebugGlobArgs),
-}
-
-#[derive(Parser, Debug)]
-struct VersionArgs {
- #[arg(short = 'C', long = "compile-info")]
- compile_info: bool,
-
- #[arg(long)]
- without_banner: bool,
-}
-
-#[derive(Subcommand, Debug)]
-enum AccountManage {
- /// Show help information
- #[command(alias = "--help", alias = "-h")]
- Help,
-
- /// Register a member to this computer
- #[command(alias = "+")]
- Add(AccountAddArgs),
-
- /// Remove a account from this computer
- #[command(alias = "rm", alias = "-")]
- Remove(AccountRemoveArgs),
-
- /// List all accounts in this computer
- #[command(alias = "ls")]
- List(AccountListArgs),
-
- /// Set current local workspace account
- As(SetLocalWorkspaceAccountArgs),
-
- /// Move private key file to account
- #[command(alias = "mvkey", alias = "mvk", alias = "movekey")]
- MoveKey(MoveKeyToAccountArgs),
-
- /// Output public key file to specified directory
- #[command(alias = "genpub")]
- GeneratePublicKey(GeneratePublicKeyArgs),
-}
-
-#[derive(Subcommand, Debug)]
-enum SheetManage {
- /// Show help information
- #[command(alias = "--help", alias = "-h")]
- Help,
-
- /// List all sheets
- #[command(alias = "ls")]
- List(SheetListArgs),
-
- /// Use a sheet
- Use(SheetUseArgs),
-
- /// Exit current sheet
- Exit(SheetExitArgs),
-
- /// Create a new sheet
- #[command(alias = "mk")]
- Make(SheetMakeArgs),
-
- /// Drop current sheet
- Drop(SheetDropArgs),
-
- /// Align file structure
- Align(SheetAlignArgs),
-}
-
-#[derive(Parser, Debug)]
-struct SheetListArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Show other's sheets
- #[arg(short, long = "other")]
- others: bool,
-
- /// Show all sheets
- #[arg(short = 'A', long)]
- all: bool,
-
- /// Show raw output
- #[arg(short, long)]
- raw: bool,
-
- /// Show json output
- #[arg(long = "json")]
- json_output: bool,
-
- /// Show json output pretty
- #[arg(long = "pretty")]
- pretty: bool,
-}
-
-#[derive(Parser, Debug)]
-struct SheetUseArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Sheet name
- sheet_name: String,
-}
-
-#[derive(Parser, Debug)]
-struct SheetExitArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-}
-
-#[derive(Parser, Debug)]
-struct SheetMakeArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Sheet name
- sheet_name: String,
-}
-
-#[derive(Parser, Debug)]
-struct SheetDropArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Whether to skip confirmation
- #[arg(short = 'C', long)]
- confirm: bool,
-
- /// Sheet name
- sheet_name: String,
-}
-
-#[derive(Parser, Debug)]
-struct SheetAlignArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Align task
- task: Option<String>,
-
- /// Align operation
- to: Option<String>,
-
- /// List Option: All
- #[arg(long = "all")]
- list_all: bool,
-
- /// List Option: Unsolved
- #[arg(long = "unsolved")]
- list_unsolved: bool,
-
- /// List Option: Created
- #[arg(long = "created")]
- list_created: bool,
-
- /// Editor mode
- #[arg(short, long)]
- work: bool,
-
- /// Show raw output (for list)
- #[arg(short, long)]
- raw: bool,
-
- /// Show json output (for list)
- #[arg(long = "json")]
- json_output: bool,
-
- /// Show json output pretty
- #[arg(long = "pretty")]
- pretty: bool,
-}
-
-#[derive(Parser, Debug)]
-struct LoginArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Whether to skip confirmation
- #[arg(short = 'C', long)]
- confirm: bool,
-
- /// Member ID
- login_member_id: MemberId,
-
- /// Upstream
- upstream: String,
-}
-
-#[derive(Parser, Debug)]
-struct CreateWorkspaceArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Workspace directory path
- path: Option<PathBuf>,
-
- /// Force create, ignore files in the directory
- #[arg(short, long)]
- force: bool,
-}
-
-#[derive(Parser, Debug)]
-struct InitWorkspaceArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-}
-
-#[derive(Parser, Debug)]
-struct HereArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Show help information
- #[arg(short = 'd', long = "desc")]
- show_description: bool,
-
- /// Show json output
- #[arg(long = "json")]
- json_output: bool,
-
- /// Show json output pretty
- #[arg(long = "pretty")]
- pretty: bool,
-}
-
-#[derive(Parser, Debug)]
-struct StatusArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Show json output
- #[arg(long = "json")]
- json_output: bool,
-
- /// Show json output pretty
- #[arg(long = "pretty")]
- pretty: bool,
-}
-
-#[derive(Parser, Debug)]
-struct InfoArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// File pattern
- file_pattern: Option<String>,
-
- /// Full histories output
- #[arg(short, long = "full")]
- full: bool,
-
- /// Show json output
- #[arg(long = "json")]
- json_output: bool,
-
- /// Show json output pretty
- #[arg(long = "pretty")]
- pretty: bool,
-}
-
-#[derive(Parser, Debug)]
-struct AccountAddArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Account name
- account_name: String,
-
- /// Auto generate ED25519 private key
- #[arg(short = 'K', long = "keygen")]
- keygen: bool,
-}
-
-#[derive(Parser, Debug)]
-struct AccountRemoveArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Account name
- account_name: String,
-}
-
-#[derive(Parser, Debug)]
-struct AccountListArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Show raw output
- #[arg(short, long)]
- raw: bool,
-
- /// Show json output
- #[arg(long = "json")]
- json_output: bool,
-
- /// Show json output pretty
- #[arg(long = "pretty")]
- pretty: bool,
-}
-
-#[derive(Parser, Debug)]
-struct SetLocalWorkspaceAccountArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Account name
- account_name: String,
-}
-
-#[derive(Parser, Debug)]
-struct MoveKeyToAccountArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Account name
- account_name: String,
-
- /// Private key file path
- key_path: PathBuf,
-}
-
-#[derive(Parser, Debug)]
-struct GeneratePublicKeyArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Account name
- account_name: String,
-
- /// Private key file path
- output_dir: Option<PathBuf>,
-}
-
-#[derive(Parser, Debug, Clone)]
-struct TrackFileArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Track file pattern
- track_file_pattern: Option<String>,
-
- /// Overwrite modified
- #[arg(short = 'o', long = "overwrite")]
- allow_overwrite: bool,
-
- /// Commit - Description
- #[arg(short, long)]
- desc: Option<String>,
-
- /// Commit - Description
- #[arg(short = 'v', long = "version")]
- next_version: Option<String>,
-
- /// Commit - Editor mode
- #[arg(short, long)]
- work: bool,
-}
-
-#[derive(Parser, Debug)]
-struct HoldFileArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Hold file pattern
- hold_file_pattern: Option<String>,
-
- /// Show fail details
- #[arg(short = 'd', long = "details")]
- show_fail_details: bool,
-
- /// Skip failed items
- #[arg(short = 'S', long)]
- skip_failed: bool,
-
- /// Skip check
- #[arg(short = 'F', long)]
- force: bool,
-}
-
-#[derive(Parser, Debug)]
-struct ThrowFileArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Throw file pattern
- throw_file_pattern: Option<String>,
-
- /// Show fail details
- #[arg(short = 'd', long = "details")]
- show_fail_details: bool,
-
- /// Skip failed items
- #[arg(short = 'S', long)]
- skip_failed: bool,
-
- /// Skip check
- #[arg(short = 'F', long)]
- force: bool,
-}
-
-#[derive(Parser, Debug)]
-struct MoveMappingArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Move mapping pattern
- move_mapping_pattern: Option<String>,
-
- /// To mapping pattern
- to_mapping_pattern: Option<String>,
-
- /// Erase
- #[arg(short = 'e', long)]
- erase: bool,
-
- /// Only modify upstream mapping
- #[arg(short = 'r', long)]
- only_remote: bool,
-}
-
-#[derive(Parser, Debug)]
-struct ShareMappingArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Arguments 1
- args1: Option<String>,
-
- /// Arguments 2
- args2: Option<String>,
-
- /// Arguments 3
- args3: Option<String>,
-
- /// Safe merge
- #[arg(short = 's', long)]
- safe: bool,
-
- /// Skip all conflicting mappings
- #[arg(short = 'S', long)]
- skip: bool,
-
- /// Overwrite all conflicting mappings
- #[arg(short = 'o', long)]
- overwrite: bool,
-
- /// Reject this share
- #[arg(short = 'R', long)]
- reject: bool,
-
- /// Show raw output
- #[arg(short = 'r', long)]
- raw: bool,
-
- /// Show json output
- #[arg(long = "json")]
- json_output: bool,
-
- /// Show json output pretty
- #[arg(long = "pretty")]
- pretty: bool,
-
- /// Share - Editor mode
- #[arg(short, long)]
- work: bool,
-}
-
-#[derive(Parser, Debug)]
-struct UpdateArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Silent mode
- #[arg(short, long)]
- silent: bool,
-}
-
-#[derive(Parser, Debug)]
-struct DirectArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Upstream vault address
- upstream: Option<String>,
-
- /// Whether to skip confirmation
- #[arg(short = 'C', long)]
- confirm: bool,
-}
-
-#[derive(Parser, Debug)]
-struct UnstainArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Whether to skip confirmation
- #[arg(short = 'C', long)]
- confirm: bool,
-}
-
-#[derive(Parser, Debug)]
-struct DocsArgs {
- /// Show help information
- #[arg(short, long)]
- help: bool,
-
- /// Name of the docs
- docs_name: Option<String>,
-
- /// Direct output
- #[arg(short, long)]
- direct: bool,
-
- /// Show raw output for list
- #[arg(short, long)]
- raw: bool,
-}
-
-#[derive(Parser, Debug)]
-struct UseArgs {
- sheet_name: String,
-}
-
-#[derive(Parser, Debug)]
-struct DebugGlobArgs {
- /// Pattern
- pattern: String, // Using 'noglob jvv _glob' in ZSH plz
-}
-
-#[tokio::main]
-async fn main() {
- // Init i18n
- set_locale(&current_locales());
-
- // Init colored
- #[cfg(windows)]
- colored::control::set_virtual_terminal(true).unwrap();
-
- // Outdate update
- let required_outdated_minutes = auto_update_outdate();
- let outdate_update_enabled = required_outdated_minutes >= 0;
-
- // Auto update
- let enable_auto_update = enable_auto_update();
-
- // The following conditions will trigger automatic update:
- // 1. Auto-update is enabled
- // 2. Vault has been modified OR (timeout update is enabled AND timeout is set to 0)
- if enable_auto_update
- && (check_vault_modified().await
- || outdate_update_enabled && required_outdated_minutes == 0)
- {
- // Record current directory
- let path = match current_dir() {
- Ok(path) => path,
- Err(e) => {
- eprintln!("{}", t!("jv.fail.get_current_dir", error = e.to_string()));
- return;
- }
- };
- // Update
- // This will change the current current_dir
- jv_update(UpdateArgs {
- help: false,
- silent: true,
- })
- .await;
- // Restore current directory
- if let Err(e) = set_current_dir(&path) {
- eprintln!(
- "{}",
- t!(
- "jv.fail.std.set_current_dir",
- dir = path.display(),
- error = e
- )
- );
- return;
- }
- } else
- // If automatic update and timeout update are enabled,
- // but required time > 0 (not in disabled or always-update state)
- if enable_auto_update && outdate_update_enabled && required_outdated_minutes > 0 {
- // Read the last update time and calculate the duration
- if let Some(local_cfg) = LocalConfig::read().await.ok() {
- if let Some(local_dir) = current_local_path() {
- if let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_cfg.current_account(),
- ))
- .await
- {
- if let Some(update_instant) = latest_info.update_instant {
- let now = SystemTime::now();
- let duration_secs = now
- .duration_since(update_instant)
- .unwrap_or_default()
- .as_secs();
-
- if duration_secs > required_outdated_minutes as u64 * 60 {
- // Update
- // This will change the current current_dir
- jv_update(UpdateArgs {
- help: false,
- silent: true,
- })
- .await
- }
- }
- }
- };
- };
- }
-
- let Ok(parser) = JustEnoughVcsWorkspace::try_parse() else {
- eprintln!("{}", md(t!("jv.fail.parse.parser_failed")));
-
- // Tips
- // Guide to create
- {
- // Check if workspace exist
- let Some(local_dir) = current_local_path() else {
- println!();
- println!("{}", t!("jv.tip.not_workspace").trim().yellow());
- return;
- };
-
- let _ = correct_current_dir();
-
- // Check if account list is not empty
- let Some(dir) = UserDirectory::current_cfg_dir() else {
- return;
- };
-
- if let Ok(ids) = dir.account_ids() {
- if ids.len() < 1 {
- println!();
- println!("{}", t!("jv.tip.no_account").trim().yellow());
- return;
- }
- }
-
- // Check if the workspace has a registered account (account = unknown)
- let Some(local_cfg) = LocalConfig::read().await.ok() else {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- return;
- };
-
- // Account exists check
- if local_cfg.current_account() == "unknown" {
- println!();
- println!("{}", t!("jv.tip.no_account_set").trim().yellow());
- } else {
- if dir
- .account_ids()
- .ok()
- .map(|ids| !ids.contains(&local_cfg.current_account()))
- .unwrap_or(false)
- {
- println!();
- println!(
- "{}",
- t!(
- "jv.tip.account_not_exist",
- account = local_cfg.current_account()
- )
- .trim()
- .yellow()
- );
- return;
- }
- }
-
- // Outdated
- let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_cfg.current_account(),
- ))
- .await
- else {
- return;
- };
- if let Some(instant) = latest_info.update_instant {
- let now = SystemTime::now();
- let duration = now.duration_since(instant).unwrap_or_default();
-
- if duration.as_secs() > 60 * required_outdated_minutes.clamp(5, i64::MAX) as u64 {
- // Automatically prompt if exceeding the set timeout (at least 5 minutes)
- let hours = duration.as_secs() / 3600;
- let minutes = (duration.as_secs() % 3600) / 60;
-
- println!(
- "\n{}",
- t!("jv.tip.outdated", hour = hours, minutes = minutes)
- .trim()
- .yellow()
- );
- }
- }
- }
-
- return;
- };
-
- match parser.command {
- JustEnoughVcsWorkspaceCommand::Version(version_args) => {
- let compile_info = CompileInfo::default();
- let core_compile_info = CoreCompileInfo::default();
- if version_args.without_banner {
- println!(
- "{}",
- md(t!(
- "jv.version.header",
- version = compile_info.cli_version,
- vcs_version = core_compile_info.vcs_version
- ))
- );
- } else {
- println!();
- let ascii_art_banner = ASCII_YIZI
- .split('\n')
- .skip_while(|line| !line.contains("#BANNER START#"))
- .skip(1)
- .take_while(|line| !line.contains("#BANNER END#"))
- .collect::<Vec<&str>>()
- .join("\n");
-
- println!(
- "{}",
- ascii_art_banner
- .replace("{banner_line_1}", "JustEnoughVCS")
- .replace(
- "{banner_line_2}",
- &format!(
- "{}: {} ({})",
- t!("common.word.cli_version"),
- &compile_info.cli_version,
- &compile_info.date
- )
- )
- .replace(
- "{banner_line_3}",
- &format!(
- "{}: {}",
- t!("common.word.vcs_version"),
- &core_compile_info.vcs_version
- )
- )
- );
-
- if !version_args.compile_info {
- println!();
- }
- }
-
- if version_args.compile_info {
- println!(
- "\n{}",
- md(t!(
- "jv.version.compile_info",
- build_time = compile_info.date,
- build_target = compile_info.target,
- build_platform = compile_info.platform,
- build_toolchain = compile_info.toolchain,
- cli_build_branch = compile_info.build_branch,
- cli_build_commit =
- &compile_info.build_commit[..7.min(compile_info.build_commit.len())],
- core_build_branch = core_compile_info.build_branch,
- core_build_commit = &core_compile_info.build_commit
- [..7.min(core_compile_info.build_commit.len())]
- ))
- );
- }
- }
-
- JustEnoughVcsWorkspaceCommand::Help => {
- println!("{}", md(t!("jv.help")));
- }
-
- JustEnoughVcsWorkspaceCommand::Account(account_manage) => {
- let user_dir = match UserDirectory::current_cfg_dir() {
- Some(dir) => dir,
- None => {
- eprintln!("{}", t!("jv.fail.account.no_user_dir"));
- return;
- }
- };
-
- match account_manage {
- AccountManage::Help => {
- println!("{}", md(t!("jv.account")));
- }
- AccountManage::Add(account_add_args) => {
- if account_add_args.help {
- println!("{}", md(t!("jv.account")));
- return;
- }
- jv_account_add(user_dir, account_add_args).await;
- }
- AccountManage::Remove(account_remove_args) => {
- if account_remove_args.help {
- println!("{}", md(t!("jv.account")));
- return;
- }
- jv_account_remove(user_dir, account_remove_args).await;
- }
- AccountManage::List(account_list_args) => {
- if account_list_args.help {
- println!("{}", md(t!("jv.account")));
- return;
- }
- jv_account_list(user_dir, account_list_args).await;
- }
- AccountManage::As(set_local_workspace_account_args) => {
- if set_local_workspace_account_args.help {
- println!("{}", md(t!("jv.account")));
- return;
- }
- jv_account_as(user_dir, set_local_workspace_account_args).await;
- }
- AccountManage::MoveKey(move_key_to_account_args) => {
- if move_key_to_account_args.help {
- println!("{}", md(t!("jv.account")));
- return;
- }
- jv_account_move_key(user_dir, move_key_to_account_args).await;
- }
- AccountManage::GeneratePublicKey(generate_public_key_args) => {
- if generate_public_key_args.help {
- println!("{}", md(t!("jv.account")));
- return;
- }
- jv_account_generate_pub_key(user_dir, generate_public_key_args).await;
- }
- }
- }
- JustEnoughVcsWorkspaceCommand::Create(create_workspace_args) => {
- if create_workspace_args.help {
- println!("{}", md(t!("jv.create")));
- return;
- }
- jv_create(create_workspace_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Init(init_workspace_args) => {
- if init_workspace_args.help {
- println!("{}", md(t!("jv.init")));
- return;
- }
- jv_init(init_workspace_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Here(here_args) => {
- if here_args.help {
- println!("{}", md(t!("jv.here")));
- return;
- }
- jv_here(here_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Status(status_args) => {
- if status_args.help {
- println!("{}", md(t!("jv.status")));
- return;
- }
- jv_status(status_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Info(info_args) => {
- if info_args.help {
- println!("{}", md(t!("jv.info")));
- return;
- }
- jv_info(info_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Sheet(sheet_manage) => match sheet_manage {
- SheetManage::Help => {
- println!("{}", md(t!("jv.sheet")));
- return;
- }
- SheetManage::List(sheet_list_args) => jv_sheet_list(sheet_list_args).await,
- SheetManage::Use(sheet_use_args) => jv_sheet_use(sheet_use_args).await,
- SheetManage::Exit(sheet_exit_args) => {
- let _ = jv_sheet_exit(sheet_exit_args).await;
- }
- SheetManage::Make(sheet_make_args) => jv_sheet_make(sheet_make_args).await,
- SheetManage::Drop(sheet_drop_args) => jv_sheet_drop(sheet_drop_args).await,
- SheetManage::Align(sheet_align_args) => jv_sheet_align(sheet_align_args).await,
- },
- JustEnoughVcsWorkspaceCommand::Track(track_file_args) => {
- if track_file_args.help {
- println!("{}", md(t!("jv.track")));
- return;
- }
- jv_track(track_file_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Hold(hold_file_args) => {
- if hold_file_args.help {
- println!("{}", md(t!("jv.hold")));
- return;
- }
- jv_hold(hold_file_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Throw(throw_file_args) => {
- if throw_file_args.help {
- println!("{}", md(t!("jv.throw")));
- return;
- }
- jv_throw(throw_file_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Move(move_file_args) => {
- if move_file_args.help {
- println!("{}", md(t!("jv.move")));
- return;
- }
- jv_move(move_file_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Share(share_file_args) => {
- if share_file_args.help {
- println!("{}", md(t!("jv.share")));
- return;
- }
- jv_share(share_file_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Update(update_file_args) => {
- if update_file_args.help {
- println!("{}", md(t!("jv.update")));
- return;
- }
- jv_update(update_file_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Direct(direct_args) => {
- if direct_args.help {
- println!("{}", md(t!("jv.direct")));
- return;
- }
- jv_direct(direct_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Unstain(unstain_args) => {
- if unstain_args.help {
- println!("{}", md(t!("jv.unstain")));
- return;
- }
- jv_unstain(unstain_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Docs(docs_args) => {
- if docs_args.help {
- println!("{}", md(t!("jv.docs")));
- return;
- }
- jv_docs(docs_args).await;
- }
- JustEnoughVcsWorkspaceCommand::Exit => {
- let _ = jv_sheet_exit(SheetExitArgs { help: false }).await;
- }
- JustEnoughVcsWorkspaceCommand::Use(use_args) => {
- if let Ok(_) = jv_sheet_exit(SheetExitArgs { help: false }).await {
- jv_sheet_use(SheetUseArgs {
- help: false,
- sheet_name: use_args.sheet_name,
- })
- .await;
- }
- }
- JustEnoughVcsWorkspaceCommand::Sheets => {
- jv_sheet_list(SheetListArgs {
- help: false,
- others: false,
- all: false,
- raw: false,
- json_output: false,
- pretty: false,
- })
- .await
- }
- JustEnoughVcsWorkspaceCommand::Accounts => {
- let user_dir = match UserDirectory::current_cfg_dir() {
- Some(dir) => dir,
- None => {
- eprintln!("{}", t!("jv.fail.account.no_user_dir"));
- return;
- }
- };
- jv_account_list(
- user_dir,
- AccountListArgs {
- help: false,
- raw: false,
- json_output: false,
- pretty: false,
- },
- )
- .await
- }
- JustEnoughVcsWorkspaceCommand::Align(sheet_align_args) => {
- if sheet_align_args.help {
- println!("{}", md(t!("jv.align")));
- return;
- }
- jv_sheet_align(sheet_align_args).await
- }
- JustEnoughVcsWorkspaceCommand::As(args) => {
- let user_dir = match UserDirectory::current_cfg_dir() {
- Some(dir) => dir,
- None => {
- eprintln!("{}", t!("jv.fail.account.no_user_dir"));
- return;
- }
- };
- jv_account_as(user_dir, args).await
- }
- JustEnoughVcsWorkspaceCommand::Make(args) => {
- jv_sheet_make(args).await;
- }
- JustEnoughVcsWorkspaceCommand::Drop(args) => {
- jv_sheet_drop(args).await;
- }
- JustEnoughVcsWorkspaceCommand::Login(args) => {
- if !args.confirm {
- println!(
- "{}",
- t!(
- "jv.confirm.login",
- account = args.login_member_id,
- upstream = args.upstream
- )
- .trim()
- .yellow()
- );
- confirm_hint_or(t!("common.confirm"), || exit(1)).await;
- }
-
- let user_dir = match UserDirectory::current_cfg_dir() {
- Some(dir) => dir,
- None => {
- eprintln!("{}", t!("jv.fail.account.no_user_dir"));
- return;
- }
- };
-
- jv_account_as(
- user_dir,
- SetLocalWorkspaceAccountArgs {
- help: false,
- account_name: args.login_member_id,
- },
- )
- .await;
-
- jv_direct(DirectArgs {
- help: false,
- upstream: Some(args.upstream.clone()),
- confirm: true,
- })
- .await;
-
- jv_update(UpdateArgs {
- help: false,
- silent: true,
- })
- .await;
-
- if let Some(local_dir) = current_local_path() {
- let _ = fs::remove_file(local_dir.join(CLIENT_FILE_TODOLIST)).await;
- };
- }
-
- // Completion Helpers
- JustEnoughVcsWorkspaceCommand::GetHistoryIpAddress => {
- get_recent_ip_address()
- .await
- .iter()
- .for_each(|ip| println!("{}", ip));
- }
- JustEnoughVcsWorkspaceCommand::GetWorkspaceDir => {
- if let Some(local_dir) = current_local_path() {
- println!("{}", local_dir.display());
- return;
- };
- exit(1)
- }
- JustEnoughVcsWorkspaceCommand::GetCurrentAccount => {
- let _ = correct_current_dir();
- if let Ok(local_config) = LocalConfig::read().await {
- if local_config.is_host_mode() {
- println!("host/{}", local_config.current_account());
- return;
- } else {
- println!("{}", local_config.current_account());
- return;
- }
- };
- exit(1)
- }
- JustEnoughVcsWorkspaceCommand::GetCurrentUpstream => {
- let _ = correct_current_dir();
- if let Ok(local_config) = LocalConfig::read().await {
- println!("{}", local_config.upstream_addr());
- return;
- };
- exit(1)
- }
- JustEnoughVcsWorkspaceCommand::GetCurrentSheet => {
- let _ = correct_current_dir();
- if let Ok(local_config) = LocalConfig::read().await {
- let sheet_name = local_config.sheet_in_use().clone().unwrap_or_default();
- if sheet_name.len() > 0 {
- println!("{}", sheet_name);
- return;
- }
- };
- exit(1)
- }
-
- // Debug Tools
- JustEnoughVcsWorkspaceCommand::DebugGlob(glob_args) => {
- jv_debug_glob(glob_args).await;
- }
- }
-}
-
-async fn jv_create(args: CreateWorkspaceArgs) {
- let Some(path) = args.path else {
- println!("{}", md(t!("jv.create")));
- return;
- };
-
- if !args.force && path.exists() && !is_directory_empty(&path).await {
- eprintln!("{}", t!("jv.fail.init_create_dir_not_empty").trim());
- return;
- }
-
- match LocalWorkspace::setup_local_workspace(path).await {
- Ok(_) => {
- println!("{}", t!("jv.success.create"));
- }
- Err(e) => {
- eprintln!("{}", t!("jv.fail.create", error = e.to_string()));
- }
- }
-}
-
-async fn jv_init(_args: InitWorkspaceArgs) {
- let path = match current_dir() {
- Ok(path) => path,
- Err(e) => {
- eprintln!("{}", t!("jv.fail.get_current_dir", error = e.to_string()));
- return;
- }
- };
-
- if path.exists() && !is_directory_empty(&path).await {
- eprintln!("{}", t!("jv.fail.init_create_dir_not_empty").trim());
- return;
- }
-
- match LocalWorkspace::setup_local_workspace(path).await {
- Ok(_) => {
- println!("{}", t!("jv.success.init"));
- }
- Err(e) => {
- eprintln!("{}", t!("jv.fail.init", error = e.to_string()));
- }
- }
-}
-
-async fn is_directory_empty(path: &PathBuf) -> bool {
- match fs::read_dir(path).await {
- Ok(mut entries) => entries.next_entry().await.unwrap().is_none(),
- Err(_) => false,
- }
-}
-
-async fn jv_here(args: HereArgs) {
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let Ok(local_cfg) = LocalConfig::read_from(local_dir.join(CLIENT_FILE_WORKSPACE)).await else {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- return;
- };
-
- let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_cfg.current_account(),
- ))
- .await
- else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_info",
- account = &local_cfg.current_account()
- ))
- );
- return;
- };
-
- let Ok(latest_file_data_path) = LatestFileData::data_path(&local_cfg.current_account()) else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_file_data",
- account = &local_cfg.current_account()
- ))
- );
- return;
- };
-
- let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_file_data",
- account = &local_cfg.current_account()
- ))
- );
- return;
- };
-
- // Print path information
- let sheet_name = if let Some(sheet_name) = local_cfg.sheet_in_use() {
- sheet_name.to_string()
- } else {
- "".to_string()
- };
-
- // Read cached sheet
- let Ok(cached_sheet) = CachedSheet::cached_sheet_data(&sheet_name).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.cached_sheet",
- sheet = &sheet_name
- ))
- );
- return;
- };
-
- let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else {
- eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim());
- return;
- };
-
- let Ok(analyzed) = AnalyzeResult::analyze_local_status(&local_workspace).await else {
- eprintln!("{}", md(t!("jv.fail.status.analyze")).trim());
- return;
- };
-
- // Read local sheet
- let Ok(local_sheet) = local_workspace
- .local_sheet(&local_cfg.current_account(), &sheet_name)
- .await
- else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.local_sheet",
- account = &local_cfg.current_account(),
- sheet = &sheet_name
- ))
- );
- return;
- };
-
- let path = match current_dir() {
- Ok(path) => path,
- Err(_) => {
- eprintln!("{}", t!("jv.fail.get_current_dir"));
- return;
- }
- };
-
- let local_dir = match current_local_path() {
- Some(path) => path,
- None => {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- }
- };
-
- let relative_path = match path.strip_prefix(&local_dir) {
- Ok(path) => path.display().to_string(),
- Err(_) => path.display().to_string(),
- };
-
- let mut remote_files = mapping_names_here(&path, &local_dir, &cached_sheet);
-
- let duration_updated = SystemTime::now()
- .duration_since(latest_info.update_instant.unwrap_or(SystemTime::now()))
- .unwrap_or_default();
- let minutes = duration_updated.as_secs() / 60;
-
- // JSON output handling
- if args.json_output {
- let mut json_result = HereJsonResult::default();
- let mut dirs = HashSet::new();
- let mut files = HashSet::new();
-
- // Process local files
- if let Ok(mut entries) = fs::read_dir(&path).await {
- while let Ok(Some(entry)) = entries.next_entry().await {
- if let Ok(file_type) = entry.file_type().await {
- let file_name = entry.file_name().to_string_lossy().to_string();
-
- if file_name == CLIENT_FOLDER_WORKSPACE_ROOT_NAME {
- continue;
- }
-
- let metadata = match entry.metadata().await {
- Ok(meta) => meta,
- Err(_) => continue,
- };
-
- let size = metadata.len() as usize;
- let is_dir = file_type.is_dir();
-
- // Skip if already processed
- if is_dir {
- if dirs.contains(&file_name) {
- continue;
- }
- dirs.insert(file_name.clone());
- } else {
- if files.contains(&file_name) {
- continue;
- }
- files.insert(file_name.clone());
- }
-
- // Get current path
- let current_path = PathBuf::from_str(relative_path.as_ref())
- .unwrap()
- .join(file_name.clone());
-
- let mut current_version = VirtualFileVersion::default();
- let mut modified = false;
- let mut holder = None;
-
- // Get mapping info (only for files)
- if !is_dir {
- if let Some(mapping) = cached_sheet.mapping().get(&current_path) {
- let id = mapping.id.clone();
- if let Some(latest_version) = latest_file_data.file_version(&id) {
- current_version = latest_version.clone();
- }
- if let Some(file_holder) = latest_file_data.file_holder(&id) {
- holder = Some(file_holder.clone());
- }
-
- // Check if file is modified
- modified = analyzed.modified.contains(&current_path);
- }
- }
-
- // Remove from remote files list
- remote_files.remove(&file_name);
-
- // Add to JSON result
- json_result.items.push(HereJsonResultItem {
- mapping: if is_dir {
- "".to_string()
- } else {
- format_path_str(&current_path.display().to_string()).unwrap_or_default()
- },
- name: file_name.clone(),
- current_version,
- size,
- exist: true,
- modified,
- holder: holder.unwrap_or_default(),
- is_dir,
- });
- }
- }
- }
-
- // Process remote files (not existing locally)
- for (name, metadata) in remote_files {
- let current_path = PathBuf::from_str(relative_path.as_ref())
- .unwrap()
- .join(name.clone());
-
- if let Some(metadata) = metadata {
- // It's a file
- // Skip if already processed
- if files.contains(&name) {
- continue;
- }
- files.insert(name.clone());
-
- let holder = latest_file_data.file_holder(&metadata.id).cloned();
- json_result.items.push(HereJsonResultItem {
- mapping: format_path_str(&current_path.display().to_string())
- .unwrap_or_default(),
- name: name.clone(),
- current_version: metadata.version,
- size: 0,
- exist: false,
- modified: false,
- holder: holder.unwrap_or_default(),
- is_dir: false,
- });
- } else {
- // It's a directory
- // Skip if already processed
- let trimmed_name = if name.ends_with('/') {
- name[..name.len() - 1].to_string()
- } else {
- name.clone()
- };
-
- if dirs.contains(&trimmed_name) {
- continue;
- }
- dirs.insert(trimmed_name.clone());
-
- json_result.items.push(HereJsonResultItem {
- mapping: String::default(),
- name: trimmed_name,
- current_version: VirtualFileVersion::default(),
- size: 0,
- exist: false,
- modified: false,
- holder: String::new(),
- is_dir: true,
- });
- }
- }
-
- print_json(json_result, args.pretty);
- return;
- }
-
- let account_str = if local_cfg.is_host_mode() {
- format!("{}/{}", "host".red(), local_cfg.current_account())
- } else {
- local_cfg.current_account()
- };
-
- println!(
- "{}",
- t!(
- "jv.success.here.path_info",
- upstream = local_cfg.upstream_addr().to_string(),
- account = account_str,
- sheet_name = sheet_name.yellow(),
- path = relative_path,
- minutes = minutes
- )
- .trim()
- );
-
- // Print file info
- let mut columns = vec![
- t!("jv.success.here.items.editing"),
- t!("jv.success.here.items.holder"),
- t!("jv.success.here.items.size"),
- t!("jv.success.here.items.version"),
- t!("jv.success.here.items.name"),
- ];
- if args.show_description {
- columns.push(t!("jv.success.here.items.description"));
- }
- let mut table = SimpleTable::new(columns);
-
- let mut dir_count = 0;
- let mut file_count = 0;
- let mut total_size = 0;
-
- // Exists files
- if let Ok(mut entries) = fs::read_dir(&path).await {
- while let Ok(Some(entry)) = entries.next_entry().await {
- if let Ok(file_type) = entry.file_type().await {
- let file_name = entry.file_name().to_string_lossy().to_string();
-
- if file_name == CLIENT_FOLDER_WORKSPACE_ROOT_NAME {
- continue;
- }
-
- let metadata = match entry.metadata().await {
- Ok(meta) => meta,
- Err(_) => continue,
- };
-
- let size = metadata.len();
- let is_dir = file_type.is_dir();
- let mut version = "-".to_string();
- let mut hold = "-".to_string();
- let mut editing = "-".to_string();
- let mut desc = "-".to_string();
-
- if is_dir {
- // Directory
- // Add directory count
- dir_count += 1;
-
- let dir_name = format!("{}/", file_name);
-
- // Remove remote dirs items that already exist locally
- remote_files.remove(&dir_name);
-
- // Add directory item
- let mut line = vec![
- editing.to_string(),
- hold.to_string(),
- "-".to_string(),
- version.to_string(),
- t!(
- "jv.success.here.append_info.name",
- name = dir_name.to_string().cyan()
- )
- .trim()
- .to_string(),
- ];
- if args.show_description {
- line.push(desc);
- }
- table.insert_item(0, line);
- } else {
- // Local File
- // Add file count
- file_count += 1;
-
- // Get current path
- let current_path = PathBuf::from_str(relative_path.as_ref())
- .unwrap()
- .join(file_name.clone());
-
- // Get mapping
- if let Some(mapping) = cached_sheet.mapping().get(&current_path) {
- let mut is_file_held = false;
- let mut is_version_match = false;
-
- // Hold status
- let id = mapping.id.clone();
- if let Some(holder) = latest_file_data.file_holder(&id) {
- if holder == &local_cfg.current_account() {
- hold = t!("jv.success.here.append_info.holder.yourself")
- .trim()
- .green()
- .to_string();
- is_file_held = true;
- } else {
- let holder_text = t!(
- "jv.success.here.append_info.holder.others",
- holder = holder
- )
- .trim()
- .truecolor(128, 128, 128);
- hold = holder_text.to_string();
- }
- }
-
- // Version status && description
- if let Some(latest_version) = latest_file_data.file_version(&id) {
- let local_version = local_sheet.mapping_data(&current_path);
- if let Ok(local_mapping) = local_version {
- let local_version = local_mapping.version_when_updated();
-
- // Append version status
- if latest_version == local_version {
- version = t!(
- "jv.success.here.append_info.version.match",
- version = latest_version
- )
- .trim()
- .to_string();
- is_version_match = true;
- } else {
- version = t!(
- "jv.success.here.append_info.version.unmatch",
- remote_version = local_version,
- )
- .trim()
- .red()
- .to_string();
- }
-
- // Append description
- if args.show_description {
- let content = local_mapping
- .version_desc_when_updated()
- .description
- .clone();
-
- let content = truncate_first_line(content);
-
- desc = t!(
- "jv.success.here.append_info.description",
- creator = local_mapping
- .version_desc_when_updated()
- .creator
- .cyan()
- .to_string(),
- description = content,
- )
- .trim()
- .to_string();
- }
- }
- }
-
- // Editing status
- let modified = analyzed.modified.contains(&current_path);
- if !is_file_held || !is_version_match {
- if modified {
- editing = t!(
- "jv.success.here.append_info.editing.cant_edit_but_modified"
- )
- .trim()
- .red()
- .to_string();
- } else {
- editing = t!("jv.success.here.append_info.editing.cant_edit")
- .trim()
- .truecolor(128, 128, 128)
- .to_string();
- }
- } else {
- if modified {
- editing = t!("jv.success.here.append_info.editing.modified")
- .trim()
- .cyan()
- .to_string();
- } else {
- editing = t!("jv.success.here.append_info.editing.can_edit")
- .trim()
- .green()
- .to_string();
- }
- }
- }
-
- // Remove remote file items that already exist locally
- remote_files.remove(&file_name);
-
- // Add Table Item
- let mut line = vec![
- editing.to_string(),
- hold.to_string(),
- t!(
- "jv.success.here.append_info.size",
- size = size_str(size as usize)
- )
- .trim()
- .yellow()
- .to_string(),
- version.to_string(),
- t!("jv.success.here.append_info.name", name = file_name)
- .trim()
- .to_string(),
- ];
-
- if args.show_description {
- line.push(desc);
- }
-
- table.push_item(line);
- }
-
- // Total Size
- total_size += size;
- }
- }
- }
-
- // Remote Files
- for mapping in remote_files {
- if let Some(metadata) = mapping.1 {
- let mut hold = "-".to_string();
- if let Some(holder) = latest_file_data.file_holder(&metadata.id) {
- if holder == &local_cfg.current_account() {
- hold = t!("jv.success.here.append_info.holder.yourself")
- .trim()
- .green()
- .to_string();
- } else {
- let holder_text =
- t!("jv.success.here.append_info.holder.others", holder = holder)
- .trim()
- .truecolor(128, 128, 128);
- hold = holder_text.to_string();
- }
- }
-
- // File
- let mut line = vec![
- t!("jv.success.here.append_info.editing.not_local")
- .trim()
- .truecolor(128, 128, 128)
- .to_string(),
- hold.to_string(),
- "-".to_string(),
- metadata.version,
- t!("jv.success.here.append_info.name", name = mapping.0)
- .trim()
- .truecolor(128, 128, 128)
- .to_string(),
- ];
-
- if args.show_description {
- line.push("-".to_string());
- }
-
- table.push_item(line);
- } else {
- // Directory
- let mut line = vec![
- "-".to_string(),
- "-".to_string(),
- "-".to_string(),
- "-".to_string(),
- t!("jv.success.here.append_info.name", name = mapping.0)
- .trim()
- .truecolor(128, 128, 128)
- .to_string(),
- ];
-
- if args.show_description {
- line.push("-".to_string());
- }
-
- table.push_item(line);
- }
- }
-
- println!("{}", table);
-
- // Print directory info
- println!(
- "{}",
- t!(
- "jv.success.here.count_info",
- dir_count = dir_count,
- file_count = file_count,
- size = size_str(total_size as usize)
- )
- .trim()
- );
-}
-
-async fn jv_status(args: StatusArgs) {
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim());
- return;
- };
-
- let Ok(local_cfg) = LocalConfig::read_from(local_dir.join(CLIENT_FILE_WORKSPACE)).await else {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- return;
- };
-
- let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_cfg.current_account(),
- ))
- .await
- else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_info",
- account = &local_cfg.current_account()
- ))
- );
- return;
- };
-
- let account = local_cfg.current_account();
-
- let Ok(latest_file_data_path) = LatestFileData::data_path(&account) else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_file_data",
- account = &account
- ))
- );
- return;
- };
-
- let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_file_data",
- account = &account
- ))
- );
- return;
- };
-
- let Some(sheet_name) = local_cfg.sheet_in_use().clone() else {
- eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim());
- return;
- };
-
- let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else {
- eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim());
- return;
- };
-
- let Ok(local_sheet) = local_workspace.local_sheet(&account, &sheet_name).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.local_sheet",
- account = &account,
- sheet = &sheet_name
- ))
- );
- return;
- };
-
- let in_ref_sheet = latest_info.reference_sheets.contains(&sheet_name);
- let is_host_mode = local_cfg.is_host_mode();
-
- let Ok(analyzed) = AnalyzeResult::analyze_local_status(&local_workspace).await else {
- eprintln!("{}", md(t!("jv.fail.status.analyze")).trim());
- return;
- };
-
- let mut created_items: Vec<String>;
- let mut erased_items: Vec<String>;
- let mut lost_items: Vec<String>;
- let mut moved_items: Vec<String>;
- let mut modified_items: Vec<String>;
-
- if args.json_output {
- let mut created: Vec<PathBuf> = analyzed.created.iter().cloned().collect();
- let mut lost: Vec<PathBuf> = analyzed.lost.iter().cloned().collect();
- let mut erased: Vec<PathBuf> = analyzed.erased.iter().cloned().collect();
- let mut moved: Vec<MovedItem> = analyzed
- .moved
- .iter()
- .map(|(_, (from, to))| MovedItem {
- from: from.clone(),
- to: to.clone(),
- })
- .collect();
- let mut modified: Vec<ModifiedItem> = analyzed
- .modified
- .iter()
- .cloned()
- .map(|path| {
- let holder_match = {
- if let Ok(mapping) = local_sheet.mapping_data(&path) {
- let vfid = mapping.mapping_vfid();
- match latest_file_data.file_holder(vfid) {
- Some(holder) => holder == &account,
- None => false,
- }
- } else {
- false
- }
- };
-
- let base_version_match = {
- if let Ok(mapping) = local_sheet.mapping_data(&path) {
- let vfid = mapping.mapping_vfid();
- let ver = mapping.version_when_updated();
- if let Some(latest_version) = latest_file_data.file_version(&vfid) {
- ver == latest_version
- } else {
- true
- }
- } else {
- true
- }
- };
-
- let modification_type = if !holder_match {
- ModifiedType::ModifiedButNotHeld
- } else if !base_version_match {
- ModifiedType::ModifiedButBaseVersionMismatch
- } else {
- ModifiedType::Modified
- };
-
- ModifiedItem {
- path,
- modification_type,
- }
- })
- .collect();
-
- // Sort
- created.sort();
- lost.sort();
- erased.sort();
- moved.sort_by(|a, b| a.from.cmp(&b.from).then(a.to.cmp(&b.to)));
- modified.sort_by(|a, b| a.path.cmp(&b.path));
-
- let json_result = AnalyzerJsonResult {
- created,
- lost,
- erased,
- moved,
- modified,
- };
-
- print_json(json_result, args.pretty);
- return;
- } else {
- // Format created items
- created_items = analyzed
- .created
- .iter()
- .map(|path| {
- t!(
- "jv.success.status.created_item",
- path = path.display().to_string()
- )
- .trim()
- .green()
- .to_string()
- })
- .collect();
-
- // Format erased items
- erased_items = analyzed
- .erased
- .iter()
- .map(|path| {
- t!(
- "jv.success.status.erased_item",
- path = path.display().to_string()
- )
- .trim()
- .magenta()
- .to_string()
- })
- .collect();
-
- // Format lost items
- lost_items = analyzed
- .lost
- .iter()
- .map(|path| {
- t!(
- "jv.success.status.lost_item",
- path = path.display().to_string()
- )
- .trim()
- .red()
- .to_string()
- })
- .collect();
-
- // Format moved items
- moved_items = analyzed
- .moved
- .iter()
- .map(|(_, (from, to))| {
- t!(
- "jv.success.status.moved_item",
- from = from.display(),
- to = to.display()
- )
- .trim()
- .yellow()
- .to_string()
- })
- .collect();
-
- // Format modified items
- modified_items = analyzed
- .modified
- .iter()
- .map(|path| {
- let holder_match = {
- if let Ok(mapping) = local_sheet.mapping_data(path) {
- let vfid = mapping.mapping_vfid();
- match latest_file_data.file_holder(vfid) {
- Some(holder) => holder == &account,
- None => false,
- }
- } else {
- false
- }
- };
-
- let base_version_match = {
- if let Ok(mapping) = local_sheet.mapping_data(path) {
- let vfid = mapping.mapping_vfid();
- let ver = mapping.version_when_updated();
- if let Some(latest_version) = latest_file_data.file_version(&vfid) {
- ver == latest_version
- } else {
- true
- }
- } else {
- true
- }
- };
-
- // Holder dismatch
- if !holder_match {
- return t!(
- "jv.success.status.invalid_modified_item",
- path = path.display().to_string(),
- reason = t!("jv.success.status.invalid_modified_reasons.not_holder")
- )
- .trim()
- .red()
- .to_string();
- }
-
- // Base version mismatch
- if !base_version_match {
- return t!(
- "jv.success.status.invalid_modified_item",
- path = path.display().to_string(),
- reason =
- t!("jv.success.status.invalid_modified_reasons.base_version_mismatch")
- )
- .trim()
- .red()
- .to_string();
- }
-
- t!(
- "jv.success.status.modified_item",
- path = path.display().to_string()
- )
- .trim()
- .cyan()
- .to_string()
- })
- .collect();
- }
-
- let has_struct_changes = !created_items.is_empty()
- || !lost_items.is_empty()
- || !erased_items.is_empty()
- || !moved_items.is_empty();
- let has_file_modifications = !modified_items.is_empty();
-
- if has_struct_changes {
- sort_paths(&mut created_items);
- sort_paths(&mut lost_items);
- sort_paths(&mut erased_items);
- sort_paths(&mut moved_items);
- }
- if has_file_modifications {
- sort_paths(&mut modified_items);
- }
-
- // Calculate duration since last update
- let update_instant = latest_info.update_instant.unwrap_or(SystemTime::now());
- let duration = SystemTime::now()
- .duration_since(update_instant)
- .unwrap_or_default();
- let h = duration.as_secs() / 3600;
- let m = (duration.as_secs() % 3600) / 60;
- let s = duration.as_secs() % 60;
-
- if has_struct_changes {
- println!(
- "{}",
- md(t!(
- "jv.success.status.struct_changes_display",
- sheet_name = sheet_name,
- moved_items = if moved_items.is_empty() {
- "".to_string()
- } else {
- moved_items.join("\n") + "\n"
- },
- lost_items = if lost_items.is_empty() {
- "".to_string()
- } else {
- lost_items.join("\n") + "\n"
- },
- erased_items = if erased_items.is_empty() {
- "".to_string()
- } else {
- erased_items.join("\n") + "\n"
- },
- created_items = if created_items.is_empty() {
- "".to_string()
- } else {
- created_items.join("\n") + "\n"
- },
- h = h,
- m = m,
- s = s
- ))
- .trim()
- );
- } else if has_file_modifications {
- println!(
- "{}",
- md(t!(
- "jv.success.status.content_modifies_display",
- sheet_name = sheet_name,
- modified_items = if modified_items.is_empty() {
- "".to_string()
- } else {
- modified_items.join("\n")
- },
- h = h,
- m = m,
- s = s
- ))
- .trim()
- );
- } else {
- if in_ref_sheet {
- println!(
- "{}",
- md(t!(
- "jv.success.status.no_changes_in_reference_sheet",
- sheet_name = sheet_name,
- h = h,
- m = m,
- s = s
- ))
- );
- } else {
- println!(
- "{}",
- md(t!(
- "jv.success.status.no_changes",
- sheet_name = sheet_name,
- h = h,
- m = m,
- s = s
- ))
- );
- }
- }
-
- if in_ref_sheet && !is_host_mode {
- println!(
- "\n{}",
- md(t!("jv.success.status.hint_in_reference_sheet")).yellow()
- );
- }
- if is_host_mode {
- println!("\n{}", md(t!("jv.success.status.hint_as_host")));
- }
-}
-
-async fn jv_info(args: InfoArgs) {
- let local_dir = match current_local_path() {
- Some(dir) => dir,
- None => {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- }
- };
-
- let query_file_paths = if let Some(pattern) = args.file_pattern.clone() {
- let files = glob(pattern, &local_dir).await;
- files
- .iter()
- .filter_map(|f| PathBuf::from_str(f.0).ok())
- .collect::<Vec<_>>()
- } else {
- println!("{}", md(t!("jv.info")));
- return;
- };
-
- let _ = correct_current_dir();
-
- let Ok(local_cfg) = LocalConfig::read().await else {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- return;
- };
-
- let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_cfg.current_account(),
- ))
- .await
- else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_info",
- account = &local_cfg.current_account()
- ))
- );
- return;
- };
-
- let account = local_cfg.current_account();
-
- let Ok(latest_file_data_path) = LatestFileData::data_path(&account) else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_file_data",
- account = &account
- ))
- );
- return;
- };
-
- // Get latest file data
- let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_file_data",
- account = &account
- ))
- );
- return;
- };
-
- let Some(sheet_name) = local_cfg.sheet_in_use().clone() else {
- eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim());
- return;
- };
-
- let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else {
- eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim());
- return;
- };
-
- let Ok(local_sheet) = local_workspace.local_sheet(&account, &sheet_name).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.local_sheet",
- account = &account,
- sheet = &sheet_name
- ))
- );
- return;
- };
-
- if query_file_paths.len() < 1 {
- return;
- }
- // File to query
- let query_file_path = query_file_paths[0].to_path_buf();
- let Ok(mapping) = local_sheet.mapping_data(&query_file_path) else {
- return;
- };
- let vfid = mapping.mapping_vfid();
-
- let query_file_path_string =
- format_path_str(query_file_path.display().to_string()).unwrap_or_default();
-
- // JSON output handling
- if args.json_output {
- let mut json_result = InfoJsonResult::default();
- json_result.mapping = query_file_path_string.clone();
-
- // Get reference sheet path
- json_result.in_ref = if let Some(path) = latest_info.ref_sheet_vfs_mapping.get(vfid) {
- path.display().to_string()
- } else {
- vfid.clone()
- };
-
- json_result.vfid = vfid.clone();
-
- // Get file version in reference sheet
- let version_in_ref = if let Some(mapping) = latest_info
- .ref_sheet_content
- .mapping()
- .get(&query_file_path)
- {
- mapping.version.clone()
- } else {
- "".to_string()
- };
-
- // Get current file version
- let version_current = latest_file_data
- .file_version(vfid)
- .cloned()
- .unwrap_or_else(|| "".to_string());
-
- // Check if file is being edited based on latest version (regardless of hold status)
- let modified_correctly = if let Ok(mapping) = local_sheet.mapping_data(&query_file_path) {
- // If base editing version is correct
- if mapping.version_when_updated() == &version_current {
- mapping.last_modifiy_check_result() // Return detection result
- } else {
- false
- }
- } else {
- false
- };
-
- // Build history list
- if let Some(histories) = latest_file_data.file_histories(vfid) {
- for (version, description) in histories {
- json_result.histories.push(InfoHistory {
- version: version.clone(),
- version_creator: description.creator.clone(),
- version_description: description.description.clone(),
- is_current_version: version == &version_current,
- is_ref_version: version == &version_in_ref,
- });
- }
- }
-
- // Add current modification if exists
- if modified_correctly {
- json_result.histories.insert(
- 0,
- InfoHistory {
- version: "CURRENT".to_string(),
- version_creator: local_workspace
- .config()
- .lock()
- .await
- .current_account()
- .clone(),
- version_description: t!("jv.success.info.oneline.description_current")
- .to_string(),
- is_current_version: true,
- is_ref_version: false,
- },
- );
- }
-
- print_json(json_result, args.pretty);
- return;
- }
-
- // Render initial location
- {
- println!("{}", query_file_path_string);
- }
-
- // Render reference sheet location, use ID if not found
- {
- let path_in_ref = if let Some(path) = latest_info.ref_sheet_vfs_mapping.get(vfid) {
- path.display().to_string()
- } else {
- vfid.clone()
- };
-
- // Offset string
- let offset_string = " ".repeat(display_width(
- if let Some(last_slash) = query_file_path_string.rfind('/') {
- &query_file_path_string[..last_slash]
- } else {
- ""
- },
- ));
-
- println!(
- "{}{}{}",
- offset_string,
- "\\_ ".truecolor(128, 128, 128),
- path_in_ref.cyan()
- );
- }
-
- // Render complete file history
- {
- if let Some(histories) = latest_file_data.file_histories(vfid) {
- // Get file version in reference sheet
- let version_in_ref = if let Some(mapping) = latest_info
- .ref_sheet_content
- .mapping()
- .get(&query_file_path)
- {
- mapping.version.clone()
- } else {
- "".to_string()
- };
-
- // Get current file version
- let version_current = latest_file_data
- .file_version(vfid)
- .cloned()
- .unwrap_or_else(|| "".to_string());
-
- // Check if file is being edited based on latest version (regardless of hold status)
- let modified_correctly = if let Ok(mapping) = local_sheet.mapping_data(&query_file_path)
- {
- // If base editing version is correct
- if mapping.version_when_updated() == &version_current {
- mapping.last_modifiy_check_result() // Return detection result
- } else {
- false
- }
- } else {
- false
- };
-
- // Text
- let (prefix_str, version_str, creator_str, description_str) = (
- t!("jv.success.info.oneline.table_headers.prefix"),
- t!("jv.success.info.oneline.table_headers.version"),
- t!("jv.success.info.oneline.table_headers.creator"),
- t!("jv.success.info.oneline.table_headers.description"),
- );
-
- // Single-line output
- if !args.full {
- // Create table
- let mut table =
- SimpleTable::new(vec![prefix_str, version_str, creator_str, description_str]);
-
- // Append data
- for (version, description) in histories {
- // If it's reference version, render "@"
- // Current version, render "\_"
- // Other versions, render "|"
- let prefix = if version == &version_in_ref {
- "@".cyan().to_string()
- } else if version == &version_current {
- "|->".yellow().to_string()
- } else {
- "|".truecolor(128, 128, 128).to_string()
- };
-
- table.insert_item(
- 0,
- vec![
- prefix,
- version.to_string(),
- format!("@{}: ", &description.creator.cyan()),
- truncate_first_line(description.description.to_string()),
- ],
- );
- }
-
- // If file has new version, append
- if modified_correctly {
- table.insert_item(
- 0,
- vec![
- "+".green().to_string(),
- "CURRENT".green().to_string(),
- format!(
- "@{}: ",
- local_workspace
- .config()
- .lock()
- .await
- .current_account()
- .cyan()
- ),
- format!(
- "{}",
- t!("jv.success.info.oneline.description_current").green()
- ),
- ],
- );
- }
-
- // Render table
- let table_str = table.to_string();
- if table_str.lines().count() > 1 {
- println!();
- }
- for line in table_str.lines().skip(1) {
- println!("{}", line);
- }
- } else {
- // Multi-line output
- if histories.len() > 0 {
- println!();
- }
- for (version, description) in histories {
- println!("{}: {}", version_str, version);
- println!("{}: {}", creator_str, description.creator.cyan());
- println!("{}", description.description);
- if version != &histories.last().unwrap().0 {
- println!("{}", "-".repeat(45));
- }
- }
- }
- }
- }
-}
-
-async fn jv_sheet_list(args: SheetListArgs) {
- let _ = correct_current_dir();
-
- let Some(local_dir) = current_local_path() else {
- if !args.raw {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- }
- return;
- };
-
- let Ok(local_cfg) = LocalConfig::read().await else {
- if !args.raw {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- }
- return;
- };
-
- let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_cfg.current_account(),
- ))
- .await
- else {
- if !args.raw {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_info",
- account = &local_cfg.current_account()
- ))
- );
- }
- return;
- };
-
- let mut your_sheet_counts = 0;
- let mut other_sheet_counts = 0;
-
- // JSON output handling
- if args.json_output {
- let mut json_result = SheetListJsonResult::default();
-
- // Populate reference sheets
- for sheet_name in &latest_info.reference_sheets {
- json_result.reference_sheets.push(SheetItem {
- name: sheet_name.clone(),
- holder: local_cfg.current_account().clone(),
- });
- }
-
- // Populate other sheets
- for sheet in &latest_info.invisible_sheets {
- json_result.other_sheets.push(SheetItem {
- name: sheet.sheet_name.clone(),
- holder: sheet.holder_name.clone().unwrap_or_default(),
- });
- }
-
- // Populate my sheets (excluding reference sheets)
- for sheet_name in &latest_info.visible_sheets {
- if !latest_info.reference_sheets.contains(sheet_name) {
- json_result.my_sheets.push(SheetItem {
- name: sheet_name.clone(),
- holder: local_cfg.current_account().clone(),
- });
- }
- }
-
- print_json(json_result, args.pretty);
- return;
- }
-
- if args.raw {
- // Print your sheets
- if !args.others && !args.all || !args.others {
- latest_info
- .visible_sheets
- .iter()
- .for_each(|s| println!("{}", s));
- }
- // Print other sheets
- if args.others || args.all {
- latest_info
- .invisible_sheets
- .iter()
- .for_each(|s| println!("{}", s.sheet_name));
- }
- } else {
- // Print your sheets
- if !args.others && !args.all || !args.others {
- println!("{}", md(t!("jv.success.sheet.list.your_sheet")));
- let in_use = local_cfg.sheet_in_use();
- for sheet in latest_info.visible_sheets {
- let is_ref_sheet = latest_info.reference_sheets.contains(&sheet);
- let display_name = if is_ref_sheet {
- format!(
- "{} {}",
- sheet,
- md(t!("jv.success.sheet.list.reference_sheet_suffix"))
- .truecolor(128, 128, 128)
- )
- } else {
- sheet.clone()
- };
-
- if let Some(in_use) = in_use
- && in_use == &sheet
- {
- println!(
- "{}",
- md(t!(
- "jv.success.sheet.list.your_sheet_item_use",
- number = your_sheet_counts + 1,
- name = display_name.cyan()
- ))
- );
- } else {
- println!(
- "{}",
- md(t!(
- "jv.success.sheet.list.your_sheet_item",
- number = your_sheet_counts + 1,
- name = display_name
- ))
- );
- }
- your_sheet_counts += 1;
- }
- }
-
- // Print other sheets
- if args.others || args.all {
- if args.all {
- println!();
- }
- println!("{}", md(t!("jv.success.sheet.list.other_sheet")));
- for sheet in latest_info.invisible_sheets {
- if let Some(holder) = sheet.holder_name {
- println!(
- "{}",
- md(t!(
- "jv.success.sheet.list.other_sheet_item",
- number = other_sheet_counts + 1,
- name = sheet.sheet_name,
- holder = holder
- ))
- );
- } else {
- println!(
- "{}",
- md(t!(
- "jv.success.sheet.list.other_sheet_item_no_holder",
- number = other_sheet_counts + 1,
- name = sheet.sheet_name
- ))
- );
- }
- other_sheet_counts += 1;
- }
- }
-
- // If not use any sheets, print tips
- if local_cfg.sheet_in_use().is_none() {
- println!();
- if your_sheet_counts > 0 {
- println!("{}", md(t!("jv.success.sheet.list.tip_has_sheet")));
- } else {
- println!("{}", md(t!("jv.success.sheet.list.tip_no_sheet")));
- }
- }
- }
-}
-
-async fn jv_sheet_use(args: SheetUseArgs) {
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let current_dir = current_dir().unwrap();
-
- if local_dir != current_dir {
- eprintln!("{}", t!("jv.fail.not_root_dir").trim());
- return;
- }
-
- let Ok(mut local_cfg) = LocalConfig::read().await else {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- return;
- };
-
- match local_cfg.use_sheet(args.sheet_name.clone()).await {
- Ok(_) => {
- match LocalConfig::write(&local_cfg).await {
- Ok(_) => (),
- Err(e) => {
- eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string())));
- return;
- }
- };
-
- // After successfully switching sheets, status should be automatically prompted
- jv_status(StatusArgs {
- help: false,
- json_output: false,
- pretty: false,
- })
- .await;
- }
- Err(e) => match e.kind() {
- std::io::ErrorKind::AlreadyExists => {} // Already In Use
- std::io::ErrorKind::NotFound => {
- eprintln!(
- "{}",
- md(t!("jv.fail.use.sheet_not_exists", name = args.sheet_name))
- );
- return;
- }
- std::io::ErrorKind::DirectoryNotEmpty => {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.use.directory_not_empty",
- name = args.sheet_name
- ))
- );
- return;
- }
- _ => {
- handle_err(e.into());
- }
- },
- }
-}
-
-async fn jv_sheet_exit(_args: SheetExitArgs) -> Result<(), ()> {
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return Err(());
- };
-
- let current_dir = current_dir().unwrap();
-
- if local_dir != current_dir {
- eprintln!("{}", t!("jv.fail.not_root_dir").trim());
- return Err(());
- }
-
- let Ok(mut local_cfg) = LocalConfig::read().await else {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- return Err(());
- };
-
- match local_cfg.exit_sheet().await {
- Ok(_) => {
- match LocalConfig::write(&local_cfg).await {
- Ok(_) => (),
- Err(e) => {
- eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string())));
- return Err(());
- }
- };
- return Ok(());
- }
- Err(e) => {
- handle_err(e.into());
- return Err(());
- }
- }
-}
-
-async fn jv_sheet_make(args: SheetMakeArgs) {
- let sheet_name = snake_case!(args.sheet_name);
-
- let local_config = match precheck().await {
- Some(config) => config,
- None => return,
- };
-
- let (pool, ctx, _output) = match build_pool_and_ctx(&local_config).await {
- Some(result) => result,
- None => return,
- };
-
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let latest_info = match LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_config.current_account(),
- ))
- .await
- {
- Ok(info) => info,
- Err(_) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_info",
- account = &local_config.current_account()
- ))
- );
- return;
- }
- };
-
- if latest_info
- .invisible_sheets
- .iter()
- .any(|sheet| sheet.sheet_name == sheet_name && sheet.holder_name.is_none())
- {
- println!(
- "{}",
- md(t!("jv.confirm.sheet.make.restore", sheet_name = sheet_name)).yellow()
- );
- if !confirm_hint(t!("common.confirm")).await {
- return;
- }
- }
-
- match proc_make_sheet_action(&pool, ctx, sheet_name.clone()).await {
- Ok(r) => match r {
- MakeSheetActionResult::Success => {
- println!(
- "{}",
- md(t!("jv.result.sheet.make.success", name = sheet_name))
- )
- }
- MakeSheetActionResult::SuccessRestore => {
- println!(
- "{}",
- md(t!(
- "jv.result.sheet.make.success_restore",
- name = sheet_name
- ))
- )
- }
- MakeSheetActionResult::AuthorizeFailed(e) => {
- eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e)))
- }
- MakeSheetActionResult::SheetAlreadyExists => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.sheet.make.sheet_already_exists",
- name = sheet_name
- ))
- );
- }
- MakeSheetActionResult::SheetCreationFailed(e) => {
- eprintln!(
- "{}",
- md(t!("jv.result.sheet.make.sheet_creation_failed", err = e))
- )
- }
- MakeSheetActionResult::Unknown => todo!(),
- },
- Err(e) => handle_err(e),
- }
-}
-
-async fn jv_sheet_drop(args: SheetDropArgs) {
- let sheet_name = snake_case!(args.sheet_name);
-
- if !args.confirm {
- println!(
- "{}",
- t!("jv.confirm.sheet.drop", sheet_name = sheet_name)
- .trim()
- .yellow()
- );
- confirm_hint_or(t!("common.confirm"), || exit(1)).await;
- }
-
- let local_config = match precheck().await {
- Some(config) => config,
- None => return,
- };
-
- let (pool, ctx, _output) = match build_pool_and_ctx(&local_config).await {
- Some(result) => result,
- None => return,
- };
-
- match proc_drop_sheet_action(&pool, ctx, sheet_name.clone()).await {
- Ok(r) => match r {
- DropSheetActionResult::Success => {
- println!(
- "{}",
- md(t!("jv.result.sheet.drop.success", name = sheet_name))
- )
- }
- DropSheetActionResult::SheetInUse => {
- eprintln!(
- "{}",
- md(t!("jv.result.sheet.drop.sheet_in_use", name = sheet_name))
- )
- }
- DropSheetActionResult::AuthorizeFailed(e) => {
- eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e)))
- }
- DropSheetActionResult::SheetNotExists => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.sheet.drop.sheet_not_exists",
- name = sheet_name
- ))
- )
- }
- DropSheetActionResult::SheetDropFailed(e) => {
- eprintln!(
- "{}",
- md(t!("jv.result.sheet.drop.sheet_drop_failed", err = e))
- )
- }
- DropSheetActionResult::NoHolder => {
- eprintln!(
- "{}",
- md(t!("jv.result.sheet.drop.no_holder", name = sheet_name))
- )
- }
- DropSheetActionResult::NotOwner => {
- eprintln!(
- "{}",
- md(t!("jv.result.sheet.drop.not_owner", name = sheet_name))
- )
- }
- _ => {}
- },
- Err(e) => handle_err(e),
- }
-}
-
-async fn jv_sheet_align(args: SheetAlignArgs) {
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim());
- return;
- };
-
- let local_cfg = match precheck().await {
- Some(config) => config,
- None => {
- return;
- }
- };
-
- let account = local_cfg.current_account();
-
- let Some(sheet_name) = local_cfg.sheet_in_use().clone() else {
- eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim());
- return;
- };
-
- let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else {
- eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim());
- return;
- };
-
- let Ok(mut local_sheet) = local_workspace.local_sheet(&account, &sheet_name).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.local_sheet",
- account = &account,
- sheet = &sheet_name
- ))
- );
- return;
- };
-
- let Ok(analyzed) = AnalyzeResult::analyze_local_status(&local_workspace).await else {
- eprintln!("{}", md(t!("jv.fail.status.analyze")).trim());
- return;
- };
-
- let align_tasks = AlignTasks::from_analyze_result(analyzed);
-
- // No task input, list all tasks needs align
- let Some(task) = args.task else {
- // Raw output
- if args.raw {
- if args.list_all {
- align_tasks.created.iter().for_each(|i| println!("{}", i.0));
- align_tasks.moved.iter().for_each(|i| println!("{}", i.0));
- align_tasks.lost.iter().for_each(|i| println!("{}", i.0));
- align_tasks.erased.iter().for_each(|i| println!("{}", i.0));
- return;
- }
- if args.list_created {
- align_tasks.created.iter().for_each(|i| println!("{}", i.0));
- return;
- }
- if args.list_unsolved {
- align_tasks.moved.iter().for_each(|i| println!("{}", i.0));
- align_tasks.lost.iter().for_each(|i| println!("{}", i.0));
- align_tasks.erased.iter().for_each(|i| println!("{}", i.0));
- return;
- }
- return;
- }
-
- // Json Output
- if args.json_output {
- let mut json_result = AlignJsonResult::default();
-
- for (name, local_path) in &align_tasks.created {
- json_result.align_tasks.insert(
- name.clone(),
- AlignTaskMapping {
- local_mapping: local_path.clone(),
- remote_mapping: PathBuf::new(),
- },
- );
- }
- for (name, (remote_path, local_path)) in &align_tasks.moved {
- json_result.align_tasks.insert(
- name.clone(),
- AlignTaskMapping {
- local_mapping: local_path.clone(),
- remote_mapping: remote_path.clone(),
- },
- );
- }
- for (name, local_path) in &align_tasks.lost {
- json_result.align_tasks.insert(
- name.clone(),
- AlignTaskMapping {
- local_mapping: local_path.clone(),
- remote_mapping: PathBuf::new(),
- },
- );
- }
- for (name, local_path) in &align_tasks.erased {
- json_result.align_tasks.insert(
- name.clone(),
- AlignTaskMapping {
- local_mapping: local_path.clone(),
- remote_mapping: PathBuf::new(),
- },
- );
- }
-
- print_json(json_result, args.pretty);
- return;
- }
-
- let mut table = SimpleTable::new(vec![
- t!("jv.success.sheet.align.task_name").to_string(),
- t!("jv.success.sheet.align.local_path").to_string(),
- if !align_tasks.moved.is_empty() {
- t!("jv.success.sheet.align.remote_path").to_string()
- } else {
- "".to_string()
- },
- ]);
-
- let mut need_align = 0;
-
- if !align_tasks.created.is_empty() {
- align_tasks.created.iter().for_each(|(n, p)| {
- table.push_item(vec![
- format!("+ {}", n).green().to_string(),
- p.display().to_string().green().to_string(),
- "".to_string(),
- ]);
- });
- }
-
- if !align_tasks.lost.is_empty() {
- align_tasks.lost.iter().for_each(|(n, p)| {
- table.push_item(vec![
- format!("- {}", n).red().to_string(),
- p.display().to_string().red().to_string(),
- "".to_string(),
- ]);
- });
- need_align += 1;
- }
-
- if !align_tasks.erased.is_empty() {
- align_tasks.erased.iter().for_each(|(n, p)| {
- table.push_item(vec![
- format!("& {}", n).magenta().to_string(),
- p.display().to_string().magenta().to_string(),
- "".to_string(),
- ]);
- });
- need_align += 1;
- }
-
- if !align_tasks.moved.is_empty() {
- align_tasks.moved.iter().for_each(|(n, (rp, lp))| {
- table.push_item(vec![
- format!("> {}", n).yellow().to_string(),
- lp.display().to_string().yellow().to_string(),
- rp.display().to_string(),
- ]);
- });
- need_align += 1;
- }
-
- if need_align > 0 {
- println!(
- "{}",
- md(t!("jv.success.sheet.align.list", tasks = table.to_string()))
- );
-
- // Suggestion1: Confirm Erased
- if align_tasks.erased.len() > 0 {
- println!(
- "\n{}",
- md(t!(
- "jv.success.sheet.align.suggestion_1",
- example_erased = align_tasks.erased[0].0
- ))
- )
- } else
- // Suggestion2: Confirm Lost
- if align_tasks.lost.len() > 0 {
- println!(
- "\n{}",
- md(t!(
- "jv.success.sheet.align.suggestion_2",
- example_lost = align_tasks.lost[0].0
- ))
- )
- } else
- // Suggestion3: Confirm Moved
- if align_tasks.moved.len() > 0 {
- println!(
- "\n{}",
- md(t!(
- "jv.success.sheet.align.suggestion_3",
- example_moved = align_tasks.moved[0].0
- ))
- )
- }
- } else {
- println!("{}", md(t!("jv.success.sheet.align.no_changes").trim()));
- }
-
- return;
- };
-
- let Some(to) = args.to else {
- eprintln!("{}", md(t!("jv.fail.sheet.align.no_direction")));
- return;
- };
-
- // Move: alignment mode
- if task.starts_with("moved") {
- let align_to = match to.trim().to_lowercase().as_str() {
- "remote" => "remote",
- "local" => "local",
- "break" => "break",
- _ => {
- eprintln!("{}", md(t!("jv.fail.sheet.align.unknown_moved_direction")));
- return;
- }
- };
-
- // Build remote move operations
- let operations: HashMap<FromRelativePathBuf, OperationArgument> = if task == "moved" {
- // Align all moved items
- align_tasks
- .moved
- .iter()
- .map(|(_, (remote_path, local_path))| {
- (
- remote_path.clone(),
- (EditMappingOperations::Move, Some(local_path.clone())),
- )
- })
- .collect()
- } else {
- // Align specific moved item
- align_tasks
- .moved
- .iter()
- .filter(|(key, _)| key == &task)
- .map(|(_, (remote_path, local_path))| {
- (
- remote_path.clone(),
- (EditMappingOperations::Move, Some(local_path.clone())),
- )
- })
- .collect()
- };
-
- if align_to == "local" {
- // Align to local
- // Network move mapping
- let (pool, ctx, _output) = match build_pool_and_ctx(&local_cfg).await {
- Some(result) => result,
- None => return,
- };
-
- // Process mapping edit, errors are handled internally
- let _ = proc_mapping_edit(&pool, ctx, EditMappingActionArguments { operations }).await;
- } else if align_to == "remote" {
- // Align to remote
- // Offline move files
- for (remote_path, (_, local_path)) in operations {
- let local_path = local_path.unwrap();
- let from = local_dir.join(&local_path);
- let to = local_dir.join(&remote_path);
-
- if to.exists() {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.sheet.align.target_exists",
- local = local_path.display(),
- remote = remote_path.display()
- ))
- );
- return;
- }
-
- if let Some(parent) = to.parent() {
- if let Err(err) = fs::create_dir_all(parent).await {
- eprintln!("{}", md(t!("jv.fail.sheet.align.move_failed", err = err)));
- continue;
- }
- } else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.sheet.align.move_failed",
- err = "no parent directory"
- ))
- );
- continue;
- }
- if let Err(err) = fs::rename(from, to).await {
- eprintln!("{}", md(t!("jv.fail.sheet.align.move_failed", err = err)));
- }
- }
- } else if align_to == "break" {
- for (remote_path, (_, _)) in operations {
- let Ok(mapping) = local_sheet.mapping_data_mut(&remote_path) else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.sheet.align.mapping_not_found",
- mapping = remote_path.display()
- ))
- );
- return;
- };
-
- // Restore the latest detected hash to the original hash,
- // making the analyzer unable to correctly match
- //
- // That is to say,
- // if the file's hash has remained completely unchanged from the beginning to the end,
- // then break is also ineffective.
- mapping.set_last_modifiy_check_hash(Some(mapping.hash_when_updated().clone()));
- }
-
- // Save sheet
- match local_sheet.write().await {
- Ok(_) => {}
- Err(e) => {
- eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string())));
- return;
- }
- };
- }
- }
- // Lost: match or confirm mode
- else if task.starts_with("lost") {
- let selected_lost_mapping: Vec<(AlignTaskName, PathBuf)> = align_tasks
- .lost
- .iter()
- .filter(|(name, _)| name.starts_with(&task))
- .cloned()
- .collect();
-
- if to == "confirm" {
- // Confirm mode
- for (_, path) in selected_lost_mapping {
- if let Err(err) = local_sheet.remove_mapping(&path) {
- eprintln!(
- "{}",
- md(t!("jv.fail.sheet.align.remove_mapping_failed", err = err))
- );
- };
- }
- // Save sheet
- match local_sheet.write().await {
- Ok(_) => {}
- Err(e) => {
- eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string())));
- return;
- }
- };
- return;
- }
-
- if to.starts_with("created") {
- // Match mode
- let created_file: Vec<(AlignTaskName, PathBuf)> = align_tasks
- .created
- .iter()
- .find(|p| p.0.starts_with(&to))
- .map(|found| found.clone())
- .into_iter()
- .collect();
-
- if selected_lost_mapping.len() < 1 {
- eprintln!("{}", md(t!("jv.fail.sheet.align.no_lost_matched")));
- return;
- }
-
- if created_file.len() < 1 {
- eprintln!("{}", md(t!("jv.fail.sheet.align.no_created_matched")));
- return;
- }
-
- if selected_lost_mapping.len() > 1 {
- eprintln!("{}", md(t!("jv.fail.sheet.align.too_many_lost")));
- return;
- }
-
- if created_file.len() > 1 {
- eprintln!("{}", md(t!("jv.fail.sheet.align.too_many_created")));
- return;
- }
-
- // Check completed, match lost and created items
- let lost_mapping = &selected_lost_mapping.first().unwrap().1;
- let created_file = local_dir.join(&created_file.first().unwrap().1);
-
- let Ok(hash_calc) = sha1_hash::calc_sha1(&created_file, 4096usize).await else {
- eprintln!("{}", md(t!("jv.fail.sheet.align.calc_hash_failed")));
- return;
- };
- let Ok(mapping) = local_sheet.mapping_data_mut(lost_mapping) else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.sheet.align.mapping_not_found",
- mapping = lost_mapping.display()
- ))
- );
- return;
- };
-
- mapping.set_last_modifiy_check_hash(Some(hash_calc.hash));
-
- // Save sheet
- match local_sheet.write().await {
- Ok(_) => {}
- Err(e) => {
- eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string())));
- return;
- }
- };
- }
- }
- // Erased: confirm mode
- else if task.starts_with("erased") {
- let selected_erased_mapping: Vec<(AlignTaskName, PathBuf)> = align_tasks
- .erased
- .iter()
- .filter(|(name, _)| name.starts_with(&task))
- .cloned()
- .collect();
-
- if to == "confirm" {
- // Confirm mode
- for (_, path) in selected_erased_mapping {
- if let Err(err) = local_sheet.remove_mapping(&path) {
- eprintln!(
- "{}",
- md(t!("jv.fail.sheet.align.delete_mapping_failed", err = err))
- );
- };
-
- let from = local_dir.join(&path);
-
- if !from.exists() {
- continue;
- }
-
- let to = local_dir
- .join(CLIENT_FOLDER_WORKSPACE_ROOT_NAME)
- .join(".temp")
- .join("erased")
- .join(path);
- let to_path = to
- .parent()
- .map(|p| p.to_path_buf())
- .unwrap_or_else(|| to.clone());
-
- let _ = fs::create_dir_all(&to_path).await;
- if let Some(e) = fs::rename(&from, &to).await.err() {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.move.rename_failed",
- from = from.display(),
- to = to.display(),
- error = e
- ))
- .yellow()
- );
- }
- }
-
- // Save sheet
- match local_sheet.write().await {
- Ok(_) => {}
- Err(e) => {
- eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string())));
- return;
- }
- };
- return;
- }
- }
-}
-
-async fn jv_track(args: TrackFileArgs) {
- // Perform glob operation before precheck, as precheck will call set_current_dir
- let track_files = if let Some(pattern) = args.track_file_pattern.clone() {
- let local_dir = match current_local_path() {
- Some(dir) => dir,
- None => {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- }
- };
- let files = glob(pattern, &local_dir).await;
- files
- .iter()
- .filter_map(|f| PathBuf::from_str(f.0).ok())
- .collect::<Vec<_>>()
- } else {
- println!("{}", md(t!("jv.track")));
- return;
- };
-
- // set_current_dir called here
- let local_config = match precheck().await {
- Some(config) => config,
- None => {
- return;
- }
- };
-
- let Some(local_workspace) = LocalWorkspace::init_current_dir(local_config.clone()) else {
- eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim());
- return;
- };
-
- if track_files.iter().len() < 1 {
- eprintln!("{}", md(t!("jv.fail.track.no_selection")));
- return;
- };
-
- let (pool, ctx, mut output) = match build_pool_and_ctx(&local_config).await {
- Some(result) => result,
- None => return,
- };
-
- let files = track_files.iter().cloned().collect();
- let overwrite = args.allow_overwrite;
- let update_info = get_update_info(local_workspace, &files, args).await;
-
- let track_action = proc_track_file_action(
- &pool,
- ctx,
- TrackFileActionArguments {
- relative_pathes: files,
- file_update_info: update_info,
- print_infos: true,
- allow_overwrite_modified: overwrite,
- },
- );
-
- tokio::select! {
- result = track_action => {
- match result {
- Ok(result) => match result {
- TrackFileActionResult::Done {
- created,
- updated,
- synced,
- skipped,
- } => {
- println!(
- "{}",
- md(t!(
- "jv.result.track.done",
- count = created.len() + updated.len() + synced.len(),
- created = created.len(),
- updated = updated.len(),
- synced = synced.len()
- ))
- );
-
- if skipped.len() > 0 {
- println!(
- "\n{}",
- md(t!(
- "jv.result.track.tip_has_skipped",
- skipped_num = skipped.len(),
- skipped = skipped
- .iter()
- .map(|f| f.display().to_string())
- .collect::<Vec<String>>()
- .join("\n")
- .trim()
- ))
- .yellow()
- );
- }
- }
- TrackFileActionResult::AuthorizeFailed(e) => {
- eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e)))
- }
- TrackFileActionResult::StructureChangesNotSolved => {
- eprintln!("{}", md(t!("jv.result.track.structure_changes_not_solved")))
- }
- TrackFileActionResult::CreateTaskFailed(create_task_result) => match create_task_result
- {
- CreateTaskResult::Success(_) => {} // Success is not handled here
- CreateTaskResult::CreateFileOnExistPath(path) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.track.create_failed.create_file_on_exist_path",
- path = path.display()
- ))
- )
- }
- CreateTaskResult::SheetNotFound(sheet) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.track.create_failed.sheet_not_found",
- name = sheet
- ))
- )
- }
- },
- TrackFileActionResult::UpdateTaskFailed(update_task_result) => match update_task_result
- {
- UpdateTaskResult::Success(_) => {} // Success is not handled here
- UpdateTaskResult::VerifyFailed { path, reason } => match reason {
- VerifyFailReason::SheetNotFound(sheet_name) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.track.update_failed.verify.sheet_not_found",
- sheet_name = sheet_name
- ))
- )
- }
- VerifyFailReason::MappingNotFound => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.track.update_failed.verify.mapping_not_found",
- path = path.display()
- ))
- )
- }
- VerifyFailReason::VirtualFileNotFound(vfid) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.track.update_failed.verify.virtual_file_not_found",
- vfid = vfid
- ))
- )
- }
- VerifyFailReason::VirtualFileReadFailed(vfid) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.track.update_failed.verify.virtual_file_read_failed",
- vfid = vfid
- ))
- )
- }
- VerifyFailReason::NotHeld => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.track.update_failed.verify.not_held",
- path = path.display()
- ))
- )
- }
- VerifyFailReason::VersionDismatch(current_version, latest_version) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.track.update_failed.verify.version_dismatch",
- version_current = current_version,
- version_latest = latest_version
- ))
- )
- }
- VerifyFailReason::UpdateButNoDescription => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.track.update_failed.verify.update_but_no_description"
- ))
- )
- }
- VerifyFailReason::VersionAlreadyExist(latest_version) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.track.update_failed.verify.version_already_exist",
- path = path.display(),
- version = latest_version
- ))
- )
- }
- },
- },
- TrackFileActionResult::SyncTaskFailed(sync_task_result) => match sync_task_result {
- SyncTaskResult::Success(_) => {} // Success is not handled here
- },
- },
- Err(e) => handle_err(e),
- }
- }
- _ = async {
- while let Some(msg) = output.recv().await {
- println!("{}", msg);
- }
- } => {}
- }
-}
-
-async fn get_update_info(
- workspace: LocalWorkspace,
- files: &HashSet<PathBuf>,
- args: TrackFileArgs,
-) -> HashMap<PathBuf, (NextVersion, UpdateDescription)> {
- let mut result = HashMap::new();
-
- if files.len() == 1 {
- if let (Some(desc), Some(ver)) = (&args.desc, &args.next_version) {
- if let Some(file) = files.iter().next() {
- result.insert(file.clone(), (ver.clone(), desc.clone()));
- return result;
- }
- }
- }
- if args.work {
- return start_update_editor(workspace, files, &args).await;
- }
-
- result
-}
-
-async fn start_update_editor(
- workspace: LocalWorkspace,
- files: &HashSet<PathBuf>,
- args: &TrackFileArgs,
-) -> HashMap<PathBuf, (NextVersion, UpdateDescription)> {
- let account = workspace.config().lock().await.current_account();
-
- let Ok(latest_file_data_path) = LatestFileData::data_path(&account) else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_file_data",
- account = &account
- ))
- );
- return HashMap::new();
- };
-
- // Get latest file data
- let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_file_data",
- account = &account
- ))
- );
- return HashMap::new();
- };
-
- // Get files
- let Ok(analyzed) = AnalyzeResult::analyze_local_status(&workspace).await else {
- return HashMap::new();
- };
- // Has unsolved moves, skip
- if analyzed.lost.len() > 0 || analyzed.moved.len() > 0 {
- return HashMap::new();
- }
- // No modified, skip
- if analyzed.modified.len() < 1 {
- return HashMap::new();
- }
- // No sheet, skip
- let Some(sheet) = workspace.config().lock().await.sheet_in_use().clone() else {
- return HashMap::new();
- };
- // No cached sheet, skip
- let Ok(cached_sheet) = CachedSheet::cached_sheet_data(&sheet).await else {
- return HashMap::new();
- };
- let files: Vec<(PathBuf, VirtualFileVersion)> = files
- .iter()
- .filter_map(|file| {
- if analyzed.modified.contains(file) {
- if let Some(mapping_item) = cached_sheet.mapping().get(file) {
- if let Some(latest_version) = latest_file_data.file_version(&mapping_item.id) {
- return Some((file.clone(), latest_version.clone()));
- }
- }
- None
- } else {
- None
- }
- })
- .collect();
-
- // Generate editor text
- let mut table = SimpleTable::new_with_padding(
- vec![
- t!("editor.modified_line.header.file_path").trim(),
- t!("editor.modified_line.header.old_version").trim(),
- "",
- t!("editor.modified_line.header.new_version").trim(),
- ],
- 2,
- );
- for item in files {
- let path = item.0.display().to_string();
- let base_ver = item.1.to_string();
- let next_ver = push_version(&base_ver).unwrap_or(" ".to_string());
- table.push_item(vec![
- path,
- base_ver,
- t!("editor.modified_line.content.arrow").trim().to_string(),
- next_ver,
- ]);
- }
- let lines = table.to_string();
-
- let str = t!(
- "editor.update_editor",
- modified_lines = lines,
- description = args.desc.clone().unwrap_or_default()
- );
-
- let path = workspace
- .local_path()
- .join(CLIENT_PATH_WORKSPACE_ROOT)
- .join(".UPDATE.md");
- let result = input_with_editor(str, path, "#").await.unwrap_or_default();
-
- let mut update_info = HashMap::new();
-
- // Parse the result returned from the editor
- let lines: Vec<&str> = result.lines().collect();
- let mut i = 0;
-
- // Find the separator line
- let mut separator_index = None;
- while i < lines.len() {
- let line = lines[i].trim();
- if line.chars().all(|c| c == '-') && line.len() >= 5 {
- separator_index = Some(i);
- break;
- }
- i += 1;
- }
-
- if let Some(sep_idx) = separator_index {
- // Parse path and version information before the separator
- for line in &lines[..sep_idx] {
- let trimmed_line = line.trim();
- if trimmed_line.is_empty() {
- continue;
- }
-
- // Parse format: /directory/file.extension version -> new_version
- if let Some(arrow_pos) = trimmed_line.find("->") {
- let before_arrow = &trimmed_line[..arrow_pos].trim();
- let after_arrow = &trimmed_line[arrow_pos + 2..].trim();
-
- // Separate path and old version
- if let Some(last_space) = before_arrow.rfind(' ') {
- let path_str = &before_arrow[..last_space].trim();
- let _old_version = &before_arrow[last_space + 1..].trim(); // Old version, needs parsing but not used
- let new_version = after_arrow.trim();
-
- if !path_str.is_empty() && !new_version.is_empty() {
- let path = PathBuf::from(path_str);
- // Get description (all content after the separator)
- let description = lines[sep_idx + 1..].join("\n").trim().to_string();
-
- // Only add to update_info if description is not empty after trimming
- if !description.is_empty() {
- update_info.insert(path, (new_version.to_string(), description));
- }
- }
- }
- }
- }
- }
-
- update_info
-}
-
-async fn jv_hold(args: HoldFileArgs) {
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let Some(hold_file_pattern) = args.hold_file_pattern else {
- println!("{}", md(t!("jv.hold")));
- return;
- };
-
- let files = glob(hold_file_pattern, &local_dir).await;
-
- let _ = correct_current_dir();
-
- jv_change_edit_right(
- files
- .iter()
- .filter_map(|f| PathBuf::from_str(f.0).ok())
- .collect(),
- EditRightChangeBehaviour::Hold,
- args.show_fail_details,
- args.skip_failed,
- args.force,
- )
- .await;
-}
-
-async fn jv_throw(args: ThrowFileArgs) {
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let Some(throw_file_pattern) = args.throw_file_pattern else {
- println!("{}", md(t!("jv.throw")));
- return;
- };
-
- let files = glob(throw_file_pattern, &local_dir).await;
-
- let _ = correct_current_dir();
-
- jv_change_edit_right(
- files
- .iter()
- .filter_map(|f| PathBuf::from_str(f.0).ok())
- .collect(),
- EditRightChangeBehaviour::Throw,
- args.show_fail_details,
- args.skip_failed,
- args.force,
- )
- .await;
-}
-
-async fn jv_change_edit_right(
- files: Vec<PathBuf>,
- behaviour: EditRightChangeBehaviour,
- show_fail_details: bool,
- mut skip_failed: bool,
- force: bool,
-) {
- // If both `--details` and `--skip-failed` are set, only enable `--details`
- if show_fail_details && skip_failed {
- skip_failed = false;
- }
-
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let Ok(local_cfg) = LocalConfig::read_from(local_dir.join(CLIENT_FILE_WORKSPACE)).await else {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- return;
- };
-
- let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else {
- eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim());
- return;
- };
-
- // Get files
- let Ok(analyzed) = AnalyzeResult::analyze_local_status(&local_workspace).await else {
- eprintln!("{}", md(t!("jv.fail.status.analyze")).trim());
- return;
- };
-
- let account = local_cfg.current_account();
-
- let Ok(latest_file_data_path) = LatestFileData::data_path(&account) else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_file_data",
- account = &account
- ))
- );
- return;
- };
-
- let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_file_data",
- account = &account
- ))
- );
- return;
- };
-
- let Some(sheet_name) = local_cfg.sheet_in_use().clone() else {
- eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim());
- return;
- };
-
- let Ok(local_sheet) = local_workspace.local_sheet(&account, &sheet_name).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.local_sheet",
- account = &account,
- sheet = &sheet_name
- ))
- );
- return;
- };
-
- let Ok(cached_sheet) = CachedSheet::cached_sheet_data(&sheet_name).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.cached_sheet",
- sheet = &sheet_name
- ))
- );
- return;
- };
-
- let num = files.iter().len();
- if num < 1 {
- eprintln!("{}", md(t!("jv.fail.change_edit_right.no_selection")));
- return;
- }
-
- let mut passed_files = Vec::new();
- let mut details = Vec::new();
- let mut failed = 0;
-
- // Helper function to handle validation failures
- fn handle_validation_failure(
- show_fail_details: bool,
- details: &mut Vec<String>,
- failed: &mut usize,
- num: usize,
- reason: String,
- ) -> bool {
- if show_fail_details {
- details.push(reason);
- *failed += 1;
- true // Continue to next file
- } else {
- eprintln!(
- "{}",
- md(t!("jv.fail.change_edit_right.check_failed", num = num))
- );
- false // Break processing
- }
- }
-
- for file in files {
- let exists = file.exists();
-
- // If force is enabled, add to the list regardless
- if force {
- passed_files.push(file);
- continue;
- }
-
- // Mapping exists
- let Some(cached_mapping) = cached_sheet.mapping().get(&file) else {
- let reason = t!(
- "jv.fail.change_edit_right.check_fail_item",
- path = file.display(),
- reason = t!("jv.fail.change_edit_right.check_fail_reason.not_found_in_sheet")
- )
- .trim()
- .to_string();
-
- if !handle_validation_failure(show_fail_details, &mut details, &mut failed, num, reason)
- {
- return;
- }
- continue;
- };
-
- let vfid: VirtualFileId = if exists {
- // Not tracked
- let Ok(local_mapping) = local_sheet.mapping_data(&file) else {
- let reason = t!(
- "jv.fail.change_edit_right.check_fail_item",
- path = file.display(),
- reason = t!("jv.fail.change_edit_right.check_fail_reason.not_a_tracked_file")
- )
- .trim()
- .to_string();
-
- if !handle_validation_failure(
- show_fail_details,
- &mut details,
- &mut failed,
- num,
- reason,
- ) {
- return;
- }
- continue;
- };
-
- let vfid = local_mapping.mapping_vfid();
- let local_version = local_mapping.version_when_updated();
-
- // Base version unmatch
- if local_version
- != latest_file_data
- .file_version(vfid)
- .unwrap_or(&String::default())
- {
- let reason = t!(
- "jv.fail.change_edit_right.check_fail_item",
- path = file.display(),
- reason = t!("jv.fail.change_edit_right.check_fail_reason.base_version_unmatch")
- )
- .trim()
- .to_string();
-
- if !handle_validation_failure(
- show_fail_details,
- &mut details,
- &mut failed,
- num,
- reason,
- ) {
- return;
- }
- continue;
- }
-
- vfid.clone()
- } else {
- cached_mapping.id.clone()
- };
-
- // Hold validation
- let holder = latest_file_data.file_holder(&vfid);
- let validation_passed = match behaviour {
- EditRightChangeBehaviour::Hold => {
- if holder.is_some_and(|h| h != &account) {
- // Has holder but not current account
- let holder_name = holder.unwrap();
- let reason = t!(
- "jv.fail.change_edit_right.check_fail_item",
- path = file.display(),
- reason = t!(
- "jv.fail.change_edit_right.check_fail_reason.has_holder",
- holder = holder_name
- )
- )
- .trim()
- .to_string();
-
- if !handle_validation_failure(
- show_fail_details,
- &mut details,
- &mut failed,
- num,
- reason,
- ) {
- return;
- }
- false
- } else if holder.is_some_and(|h| h == &account) {
- // Already held by current account
- let reason = t!(
- "jv.fail.change_edit_right.check_fail_item",
- path = file.display(),
- reason = t!("jv.fail.change_edit_right.check_fail_reason.already_held")
- )
- .trim()
- .to_string();
-
- if !handle_validation_failure(
- show_fail_details,
- &mut details,
- &mut failed,
- num,
- reason,
- ) {
- return;
- }
- false
- } else {
- true
- }
- }
- EditRightChangeBehaviour::Throw => {
- if holder.is_some_and(|h| h != &account) {
- // Not the holder
- let reason = t!(
- "jv.fail.change_edit_right.check_fail_item",
- path = file.display(),
- reason = t!("jv.fail.change_edit_right.check_fail_reason.not_holder")
- )
- .trim()
- .to_string();
-
- if !handle_validation_failure(
- show_fail_details,
- &mut details,
- &mut failed,
- num,
- reason,
- ) {
- return;
- }
- false
- } else if analyzed.modified.contains(&file) {
- // Already modified
- let reason = t!(
- "jv.fail.change_edit_right.check_fail_item",
- path = file.display(),
- reason = t!("jv.fail.change_edit_right.check_fail_reason.already_modified")
- )
- .trim()
- .to_string();
-
- if !handle_validation_failure(
- show_fail_details,
- &mut details,
- &mut failed,
- num,
- reason,
- ) {
- return;
- }
- false
- } else {
- true
- }
- }
- };
-
- if validation_passed {
- passed_files.push(file);
- }
- }
-
- if failed > 0 && show_fail_details {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.change_edit_right.check_failed_details",
- num = num,
- failed = failed,
- items = details.join("\n").trim().yellow()
- ))
- );
- return;
- }
-
- if !(failed > 0 && skip_failed) && failed != 0 {
- return;
- }
-
- let (pool, ctx, _output) = match build_pool_and_ctx(&local_cfg).await {
- Some(result) => result,
- None => return,
- };
-
- let passed = passed_files
- .iter()
- .map(|f| (f.clone(), behaviour.clone()))
- .collect();
-
- match proc_change_virtual_file_edit_right_action(&pool, ctx, (passed, true)).await {
- Ok(r) => match r {
- ChangeVirtualFileEditRightResult::Success {
- success_hold,
- success_throw,
- } => {
- if success_hold.len() > 0 && success_throw.len() == 0 {
- println!(
- "{}",
- md(t!(
- "jv.result.change_edit_right.success.hold",
- num = success_hold.len()
- ))
- )
- } else if success_hold.len() == 0 && success_throw.len() > 0 {
- println!(
- "{}",
- md(t!(
- "jv.result.change_edit_right.success.throw",
- num = success_throw.len()
- ))
- )
- } else if success_hold.len() > 0 && success_throw.len() > 0 {
- println!(
- "{}",
- md(t!(
- "jv.result.change_edit_right.success.mixed",
- num = success_hold.len() + success_throw.len(),
- num_hold = success_hold.len(),
- num_throw = success_throw.len()
- ))
- )
- } else {
- eprintln!("{}", md(t!("jv.result.change_edit_right.failed.none")))
- }
- }
- ChangeVirtualFileEditRightResult::AuthorizeFailed(e) => {
- eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e)))
- }
- ChangeVirtualFileEditRightResult::DoNothing => {
- eprintln!("{}", md(t!("jv.result.change_edit_right.failed.none")))
- }
- },
- Err(e) => handle_err(e),
- }
-}
-
-async fn jv_move(args: MoveMappingArgs) {
- let local_dir = match current_local_path() {
- Some(dir) => dir,
- None => {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- }
- };
-
- let move_files = if let Some(from_pattern) = args.move_mapping_pattern.clone() {
- let from = glob(from_pattern, &local_dir).await;
- from.iter()
- .filter_map(|f| PathBuf::from_str(f.0).ok())
- .collect::<Vec<_>>()
- } else {
- println!("{}", md(t!("jv.move")));
- return;
- };
-
- let to_pattern = if args.to_mapping_pattern.is_some() {
- args.to_mapping_pattern.unwrap()
- } else {
- if args.erase {
- "".to_string()
- } else {
- eprintln!("{}", md(t!("jv.fail.move.no_target_dir")));
- return;
- }
- };
-
- let is_to_pattern_a_dir = to_pattern.ends_with('/') || to_pattern.ends_with('\\');
-
- let from_mappings = move_files
- .iter()
- .map(|f| f.display().to_string())
- .collect::<Vec<_>>();
-
- let base_path = Globber::from(&to_pattern).base().clone();
- let base_path = format_path(base_path.strip_prefix(&local_dir).unwrap().join("./")).unwrap();
- let to_path = base_path.join(to_pattern);
-
- let mut edit_mapping_args: EditMappingActionArguments = EditMappingActionArguments {
- operations: HashMap::<FromRelativePathBuf, OperationArgument>::new(),
- };
-
- if args.erase {
- // Generate erase operation parameters
- for from_mapping in from_mappings {
- edit_mapping_args
- .operations
- .insert(from_mapping.into(), (EditMappingOperations::Erase, None));
- }
- } else {
- // Generate move operation parameters
- // Single file move
- if from_mappings.len() == 1 {
- let from = format_path_str(from_mappings[0].clone()).unwrap();
- let to = if is_to_pattern_a_dir {
- // Input is a directory, append the filename
- format_path(
- to_path
- .join(from.strip_prefix(&base_path.display().to_string()).unwrap())
- .to_path_buf(),
- )
- .unwrap()
- } else {
- // Input is a filename, use it directly
- format_path(to_path.to_path_buf()).unwrap()
- };
-
- let from: PathBuf = from.into();
- // If the from path contains to_path, ignore it to avoid duplicate moves
- if !from.starts_with(to_path) {
- edit_mapping_args
- .operations
- .insert(from, (EditMappingOperations::Move, Some(to.clone())));
- }
- } else
- // Multiple file move
- if from_mappings.len() > 1 && is_to_pattern_a_dir {
- let to_path = format_path(to_path).unwrap();
- for p in &from_mappings {
- let name = p.strip_prefix(&base_path.display().to_string()).unwrap();
- let to = format_path(to_path.join(name))
- .unwrap()
- .display()
- .to_string();
-
- let from: PathBuf = p.into();
- // If the from path contains to_path, ignore it to avoid duplicate moves
- if !from.starts_with(to_path.display().to_string()) {
- edit_mapping_args
- .operations
- .insert(from, (EditMappingOperations::Move, Some(to.into())));
- }
- }
- }
- if from_mappings.len() > 1 && !is_to_pattern_a_dir {
- eprintln!("{}", md(t!("jv.fail.move.count_doesnt_match")));
- return;
- }
-
- // NOTE
- // if move_file_mappings.len() < 1 {
- // This case has already been handled earlier: output Help
- // }
- }
-
- let local_cfg = match precheck().await {
- Some(config) => config,
- None => return,
- };
-
- let (pool, ctx, _output) = match build_pool_and_ctx(&local_cfg).await {
- Some(result) => result,
- None => return,
- };
-
- if proc_mapping_edit(&pool, ctx, edit_mapping_args.clone())
- .await
- .is_ok()
- {
- // If the operation succeeds and only_remote is not enabled,
- // synchronize local moves
- if !args.only_remote {
- let erase_dir = local_dir
- .join(CLIENT_FOLDER_WORKSPACE_ROOT_NAME)
- .join(".temp")
- .join("erased");
-
- let mut skipped = 0;
- for (from_relative, (operation, to_relative)) in edit_mapping_args.operations {
- let from = local_dir.join(&from_relative);
-
- if !from.exists() {
- continue;
- }
-
- let to = match operation {
- EditMappingOperations::Move => local_dir.join(to_relative.unwrap()),
- EditMappingOperations::Erase => erase_dir.join(&from_relative),
- };
- if let Some(to_dir) = to.parent() {
- let _ = fs::create_dir_all(to_dir).await;
- }
- if let Some(e) = fs::rename(&from, &to).await.err() {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.move.rename_failed",
- from = from.display(),
- to = to.display(),
- error = e
- ))
- .yellow()
- );
- skipped += 1;
- }
- }
- if skipped > 0 {
- eprintln!("{}", md(t!("jv.fail.move.has_rename_failed")));
- }
- }
- }
-}
-
-async fn proc_mapping_edit(
- pool: &ActionPool,
- ctx: ActionContext,
- edit_mapping_args: EditMappingActionArguments,
-) -> Result<(), ()> {
- match proc_edit_mapping_action(
- pool,
- ctx,
- EditMappingActionArguments {
- operations: edit_mapping_args.operations.clone(),
- },
- )
- .await
- {
- Ok(r) => match r {
- EditMappingActionResult::Success => {
- println!("{}", md(t!("jv.result.move.success")));
- Ok(())
- }
- EditMappingActionResult::AuthorizeFailed(e) => {
- eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e)));
- Err(())
- }
- EditMappingActionResult::MappingNotFound(path_buf) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.move.mapping_not_found",
- path = path_buf.display()
- ))
- );
- Err(())
- }
- EditMappingActionResult::InvalidMove(invalid_move_reason) => {
- match invalid_move_reason {
- InvalidMoveReason::MoveOperationButNoTarget(path_buf) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.move.invalid_move.no_target",
- path = path_buf.display()
- ))
- );
- }
- InvalidMoveReason::ContainsDuplicateMapping(path_buf) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.move.invalid_move.duplicate_mapping",
- path = path_buf.display()
- ))
- );
- }
- }
- Err(())
- }
- EditMappingActionResult::Unknown => {
- eprintln!("{}", md(t!("jv.result.move.unknown")));
- Err(())
- }
- EditMappingActionResult::EditNotAllowed => {
- eprintln!(
- "{}",
- md(t!("jv.result.common.not_allowed_in_reference_sheet"))
- );
- Err(())
- }
- },
- Err(e) => {
- handle_err(e);
- Err(())
- }
- }
-}
-
-async fn jv_share(args: ShareMappingArgs) {
- if let (Some(args1), None, None) = (&args.args1, &args.args2, &args.args3) {
- // List mode
- if args1.trim() == "list" || args1.trim() == "ls" {
- share_list(args).await;
- return;
- }
-
- share_accept(args1.to_string(), args).await;
- return;
- }
-
- if let (Some(args1), Some(args2), None) = (&args.args1, &args.args2, &args.args3) {
- // 如果是 work 模式,那么就是分享
- if args.work {
- share_out(
- args1.to_string(),
- args2.to_string(),
- String::default(),
- args,
- )
- .await;
- return;
- }
-
- // See mode
- if args1.trim() == "see" {
- share_see(args2.to_string(), args).await;
- return;
- }
-
- share_in(args1.to_string(), args2.to_string(), args).await;
- return;
- }
-
- if let (Some(share_pattern), Some(to_sheet), Some(description)) =
- (&args.args1, &args.args2, &args.args3)
- {
- share_out(
- share_pattern.to_string(),
- to_sheet.to_string(),
- description.to_string(),
- args,
- )
- .await;
- return;
- }
-
- println!("{}", md(t!("jv.share")));
-}
-
-async fn share_list(args: ShareMappingArgs) {
- let _ = correct_current_dir();
-
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let local_config = match precheck().await {
- Some(config) => config,
- None => return,
- };
-
- let sheet_name = local_config.sheet_in_use().clone().unwrap_or_default();
-
- let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_config.current_account(),
- ))
- .await
- else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_info",
- account = &local_config.current_account()
- ))
- );
- return;
- };
-
- if let Some(shares) = latest_info.shares_in_my_sheets.get(&sheet_name) {
- // Sort
- let mut sorted_shares: BTreeMap<String, &Share> = BTreeMap::new();
- for (id, share) in shares {
- sorted_shares.insert(id.clone(), share);
- }
-
- if args.json_output {
- let share_list: Vec<ShareItem> = sorted_shares
- .iter()
- .map(|(share_id, share)| ShareItem {
- share_id: share_id.clone(),
- sharer: share.sharer.clone(),
- description: share.description.clone(),
- file_count: share.mappings.len(),
- })
- .collect();
- let result = ShareListResult { share_list };
- print_json(result, args.pretty);
- return;
- }
-
- if !args.raw {
- // Create table and insert information
- let mut table = SimpleTable::new(vec![
- t!("jv.success.share.list.headers.id"),
- t!("jv.success.share.list.headers.sharer"),
- t!("jv.success.share.list.headers.description"),
- t!("jv.success.share.list.headers.file_count"),
- ]);
- for (id, share) in sorted_shares {
- table.insert_item(
- 0,
- vec![
- id.to_string(),
- share.sharer.to_string(),
- truncate_first_line(share.description.to_string()),
- share.mappings.len().to_string(),
- ],
- );
- }
-
- // Render
- println!("{}", table);
- println!("{}", md(t!("jv.success.share.list.footer")));
- } else {
- sorted_shares
- .iter()
- .for_each(|share| println!("{}", share.0));
- }
- }
-}
-
-async fn share_see(share_id: String, args: ShareMappingArgs) {
- let _ = correct_current_dir();
-
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let local_config = match precheck().await {
- Some(config) => config,
- None => return,
- };
-
- let sheet_name = local_config.sheet_in_use().clone().unwrap_or_default();
-
- let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_config.current_account(),
- ))
- .await
- else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_info",
- account = &local_config.current_account()
- ))
- );
- return;
- };
-
- if let Some(shares) = latest_info.shares_in_my_sheets.get(&sheet_name) {
- if let Some(share) = shares.get(&share_id) {
- if args.json_output {
- let result = SeeShareResult {
- share_id: share_id.clone(),
- sharer: share.sharer.clone(),
- description: share.description.clone(),
- mappings: share.mappings.clone(),
- };
- print_json(result, args.pretty);
- return;
- }
-
- println!(
- "{}",
- md(t!(
- "jv.success.share.content",
- share_id = share_id,
- sharer = share.sharer,
- description = share.description,
- mappings = render_share_path_tree(&share.mappings)
- ))
- );
- }
- }
-}
-
-async fn share_accept(import_id: String, args: ShareMappingArgs) {
- let _ = correct_current_dir();
-
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let local_config = match precheck().await {
- Some(config) => config,
- None => return,
- };
-
- let sheet_name = local_config.sheet_in_use().clone().unwrap_or_default();
-
- let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_config.current_account(),
- ))
- .await
- else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_info",
- account = &local_config.current_account()
- ))
- );
- return;
- };
-
- let contains_share = if let Some(share_ids) = latest_info.shares_in_my_sheets.get(&sheet_name) {
- share_ids.contains_key(&import_id)
- } else {
- false
- };
-
- if !contains_share {
- eprintln!(
- "{}",
- md(t!("jv.fail.share.share_id_not_exist", id = &import_id))
- );
- return;
- }
-
- let (pool, ctx, _output) = match build_pool_and_ctx(&local_config).await {
- Some(result) => result,
- None => return,
- };
-
- let share_merge_mode = {
- if args.safe {
- ShareMergeMode::Safe
- } else if args.skip {
- ShareMergeMode::Skip
- } else if args.overwrite {
- ShareMergeMode::Overwrite
- } else if args.reject {
- ShareMergeMode::RejectAll
- } else {
- ShareMergeMode::Safe
- }
- };
-
- match proc_merge_share_mapping_action(
- &pool,
- ctx,
- MergeShareMappingArguments {
- share_id: import_id.clone(),
- share_merge_mode,
- },
- )
- .await
- {
- Ok(r) => match r {
- MergeShareMappingActionResult::Success => {
- if args.reject {
- println!(
- "{}",
- md(t!(
- "jv.result.share.merge_shares.success_reject",
- share_id = &import_id
- ))
- );
- } else {
- println!(
- "{}",
- md(t!(
- "jv.result.share.merge_shares.success",
- share_id = &import_id,
- sheet = &sheet_name
- ))
- );
- }
- }
- MergeShareMappingActionResult::HasConflicts => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.share.merge_shares.has_conflicts",
- share_id = &import_id
- ))
- );
- }
- MergeShareMappingActionResult::AuthorizeFailed(e) => {
- eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e)));
- }
- MergeShareMappingActionResult::EditNotAllowed => {
- eprintln!(
- "{}",
- md(t!("jv.result.share.merge_shares.edit_not_allowed"))
- );
- }
- MergeShareMappingActionResult::ShareIdNotFound(share_id) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.share.merge_shares.share_id_not_found",
- share_id = share_id
- ))
- );
- }
- MergeShareMappingActionResult::MergeFails(error) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.share.merge_shares.merge_failed",
- error = error
- ))
- );
- }
- MergeShareMappingActionResult::Unknown => {
- eprintln!("{}", md(t!("jv.result.share.merge_shares.unknown")));
- }
- },
- Err(e) => handle_err(e),
- }
-}
-
-async fn share_in(_from_sheet: String, _import_pattern: String, _args: ShareMappingArgs) {
- // TODO: Implement pull mode logic
-}
-
-async fn share_out(
- share_pattern: String,
- to_sheet: String,
- description: String,
- args: ShareMappingArgs,
-) {
- let mut shared_files = {
- let local_dir = match current_local_path() {
- Some(dir) => dir,
- None => {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- }
- };
- let files = glob(share_pattern, &local_dir).await;
- files
- .iter()
- .filter_map(|f| PathBuf::from_str(f.0).ok())
- .collect::<Vec<_>>()
- };
-
- let _ = correct_current_dir();
-
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let local_config = match precheck().await {
- Some(config) => config,
- None => return,
- };
-
- let Some(local_workspace) = LocalWorkspace::init_current_dir(local_config.clone()) else {
- eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim());
- return;
- };
-
- let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path(
- &local_dir,
- &local_config.current_account(),
- ))
- .await
- else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.latest_info",
- account = &local_config.current_account()
- ))
- );
- return;
- };
-
- // Pre-check if the sheet exists
- let contains_in_my_sheet = latest_info.visible_sheets.contains(&to_sheet);
- let contains_in_other_sheet = latest_info
- .invisible_sheets
- .iter()
- .find(|info| info.sheet_name == to_sheet)
- .is_some();
- if !contains_in_my_sheet && !contains_in_other_sheet {
- eprintln!(
- "{}",
- md(t!("jv.fail.share.invalid_target_sheet", sheet = &to_sheet))
- );
- return;
- }
-
- let (pool, ctx, _output) = match build_pool_and_ctx(&local_config).await {
- Some(result) => result,
- None => return,
- };
-
- let to_sheet_holder = {
- if latest_info.reference_sheets.contains(&to_sheet) {
- VAULT_HOST_NAME.to_string()
- } else if latest_info.visible_sheets.contains(&to_sheet) {
- local_config.current_account()
- } else {
- let mut holder = String::new();
- for info in &latest_info.invisible_sheets {
- if info.sheet_name == to_sheet {
- holder = info.holder_name.as_ref().cloned().unwrap_or_default();
- break;
- }
- }
- holder
- }
- };
-
- let Some(description) = (if args.work {
- start_share_editor(&local_workspace, &mut shared_files, &to_sheet_holder).await
- } else {
- Some(description.to_string())
- }) else {
- eprintln!("{}", md(t!("jv.fail.share.no_description")));
- return;
- };
-
- match proc_share_mapping_action(
- &pool,
- ctx,
- ShareMappingArguments {
- mappings: shared_files.clone(),
- description,
-
- // Since the Action internally checks the current sheet,
- // there's no need to fill in from_sheet here.
- // This is prepared for pull operations.
- from_sheet: None,
- to_sheet: to_sheet.clone(),
- },
- )
- .await
- {
- Ok(r) => match r {
- ShareMappingActionResult::Success => {
- println!(
- "{}",
- md(t!(
- "jv.result.share.share_mapping.success",
- file_nums = shared_files.len(),
- to_sheet = to_sheet,
- to_sheet_holder = to_sheet_holder
- ))
- );
- }
- ShareMappingActionResult::AuthorizeFailed(e) => {
- eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e)));
- }
- ShareMappingActionResult::TargetSheetNotFound(sheet) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.share.share_mapping.target_sheet_not_found",
- to_sheet = sheet
- ))
- );
- }
- ShareMappingActionResult::TargetIsSelf => {
- eprintln!("{}", md(t!("jv.result.share.share_mapping.target_is_self")));
- }
- ShareMappingActionResult::MappingNotFound(path_buf) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.share.share_mapping.mapping_not_found",
- mapping = path_buf.display()
- ))
- );
- }
- ShareMappingActionResult::Unknown => {
- eprintln!("{}", md(t!("jv.result.share.share_mapping.unknown")));
- }
- },
- Err(e) => handle_err(e),
- }
-}
-
-async fn start_share_editor(
- workspace: &LocalWorkspace,
- shared_files: &mut Vec<PathBuf>,
- holder: &MemberId,
-) -> Option<String> {
- let config = workspace.config().lock().await.clone();
-
- let sheet_name = config.sheet_in_use().clone();
- let Some(sheet_name) = sheet_name else {
- eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim());
- return None;
- };
-
- let account = config.current_account();
- let Ok(local_sheet) = workspace.local_sheet(&account, &sheet_name).await else {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.cfg_not_found.local_sheet",
- account = &account,
- sheet = &sheet_name
- ))
- );
- return None;
- };
-
- // Generate shared files and automatically comment out lost mappings
- let shared_files_str: String = shared_files
- .iter()
- .map(|p| {
- if local_sheet.mapping_data(p).is_err() {
- format!("# {}", p.display().to_string())
- } else {
- p.display().to_string()
- }
- })
- .collect::<Vec<String>>()
- .join("\n");
- let shared_files_str = shared_files_str.trim();
-
- let editor_content = t!(
- "editor.share_editor",
- sheet = sheet_name,
- holder = holder,
- shared_files = shared_files_str
- );
-
- let path = workspace
- .local_path()
- .join(CLIENT_PATH_WORKSPACE_ROOT)
- .join(".SHARE.md");
-
- let result = input_with_editor(format!("{}\n", editor_content), path, "#")
- .await
- .unwrap_or_default();
-
- // Find the last separator line (a line that, when trimmed, consists of at least 3 '-' characters)
- let lines: Vec<&str> = result.lines().collect();
- let mut last_separator_index = None;
-
- for (i, line) in lines.iter().enumerate() {
- let trimmed = line.trim();
- if trimmed.chars().all(|c| c == '-') && trimmed.len() >= 3 {
- last_separator_index = Some(i);
- }
- }
-
- // Extract content after the last separator
- let description = if let Some(sep_idx) = last_separator_index {
- lines[sep_idx + 1..].join("\n").trim().to_string()
- } else {
- result.trim().to_string()
- };
-
- // Update shared_files with the first segment (before the first separator)
- if let Some(sep_idx) = last_separator_index {
- let first_segment: Vec<PathBuf> = lines[..sep_idx]
- .iter()
- .map(|line| line.trim())
- .filter(|line| !line.is_empty())
- .map(PathBuf::from)
- .collect();
- *shared_files = first_segment;
- }
-
- if description.trim().is_empty() {
- None
- } else {
- Some(description)
- }
-}
-
-async fn jv_account_add(user_dir: UserDirectory, args: AccountAddArgs) {
- let member = Member::new(args.account_name.clone());
-
- match user_dir.register_account(member).await {
- Ok(_) => {
- println!(
- "{}",
- t!("jv.success.account.add", account = args.account_name)
- );
- }
- Err(_) => {
- eprintln!("{}", t!("jv.fail.account.add", account = args.account_name));
- return;
- }
- }
- if args.keygen {
- let output_path = current_dir().unwrap().join("tempkey.pem");
-
- match Command::new("openssl")
- .args([
- "genpkey",
- "-algorithm",
- "ed25519",
- "-out",
- &output_path.to_string_lossy(),
- ])
- .status()
- .await
- {
- Ok(status) if status.success() => {
- jv_account_move_key(
- user_dir,
- MoveKeyToAccountArgs {
- help: false,
- account_name: args.account_name,
- key_path: output_path,
- },
- )
- .await
- }
- Ok(_) => {
- eprintln!("{}", t!("jv.fail.account.keygen"));
- }
- Err(_) => {
- eprintln!("{}", t!("jv.fail.account.keygen_exec"));
- }
- }
- }
-}
-
-async fn jv_account_remove(user_dir: UserDirectory, args: AccountRemoveArgs) {
- match user_dir.remove_account(&args.account_name) {
- Ok(_) => {
- println!(
- "{}",
- t!("jv.success.account.remove", account = args.account_name)
- );
- }
- Err(_) => {
- eprintln!(
- "{}",
- t!("jv.fail.account.remove", account = args.account_name)
- );
- }
- }
-}
-
-async fn jv_account_list(user_dir: UserDirectory, args: AccountListArgs) {
- let _ = correct_current_dir();
-
- if args.json_output {
- let Ok(account_ids) = user_dir.account_ids() else {
- return;
- };
- let mut result = HashMap::new();
- for account_id in account_ids {
- let has_private_key = user_dir.has_private_key(&account_id);
- result.insert(account_id, AccountItem { has_private_key });
- }
- let json_result = AccountListJsonResult { result };
- print_json(json_result, args.pretty);
- return;
- }
-
- if args.raw {
- let Ok(account_ids) = user_dir.account_ids() else {
- return;
- };
- account_ids.iter().for_each(|a| println!("host/{}", a));
- account_ids.iter().for_each(|a| println!("{}", a));
- return;
- }
-
- match user_dir.account_ids() {
- Ok(account_ids) => {
- println!(
- "{}",
- md(t!(
- "jv.success.account.list.header",
- num = account_ids.len()
- ))
- );
-
- let mut i = 0;
- for account_id in account_ids {
- println!("{}. {} {}", i + 1, &account_id, {
- if user_dir.has_private_key(&account_id) {
- t!("jv.success.account.list.status_has_key")
- } else {
- std::borrow::Cow::Borrowed("")
- }
- });
- i += 1;
- }
- }
- Err(_) => {
- eprintln!("{}", t!("jv.fail.account.list"));
- }
- }
-}
-
-async fn jv_account_as(user_dir: UserDirectory, args: SetLocalWorkspaceAccountArgs) {
- let (account, is_host_mode) = process_account_parameter(args.account_name);
-
- // Account exist
- let Ok(member) = user_dir.account(&account).await else {
- eprintln!("{}", t!("jv.fail.account.not_found", account = &account));
- return;
- };
-
- let Some(_local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let Ok(mut local_cfg) = LocalConfig::read().await else {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- return;
- };
-
- if let Err(_) = local_cfg.set_current_account(member.id()) {
- eprintln!("{}", md(t!("jv.fail.account.as")));
- return;
- };
-
- local_cfg.set_host_mode(is_host_mode);
-
- match LocalConfig::write(&local_cfg).await {
- Ok(_) => {}
- Err(e) => {
- eprintln!("{}", md(t!("jv.fail.write_cfg", error = e)));
- return;
- }
- };
-
- if is_host_mode {
- println!(
- "{}",
- md(t!("jv.success.account.as_host", account = member.id()))
- );
- } else {
- println!(
- "{}",
- t!("jv.success.account.as", account = member.id()).trim()
- );
- }
-}
-
-/// Input account, get MemberId and whether it's a host
-/// If input is host/xxx, output is xxx, true
-/// If input is xxx, output is xxx, false
-fn process_account_parameter(account_input: String) -> (MemberId, bool) {
- let is_host = account_input.starts_with("host/");
- let account_id = if is_host {
- account_input.strip_prefix("host/").unwrap().to_string()
- } else {
- account_input
- };
- (snake_case!(account_id), is_host)
-}
-
-async fn jv_account_move_key(user_dir: UserDirectory, args: MoveKeyToAccountArgs) {
- // Key file exist
- if !args.key_path.exists() {
- eprintln!(
- "{}",
- t!("jv.fail.path_not_found", path = args.key_path.display())
- );
- return;
- }
-
- // Account exist
- let Ok(_member) = user_dir.account(&args.account_name).await else {
- eprintln!(
- "{}",
- t!("jv.fail.account.not_found", account = args.account_name)
- );
- return;
- };
-
- // Rename key file
- match move_across_partitions(
- args.key_path,
- user_dir.account_private_key_path(&args.account_name),
- )
- .await
- {
- Ok(_) => println!("{}", t!("jv.success.account.move_key")),
- Err(_) => eprintln!("{}", t!("jv.fail.account.move_key")),
- }
-}
-
-async fn jv_account_generate_pub_key(user_dir: UserDirectory, args: GeneratePublicKeyArgs) {
- let private_key_path = user_dir.account_private_key_path(&args.account_name);
- let target_path = args
- .output_dir
- .unwrap_or(current_dir().unwrap())
- .join(format!("{}.pem", args.account_name));
-
- match Command::new("openssl")
- .args([
- "pkey",
- "-in",
- &private_key_path.to_string_lossy(),
- "-pubout",
- "-out",
- &target_path.to_string_lossy(),
- ])
- .status()
- .await
- {
- Ok(status) if status.success() => {
- println!(
- "{}",
- t!(
- "jv.success.account.generate_pub_key",
- export = target_path.display()
- )
- );
- }
- Ok(_) => {
- eprintln!("{}", t!("jv.fail.account.keygen"));
- }
- Err(_) => {
- eprintln!("{}", t!("jv.fail.account.keygen_exec"));
- }
- }
-}
-
-async fn jv_update(update_file_args: UpdateArgs) {
- let local_config = match precheck().await {
- Some(config) => config,
- None => return,
- };
-
- let (pool, ctx, _output) = match build_pool_and_ctx(&local_config).await {
- Some(result) => result,
- None => return,
- };
-
- match proc_update_to_latest_info_action(&pool, ctx, ()).await {
- Err(e) => handle_err(e),
- Ok(result) => {
- if !update_file_args.silent {
- match result {
- UpdateToLatestInfoResult::Success => {
- println!("{}", md(t!("jv.result.update.success")));
- }
- UpdateToLatestInfoResult::AuthorizeFailed(e) => {
- eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e)))
- }
- UpdateToLatestInfoResult::SyncCachedSheetFail(
- sync_cached_sheet_fail_reason,
- ) => match sync_cached_sheet_fail_reason {
- SyncCachedSheetFailReason::PathAlreadyExist(path_buf) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.result.update.fail.sync_cached_sheet_fail.path_already_exist",
- path = path_buf.display()
- ))
- );
- }
- },
- }
- }
- }
- }
-}
-
-async fn jv_direct(args: DirectArgs) {
- let Some(upstream) = args.upstream else {
- println!("{}", md(t!("jv.direct")));
- return;
- };
-
- if !args.confirm {
- println!(
- "{}",
- t!("jv.confirm.direct", upstream = upstream).trim().yellow()
- );
- confirm_hint_or(t!("common.confirm"), || exit(1)).await;
- }
-
- let pool = client_registry::client_action_pool();
- let upstream = match socket_addr_helper::get_socket_addr(&upstream, PORT).await {
- Ok(result) => result,
- Err(e) => {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.parse.str_to_sockaddr",
- str = &upstream.trim(),
- err = e
- ))
- );
- return;
- }
- };
-
- let Some(instance) = connect(upstream).await else {
- // Since connect() function already printed error messages, we only handle the return here
- return;
- };
-
- let ctx = ActionContext::local().insert_instance(instance);
-
- match proc_set_upstream_vault_action(&pool, ctx, upstream).await {
- Err(e) => handle_err(e),
- Ok(result) => match result {
- SetUpstreamVaultActionResult::DirectedAndStained => {
- println!(
- "{}",
- md(t!(
- "jv.result.direct.directed_and_stained",
- upstream = upstream
- ))
- );
- insert_recent_ip_address(upstream.to_string().trim()).await;
- }
- SetUpstreamVaultActionResult::Redirected => {
- println!(
- "{}",
- md(t!("jv.result.direct.redirected", upstream = upstream))
- );
- insert_recent_ip_address(upstream.to_string().trim()).await;
- }
- SetUpstreamVaultActionResult::AlreadyStained => {
- eprintln!("{}", md(t!("jv.result.direct.already_stained")))
- }
- SetUpstreamVaultActionResult::AuthorizeFailed(e) => {
- eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e)))
- }
- SetUpstreamVaultActionResult::RedirectFailed(e) => {
- eprintln!("{}", md(t!("jv.result.direct.redirect_failed", err = e)))
- }
- SetUpstreamVaultActionResult::SameUpstream => {
- eprintln!("{}", md(t!("jv.result.direct.same_upstream")))
- }
- _ => {}
- },
- };
-}
-
-async fn jv_unstain(args: UnstainArgs) {
- let Some(_local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- };
-
- let Ok(mut local_cfg) = LocalConfig::read().await else {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- return;
- };
-
- if !local_cfg.stained() {
- eprintln!("{}", md(t!("jv.fail.unstain")));
- return;
- }
-
- if !args.confirm {
- println!(
- "{}",
- md(t!("jv.confirm.unstain", upstream = local_cfg.vault_addr())).yellow()
- );
- confirm_hint_or(t!("common.confirm"), || exit(1)).await;
- }
-
- local_cfg.unstain();
- match LocalConfig::write(&local_cfg).await {
- Ok(_) => {}
- Err(e) => {
- eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string())));
- return;
- }
- };
-
- println!("{}", md(t!("jv.success.unstain")));
-}
-
-async fn jv_docs(args: DocsArgs) {
- let Some(docs_name) = args.docs_name else {
- if !args.raw {
- println!("{}", md(t!("jv.docs")));
- }
- return;
- };
-
- if docs_name.trim() == "ls" || docs_name.trim() == "list" {
- let docs = documents();
-
- if args.raw {
- docs.iter().for_each(|d| {
- if d.starts_with("docs_") {
- println!("{}", d.trim_start_matches("docs_"))
- }
- });
- return;
- }
-
- println!("{}", md(t!("jv.success.docs.list.header")));
- let mut i = 0;
- for doc in docs {
- if doc.starts_with("docs_") {
- println!(
- "{}",
- md(t!(
- "jv.success.docs.list.item",
- num = i + 1,
- docs_name = doc.trim_start_matches("docs_")
- ))
- );
- i += 1;
- }
- }
- println!("{}", md(t!("jv.success.docs.list.footer")));
-
- return;
- }
-
- let name = format!("docs_{}", snake_case!(docs_name.clone()));
- let Some(document) = document(name) else {
- eprintln!(
- "{}",
- md(t!("jv.fail.docs.not_found", docs_name = docs_name))
- );
- return;
- };
-
- if args.direct {
- println!("{}", document.trim());
- } else {
- let Some(doc_dir) = current_cfg_dir() else {
- eprintln!(
- "{}",
- md(t!("jv.fail.docs.no_doc_dir", docs_name = docs_name))
- );
- return;
- };
- let file = doc_dir.join("DOCS.MD");
- if let Err(e) = show_in_pager(document, file).await {
- eprintln!(
- "{}",
- md(t!(
- "jv.fail.docs.open_editor",
- err = e,
- docs_name = docs_name
- ))
- );
- }
- }
-}
-
-async fn jv_debug_glob(glob_args: DebugGlobArgs) {
- let local_dir = match current_local_path() {
- Some(dir) => dir,
- None => {
- // No, dont print anything
- // eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return;
- }
- };
-
- for path in glob(glob_args.pattern, &local_dir).await.keys() {
- println!("{}", path);
- }
-}
-
-async fn glob(pattern: impl Into<String>, local_dir: &PathBuf) -> BTreeMap<String, ()> {
- let pattern = pattern.into();
- let globber = match get_globber(&pattern, true).await {
- Ok(g) => g,
- Err(_) => match get_globber(&pattern, false).await {
- Ok(g) => g,
- Err(_) => return BTreeMap::new(),
- },
- };
-
- let result = globber.names();
- let base_dir = globber.base();
-
- let relative_path = base_dir
- .strip_prefix(local_dir)
- .unwrap_or(local_dir.as_path());
-
- let mut filtered_paths: Vec<String> = result
- .into_iter()
- .filter_map(|name| Some(relative_path.join(name).display().to_string()))
- .collect();
-
- let path_map: BTreeMap<String, ()> = filtered_paths.drain(..).map(|path| (path, ())).collect();
- path_map
-}
-
-async fn get_globber(
- pattern: impl Into<String>,
- with_current_sheet: bool,
-) -> Result<Globber, std::io::Error> {
- // Build globber
- let globber = Globber::from(pattern.into());
-
- let globber = if with_current_sheet {
- // Get necessary informations
- let Some(local_dir) = current_local_path() else {
- return Err(Error::new(
- std::io::ErrorKind::NotFound,
- "Workspace not found",
- ));
- };
-
- let Ok(local_cfg) = LocalConfig::read_from(local_dir.join(CLIENT_FILE_WORKSPACE)).await
- else {
- return Err(Error::new(
- std::io::ErrorKind::NotFound,
- "Local Config read failed",
- ));
- };
- let Some(sheet_name) = local_cfg.sheet_in_use().clone() else {
- return Err(Error::new(std::io::ErrorKind::NotFound, "No sheet in use"));
- };
-
- let Ok(cached_sheet) = CachedSheet::cached_sheet_data(&sheet_name).await else {
- return Err(Error::new(
- std::io::ErrorKind::NotFound,
- "Cached sheet not found",
- ));
- };
-
- let current_dir = current_dir()?;
-
- if !current_dir.starts_with(&local_dir) {
- return Err(Error::new(
- std::io::ErrorKind::NotFound,
- "Not a local workspace",
- ));
- }
-
- // Sheet mode
- globber.glob(|current_dir| {
- let mut result = HashSet::new();
-
- // First, add local files
- get_local_files(&current_dir, &mut result);
-
- // Start collecting sheet files
- // Check if we're in the workspace directory (get current path relative to local workspace)
- let Ok(relative_path_to_local) = current_dir.strip_prefix(&local_dir) else {
- return result.into_iter().collect();
- };
-
- let mut dirs = HashSet::new();
-
- cached_sheet.mapping().iter().for_each(|(path, _)| {
- let left = relative_path_to_local;
-
- // Skip: files that don't start with the current directory
- let Ok(right) = path.strip_prefix(left) else {
- return;
- };
-
- // File: starts with current directory and doesn't contain "/"
- // (since we already filtered out files that don't start with current directory,
- // here we just check if it contains a path separator)
- let file_name = right.display().to_string();
- if !file_name.contains("/") {
- result.insert(GlobItem::File(file_name));
- } else {
- // Directory: contains separator, take the first part, add to dirs set
- if let Some(first_part) = file_name.split('/').next() {
- dirs.insert(first_part.to_string());
- }
- }
- });
-
- dirs.into_iter().for_each(|dir| {
- result.insert(GlobItem::Directory(dir));
- });
-
- result.into_iter().collect()
- })
- } else {
- // Local mode
- globber.glob(|current| {
- let mut items = HashSet::new();
- get_local_files(&current, &mut items);
- items.iter().cloned().collect()
- })
- }?;
-
- Ok(globber)
-}
-
-fn get_local_files(current: &PathBuf, items: &mut HashSet<GlobItem>) {
- if let Ok(entries) = std::fs::read_dir(&current) {
- for entry in entries.flatten() {
- let path = entry.path();
- let name = path
- .file_name()
- .and_then(|n| n.to_str())
- .map(|s| s.to_string())
- .unwrap_or_default();
-
- if path.is_file() {
- items.insert(GlobItem::File(name));
- } else if path.is_dir() {
- if name != CLIENT_FOLDER_WORKSPACE_ROOT_NAME {
- items.insert(GlobItem::Directory(name));
- }
- }
- }
- }
-}
-
-pub fn handle_err(err: TcpTargetError) {
- eprintln!("{}", md(t!("jv.fail.from_core", err = err)))
-}
-
-async fn connect(upstream: SocketAddr) -> Option<ConnectionInstance> {
- // Create Socket
- let socket = if upstream.is_ipv4() {
- match TcpSocket::new_v4() {
- Ok(socket) => socket,
- Err(_) => {
- eprintln!("{}", t!("jv.fail.create_socket").trim());
- return None;
- }
- }
- } else {
- match TcpSocket::new_v6() {
- Ok(socket) => socket,
- Err(_) => {
- eprintln!("{}", t!("jv.fail.create_socket").trim());
- return None;
- }
- }
- };
-
- // Connect
- let Ok(stream) = socket.connect(upstream).await else {
- eprintln!("{}", t!("jv.fail.connection_failed").trim());
- return None;
- };
-
- Some(ConnectionInstance::from(stream))
-}
-
-// Check if the workspace is stained and has a valid configuration
-// Returns LocalConfig if valid, None otherwise
-async fn precheck() -> Option<LocalConfig> {
- let Some(local_dir) = current_local_path() else {
- eprintln!("{}", t!("jv.fail.workspace_not_found").trim());
- return None;
- };
-
- if let Err(e) = set_current_dir(&local_dir) {
- eprintln!(
- "{}",
- t!(
- "jv.fail.std.set_current_dir",
- dir = local_dir.display(),
- error = e
- )
- );
- return None;
- }
-
- let Ok(local_config) = LocalConfig::read().await else {
- eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config")));
- return None;
- };
-
- if !local_config.stained() {
- eprintln!("{}", md(t!("jv.fail.not_stained")));
- return None;
- }
-
- Some(local_config)
-}
-
-/// Build action pool and context for upstream communication
-/// Returns Some((ActionPool, ActionContext)) if successful, None otherwise
-async fn build_pool_and_ctx(
- local_config: &LocalConfig,
-) -> Option<(ActionPool, ActionContext, Receiver<String>)> {
- let pool = client_registry::client_action_pool();
- let upstream = local_config.upstream_addr();
-
- let instance = connect(upstream).await?;
-
- // Build context and insert instance
- let mut ctx = ActionContext::local().insert_instance(instance);
-
- // Build channel for communication
- let (tx, rx) = mpsc::channel::<String>(100);
- ctx.insert_arc_data(Arc::new(tx));
-
- Some((pool, ctx, rx))
-}
-
-/// Sort paths in a vector of strings.
-/// Paths are strings with structure A/B/C/D/E.
-/// Paths with deeper levels (more '/' segments) are sorted first, followed by paths with shallower levels.
-/// Within the same level, paths are sorted based on the first letter or digit encountered, with the order A-Z > a-z > 0-9.
-fn sort_paths(paths: &mut Vec<String>) {
- quick_sort_with_cmp(paths, false, |a, b| {
- let depth_a = a.matches('/').count();
- let depth_b = b.matches('/').count();
-
- if depth_a != depth_b {
- return if depth_a > depth_b { -1 } else { 1 };
- }
- a.cmp(b) as i32
- });
-}
-
-/// Get paths that exist in the Cached Sheet under the current directory
-fn mapping_names_here(
- current_dir: &PathBuf,
- local_dir: &PathBuf,
- cached_sheet: &SheetData,
-) -> std::collections::BTreeMap<String, Option<SheetMappingMetadata>> {
- let Ok(relative_path) = current_dir.strip_prefix(local_dir) else {
- return std::collections::BTreeMap::new();
- };
-
- // Collect files directly under current directory
- let files_here: std::collections::BTreeMap<String, Option<SheetMappingMetadata>> = cached_sheet
- .mapping()
- .iter()
- .filter_map(|(f, mapping)| {
- // Check if the file is directly under the current directory
- f.parent()
- .filter(|&parent| parent == relative_path)
- .and_then(|_| f.file_name())
- .and_then(|name| name.to_str())
- .map(|s| (s.to_string(), Some(mapping.clone())))
- })
- .collect();
-
- // Collect directories that appear in the mapping
- let mut dirs_set = std::collections::BTreeSet::new();
-
- for (f, _mapping) in cached_sheet.mapping().iter() {
- // Get all parent directories of the file relative to the current directory
- let mut current = f.as_path();
-
- while let Some(parent) = current.parent() {
- if parent == relative_path {
- // This is a parent directory, not the file itself
- if current != f.as_path() {
- if let Some(dir_name) = current.file_name() {
- if let Some(dir_str) = dir_name.to_str() {
- dirs_set.insert(format!("{}/", dir_str));
- }
- }
- }
- break;
- }
- current = parent;
- }
- }
-
- // Filter out directories that are actually files
- let filtered_dirs: std::collections::BTreeMap<String, Option<SheetMappingMetadata>> = dirs_set
- .into_iter()
- .filter(|dir_with_slash| {
- let dir_name = dir_with_slash.trim_end_matches('/');
- !files_here.contains_key(dir_name)
- })
- .map(|dir_name| (dir_name, None))
- .collect();
-
- // Combine results
- let mut result = files_here;
- result.extend(filtered_dirs);
- result
-}
-
-/// Trims the content, takes the first line, and truncates it to a display width of 24 characters.
-/// If the display width exceeds 24, it truncates and adds "...".
-fn truncate_first_line(content: String) -> String {
- let trimmed = content.trim();
- let first_line = trimmed.lines().next().unwrap_or("");
- let display_len = display_width(first_line);
- if display_len > 24 {
- let mut truncated = String::new();
- let mut current_len = 0;
- for ch in first_line.chars() {
- let ch_width = display_width(&ch.to_string());
- if current_len + ch_width > 24 {
- break;
- }
- truncated.push(ch);
- current_len += ch_width;
- }
- truncated.push_str("...");
- truncated
- } else {
- first_line.to_string()
- }
-}
-
-fn print_json<T: serde::Serialize>(obj: T, pretty: bool) {
- let result = if pretty {
- serde_json::to_string_pretty(&obj)
- } else {
- serde_json::to_string(&obj)
- };
- println!("{}", result.unwrap_or("{}".to_string()));
-}