EnableExplicit ; ============================================================================= ;-MODULE TRANSLATE ; ============================================================================= DeclareModule Translate Declare.s T(key.s, DefaultLng.s = "") Declare LoadLanguage(filename.s) Declare SaveLanguage(filename.s = "DefaultLanguage.ini") ; ← valeur par défaut EndDeclareModule Module Translate ; =============================================== ; Système Multilingue Minimaliste pour PureBasic ; =============================================== Global NewMap Translations.s() Procedure.s T(key.s, DefaultLng.s = "") If FindMapElement(Translations(), key) ProcedureReturn Translations(key) Else If DefaultLng <> "" Translations(key) = DefaultLng Else Translations(key) = "!!!" + key EndIf EndIf ProcedureReturn Translations(key) EndProcedure ; =============================================== ; Chargement d'un fichier de langue (clé=valeur) ; =============================================== Procedure LoadLanguage(filename.s) Protected file, line$, pos, key$, value$ file = ReadFile(#PB_Any, filename) If file While Eof(file) = 0 line$ = Trim(ReadString(file)) If line$ <> "" And Left(line$, 1) <> ";" And Left(line$, 1) <> "#" pos = FindString(line$, "=", 1) If pos > 0 key$ = Trim(Left(line$, pos - 1)) value$ = Trim(Mid(line$, pos + 1)) value$ = ReplaceString(value$, "\n", Chr(10)) value$ = ReplaceString(value$, "\t", Chr(9)) Translations(key$) = value$ EndIf EndIf Wend CloseFile(file) ProcedureReturn #True EndIf ProcedureReturn #False EndProcedure ; --------------------------------------------------------------------------- ; ---------------------- UTILITAIRES INTERNES ------------------------------ ; --------------------------------------------------------------------------- ; Supprime le commentaire ';' en fin de ligne (en respectant les chaînes "...") Procedure.s _StripLineComment(line$) Protected out$, i, c$, inString = #False, q$ = Chr(34) For i = 1 To Len(line$) c$ = Mid(line$, i, 1) If c$ = q$ ; Gérer "" (guillemet échappé style BASIC) If inString And i < Len(line$) And Mid(line$, i + 1, 1) = q$ out$ + q$ + q$ i + 1 Continue EndIf inString ! 1 out$ + c$ ElseIf c$ = ";" And inString = #False Break ; commentaire atteint Else out$ + c$ EndIf Next ProcedureReturn out$ EndProcedure ; Détecte un chemin absolu rudimentaire (Windows / Unix-like) Procedure.i _IsAbsolutePath(path$) If Len(path$) = 0 : ProcedureReturn #False : EndIf If Mid(path$, 2, 1) = ":" : ProcedureReturn #True : EndIf ; "C:\..." If Left(path$, 1) = "\" Or Left(path$, 1) = "/" : ProcedureReturn #True : EndIf ProcedureReturn #False EndProcedure ; Résout un include relatif par rapport au dossier du fichier courant Procedure.s _ResolveInclude(baseFile$, include$) Protected baseDir$ = GetPathPart(baseFile$) If _IsAbsolutePath(include$) ProcedureReturn include$ Else ProcedureReturn baseDir$+#PS$ + include$ EndIf EndProcedure ; Analyse un fichier source : récupère T("KEY","TEXT") et suit les Include* Procedure _ScanSourceFile(file$, Map outPairs.s(), Map visited.b()) Protected fileID, all$, line$, clean$, tmp$, pos1, pos2, inc$, q$ = Chr(34) If FindMapElement(visited(), UCase(file$)) : ProcedureReturn : EndIf If FileSize(file$) < 0 : ProcedureReturn : EndIf visited(UCase(file$)) = 1 fileID = ReadFile(#PB_Any, file$) If fileID = 0 : ProcedureReturn : EndIf While Eof(fileID) = 0 line$ = ReadString(fileID) clean$ = _StripLineComment(line$) all$ + clean$ + #CRLF$ ; Détection IncludeFile / XIncludeFile "xxx" tmp$ = LCase(Trim(clean$)) If Left(tmp$, 11) = "includefile" Or Left(tmp$, 12) = "xincludefile" pos1 = FindString(clean$, q$, 1) If pos1 pos2 = FindString(clean$, q$, pos1 + 1) If pos2 > pos1 inc$ = Mid(clean$, pos1 + 1, pos2 - pos1 - 1) inc$ = _ResolveInclude(file$, inc$) _ScanSourceFile(inc$, outPairs(), visited()) EndIf EndIf EndIf Wend CloseFile(fileID) ; Extraction des T("KEY","TEXT") ; - sensible aux guillemets échappés par "" (gérés plus haut partiellement) If CreateRegularExpression(1, ~"(?i)T\\s*\\(\\s*\"([^\"]+)\"\\s*,\\s*\"([^\"]*)\"\\s*\\)") If ExamineRegularExpression(1, all$) Protected k$, v$ While NextRegularExpressionMatch(1) k$ = RegularExpressionGroup(1, 1) v$ = RegularExpressionGroup(1, 2) ; Si la clé n'existe pas encore, on la retient (évite doublons) If FindMapElement(outPairs(), k$) = 0 outPairs(k$) = v$ EndIf Wend EndIf FreeRegularExpression(1) EndIf EndProcedure ; =============================================== ; Sauvegarde des T("KEY","TEXT") -> DefaultLanguage.ini ; =============================================== Procedure SaveLanguage(filename.s = "DefaultLanguage.ini") Protected outFile$ = filename If Trim(outFile$) = "" : outFile$ = "DefaultLanguage.ini" : EndIf ; Point de départ : fichier compilé (si module inclus, appeler depuis le main) Protected mainSource$ = #PB_Compiler_File NewMap pairs.s() NewMap visited.b() _ScanSourceFile(mainSource$, pairs(), visited()) If MapSize(pairs()) = 0 ; Rien trouvé -> on retourne #False pour signaler l'absence d'extractions ProcedureReturn #False EndIf ; Tri des clés pour un INI stable NewList keys.s() ForEach pairs() AddElement(keys()) keys() = MapKey(pairs()) Next SortList(keys(), #PB_Sort_Ascending | #PB_Sort_NoCase) Protected f = CreateFile(#PB_Any, outFile$) If f WriteStringN(f, "; Fichier de langue généré automatiquement", #PB_UTF8) WriteStringN(f, "; Source analysée: " + mainSource$, #PB_UTF8) WriteStringN(f, "; Format: Key = TEXT", #PB_UTF8) WriteStringN(f, "", #PB_UTF8) ForEach keys() WriteStringN(f, keys() + " = " + pairs(keys()), #PB_UTF8) Next CloseFile(f) ProcedureReturn #True EndIf ProcedureReturn #False EndProcedure EndModule UseModule Translate SaveLanguage() ; ============================================================================= ;-STRUCTURES / STRUCTURES ; ============================================================================= ; To use with RunExe() Helper Structure runProgramCallStruct exec.s ; program to execute / Programme à executer args.s ; Command arguments / Arguments de la commande workdir.s ; Working directory / Répertoire de travail output.s ; Standard output / Sortie standard errors.s ; Error output / Sortie d'erreur exitcode.i ; Exit code / Code de sortie EndStructure Structure FilesStruct name.s status.s statusDescription.s importance.i EndStructure Structure mainStruct gitCall.runProgramCallStruct List Files.FilesStruct() ;Helpers Info gitVersion$ IsRepository.b hasRemoteUrl.b EndStructure Global main.mainStruct ; ============================================================================= ;-Helper ; ============================================================================= Procedure Max(nb1, nb2) Protected Result.l If nb1 > nb2 Result = nb1 Else Result = nb2 EndIf ProcedureReturn Result EndProcedure Procedure Min(nb1, nb2) Protected Result.l If nb1 < nb2 Result = nb1 Else Result = nb2 EndIf ProcedureReturn Result EndProcedure Procedure.s SupTrim(text.s) If text = "" : ProcedureReturn "" : EndIf Protected i.i = 1 Protected j.i = Len(text) Protected c.l ; Trim gauche : avancer tant que <= 32 While i <= j c = Asc(Mid(text, i, 1)) If c <= 32 i + 1 Else Break EndIf Wend ; Trim droite : reculer tant que <= 32 While j >= i c = Asc(Mid(text, j, 1)) If c <= 32 j - 1 Else Break EndIf Wend If j < i ProcedureReturn "" Else ProcedureReturn Mid(text, i, j - i + 1) EndIf EndProcedure Procedure.i RunExe(*call.runProgramCallStruct) Protected prg.i, line$, lineError$, out$, err$ If FileSize(*call\workdir)<>-2 ; Correct bad git detect if bad workdir *call\workdir=GetCurrentDirectory() EndIf Debug "[RunExe] "+*call\exec+" "+*call\args prg = RunProgram(*call\exec, *call\args, *call\workdir, #PB_Program_Open | #PB_Program_Read | #PB_Program_Error | #PB_Program_Hide) If prg = 0 *call\output = "" *call\errors = "Impossible de lancer '" + *call\exec+"'." *call\exitcode = -1 ProcedureReturn *call\exitcode EndIf ; Read output while program is running / Lire la sortie pendant l'exécution While ProgramRunning(prg) While AvailableProgramOutput(prg) line$ = ReadProgramString(prg) If line$ <> "" : out$ + line$ + #LF$ : EndIf lineError$ = ReadProgramError(prg) If lineError$ <> "" : err$ + lineError$ + #LF$ : EndIf Wend Delay(5) Wend ; Read remaining output / Lire le reste de la sortie While AvailableProgramOutput(prg) line$ = ReadProgramString(prg) If line$ <> "" : out$ + line$ + #LF$ : EndIf Wend ; Read remaining errors / Lire le reste des erreurs Repeat line$ = ReadProgramError(prg) If line$ = "" : Break : EndIf err$ + line$ + #LF$ ForEver *call\output = out$ *call\errors = err$ *call\exitcode = ProgramExitCode(prg) CloseProgram(prg) Debug "[Exec] code=" + Str(*call\exitcode) If *call\output <> "" : Debug "[std]" + *call\output : EndIf If *call\errors <> "" : Debug "[stderr] " + *call\errors : EndIf ProcedureReturn *call\exitcode EndProcedure ; ============================================================================= ;-GUI ; ============================================================================= ; ----------------------------------------------------------------------------- ; UI CONSTANTS / CONSTANTES UI ; ----------------------------------------------------------------------------- #AppTitle$ = "Git Companion" #UI_WinStartW = 1280 #UI_WinStartH = 720 #UI_WinMinW = 980 #UI_WinMinH = 600 #UI_Margin = 10 ; outer margin / marge extérieure #UI_Inset = 10 ; inner padding / marge interne #UI_RowH = 30 ; default row height / hauteur ligne #UI_BtnW = 100 #UI_PanelStartW = 920 ; initial left panel width / largeur initiale du panel de gauche #UI_PanelMinW = 740 ; minimum left panel width / largeur mini du panel #UI_HelpMinW = 240 ; minimum help area width / largeur mini de l’aide #UI_FrameHeaderH = 30 ; frame header area (title) / bandeau titre du frame ; ----------------------------------------------------------------------------- ; ENUMS: WINDOWS & GADGETS / ÉNUMÉRATIONS : FENÊTRES & GADGETS ; ----------------------------------------------------------------------------- Enumeration WindowIDs #WinMain EndEnumeration Enumeration GadgetsIDs ; Core layout #GID_Panel #GID_HelpFrame #GID_HelpWeb ; === TAB 1: REPO (Dépôt) === #GID_Tab_Repo ; Local frame #GID_FrmLocal #GID_LblRepo #GID_EdRepo #GID_BtnBrowseRepo #GID_BtnInit #GID_BtnRefresh ; --- NEW: Local branches --- #GID_LblLocalBranch #GID_CbLocalBranch #GID_BtnNewLocalBranch ; Remote frame #GID_FrmRemote #GID_LblRemote #GID_EdRemote ; --- NEW: Remote branches --- #GID_LblRemoteBranch #GID_CbRemoteBranch #GID_BtnNewRemoteBranch #GID_BtnClone #GID_BtnPull #GID_BtnPush #GID_LblRemoteStatus #GID_TxtRemoteStatus #GID_LblLastFetch #GID_TxtLastFetch #GID_BtnVerify #GID_LblAction #GID_TxtAction ; Files frame #GID_FrmFiles #GID_ListStatus #GID_BtnRestore #GID_BtnRename #GID_BtnDelete #GID_BtnIgnore #GID_LblMessage #GID_EdMessage #GID_BtnCommit ; === TAB 2: HISTORY === #GID_Tab_History #GID_ListHistory #GID_BtnRestoreCommit #GID_TxtCommitInfo ; === TAB 3: .gitignore === #GID_Tab_Gitignore #GID_TxtGitIgnore #GID_BtnSaveGitIgnore ; === TAB 4: CONFIG === #GID_Tab_Config #GID_FrmConfig #GID_LblUserName #GID_EdUserName #GID_LblUserEmail #GID_EdUserEmail #GID_LblScope #GID_CbScope #GID_BtnSaveCfg ; --- App settings (langue) --- #GID_FrmApp #GID_LblAppLang #GID_CbAppLang #GID_FrmProxy #GID_ChkProxy ; HTTP #GID_LblHttpSrv #GID_EdHttpSrv #GID_LblHttpPort #GID_EdHttpPort #GID_LblHttpUser #GID_EdHttpUser #GID_LblHttpPass #GID_EdHttpPass ; HTTPS #GID_LblHttpsSrv #GID_EdHttpsSrv #GID_LblHttpsPort #GID_EdHttpsPort #GID_LblHttpsUser #GID_EdHttpsUser #GID_LblHttpsPass #GID_EdHttpsPass #GID_BtnApplyProxy EndEnumeration ; ----------------------------------------------------------------------------- ; SMALL HELPERS / AIDES UTILITAIRES ; ----------------------------------------------------------------------------- Macro RightOf(g) : GadgetX(g) + GadgetWidth(g) : EndMacro Macro BottomOf(g) : GadgetY(g) + GadgetHeight(g) : EndMacro ; ----------------------------------------------------------------------------- ; LAYOUT COMPUTATION / CALCUL DE MISE EN PAGE ; ----------------------------------------------------------------------------- Procedure ResizeGUI() Protected winW = WindowWidth(#WinMain) Protected winH = WindowHeight(#WinMain) If winW < #UI_WinMinW : winW = #UI_WinMinW : EndIf If winH < #UI_WinMinH : winH = #UI_WinMinH : EndIf ; Largeur du panel gauche (avec min/max) Protected panelW = #UI_PanelStartW panelW = Min(panelW, winW - #UI_HelpMinW - #UI_Margin*4) panelW = Max(panelW, #UI_PanelMinW) Protected panelH = winH - #UI_Margin*2 ResizeGadget(#GID_Panel, #UI_Margin, #UI_Margin, panelW, panelH) ; Zone d’aide à droite Protected helpX = #UI_Margin*2 + panelW Protected helpW = winW - helpX - #UI_Margin Protected helpH = panelH ResizeGadget(#GID_HelpFrame, helpX, #UI_Margin, helpW, helpH) Protected innerX = helpX + #UI_Inset Protected innerY = #UI_Margin + #UI_FrameHeaderH Protected innerW = helpW - #UI_Inset*2 Protected innerH = helpH - #UI_Inset - #UI_FrameHeaderH ResizeGadget(#GID_HelpWeb, innerX, innerY, innerW, innerH) ; === Onglet 1 : Local + Remote + Files === ; Étire les frames Local/Remote à la nouvelle largeur et recalcule l'empilement Protected hLocal = GadgetHeight(#GID_FrmLocal) Protected hRemote = GadgetHeight(#GID_FrmRemote) ResizeGadget(#GID_FrmLocal, #UI_Inset, #UI_Inset, panelW - #UI_Inset*2, hLocal) Protected yRemote = BottomOf(#GID_FrmLocal) + #UI_Inset ResizeGadget(#GID_FrmRemote, #UI_Inset, yRemote, panelW - #UI_Inset*2, hRemote) ; --- Ajuste les contrôles internes dépendants de la largeur --- ; LOCAL: Combo branche + bouton Protected yLocalBranch = GadgetY(#GID_CbLocalBranch) Protected cbLocalW = GadgetWidth(#GID_FrmLocal) - #UI_Inset*4 - GadgetWidth(#GID_LblLocalBranch) - 150 If cbLocalW < 120 : cbLocalW = 120 : EndIf ResizeGadget(#GID_CbLocalBranch, RightOf(#GID_LblLocalBranch), yLocalBranch, cbLocalW, #UI_RowH) ResizeGadget(#GID_BtnNewLocalBranch, RightOf(#GID_CbLocalBranch) + #UI_Inset, yLocalBranch, 150, #UI_RowH) ; REMOTE: URL + combo branche distante + bouton Protected yRemoteUrl = GadgetY(#GID_EdRemote) Protected edRemoteW = GadgetWidth(#GID_FrmRemote) - #UI_Inset*2 - GadgetWidth(#GID_LblRemote) - #UI_Inset If edRemoteW < 120 : edRemoteW = 120 : EndIf ResizeGadget(#GID_EdRemote, RightOf(#GID_LblRemote) + #UI_Inset, yRemoteUrl, edRemoteW, #UI_RowH) Protected yRemoteBranch = GadgetY(#GID_CbRemoteBranch) Protected cbRemoteW = GadgetWidth(#GID_FrmRemote) - #UI_Inset*3 - GadgetWidth(#GID_LblRemoteBranch) - 160 If cbRemoteW < 120 : cbRemoteW = 120 : EndIf ResizeGadget(#GID_CbRemoteBranch, RightOf(#GID_LblRemoteBranch), yRemoteBranch, cbRemoteW, #UI_RowH) ResizeGadget(#GID_BtnNewRemoteBranch, RightOf(#GID_CbRemoteBranch) + #UI_Inset, yRemoteBranch, 160, #UI_RowH) ; FILES: prend tout l'espace restant Protected filesTop = BottomOf(#GID_FrmRemote) + #UI_Inset Protected filesH = panelH - filesTop - #UI_Inset If filesH < 150 : filesH = 150 : EndIf ResizeGadget(#GID_FrmFiles, #UI_Inset, filesTop, panelW - #UI_Inset*2, filesH) Protected listH = filesH - #UI_RowH*2 - #UI_Inset*4 - #UI_FrameHeaderH If listH < 100 : listH = 100 : EndIf ResizeGadget(#GID_ListStatus, #UI_Inset, #UI_FrameHeaderH, GadgetWidth(#GID_FrmFiles) - #UI_Inset*2, listH) Protected btnY = #UI_FrameHeaderH + listH + #UI_Inset ResizeGadget(#GID_BtnRestore, #UI_Inset + 10, btnY, 110, #UI_RowH) ResizeGadget(#GID_BtnRename, #UI_Inset + 130, btnY, 110, #UI_RowH) ResizeGadget(#GID_BtnDelete, #UI_Inset + 250, btnY, 110, #UI_RowH) ResizeGadget(#GID_BtnIgnore, #UI_Inset + 370, btnY, 110, #UI_RowH) Protected msgY = btnY + #UI_RowH + #UI_Inset ResizeGadget(#GID_LblMessage, #UI_Inset + 10, msgY + 4, 80, 22) ResizeGadget(#GID_BtnCommit, GadgetWidth(#GID_FrmFiles) - #UI_Inset - 100, msgY - 2, 100, #UI_RowH) ResizeGadget(#GID_EdMessage, #UI_Inset + 95, msgY, GadgetX(#GID_BtnCommit) - (#UI_Inset + 95) - #UI_Inset, #UI_RowH) ; === Onglet 2 : History === Protected histListH = panelH - #UI_Margin*2 - #UI_RowH - 10 - 150 - 10 If histListH < 100 : histListH = 100 : EndIf ResizeGadget(#GID_ListHistory, #UI_Inset, #UI_Inset, panelW - #UI_Inset*2, histListH) ResizeGadget(#GID_BtnRestoreCommit, #UI_Inset, #UI_Inset + histListH + #UI_Inset, 180, #UI_RowH) ResizeGadget(#GID_TxtCommitInfo, #UI_Inset, panelH - #UI_Inset - 150, panelW - #UI_Inset*2, 150) ; === Onglet 3 : .gitignore === Protected gitEdH = panelH - #UI_Inset*4 - #UI_RowH ResizeGadget(#GID_TxtGitIgnore, #UI_Inset, #UI_Inset, panelW - #UI_Inset*2, gitEdH) ResizeGadget(#GID_BtnSaveGitIgnore, #UI_Inset, #UI_Inset + gitEdH + #UI_Inset, 100, #UI_RowH) ; Onglet 4 : statique (inchangé) EndProcedure ; ----------------------------------------------------------------------------- ; Tooltips & i18n ; FR: Appeler après la création des gadgets, puis après tout changement de langue ; EN: Call after creating gadgets, then after any language change. ; ----------------------------------------------------------------------------- Procedure ApplyToolTips() ; --- Dépôt / Local repo --- GadgetToolTip(#GID_BtnInit, T("tip.init", "Initialiser un dépôt Git ici")) GadgetToolTip(#GID_BtnRefresh, T("tip.refresh", "Rafraîchir la liste des fichiers et l’état Git")) GadgetToolTip(#GID_EdRepo, T("tip.repo.path", "Chemin du dossier projet (workdir)")) ; --- Remote / Branch --- GadgetToolTip(#GID_EdRemote, T("tip.remote", "URL du remote (ex.: https://... ou git@host:org/repo.git)")) ; Local branches GadgetToolTip(#GID_CbLocalBranch, T("tip.branch.local.select", "Choisir la branche locale active")) GadgetToolTip(#GID_BtnNewLocalBranch,T("tip.branch.local.new", "Créer une nouvelle branche locale")) ; Remote branches GadgetToolTip(#GID_CbRemoteBranch, T("tip.branch.remote.select", "Choisir une branche distante (remote-tracking)")) GadgetToolTip(#GID_BtnNewRemoteBranch,T("tip.branch.remote.new", "Créer une branche sur le dépôt distant / publier")) ; --- Remote / Branch --- GadgetToolTip(#GID_EdRemote, T("tip.remote", "URL du remote (ex.: https://... ou git@host:org/repo.git)")) GadgetToolTip(#GID_BtnClone, T("tip.clone", "Cloner depuis l’URL remote")) GadgetToolTip(#GID_BtnPull, T("tip.pull", "Récupérer et fusionner depuis le remote")) GadgetToolTip(#GID_BtnPush, T("tip.push", "Envoyer vos commits sur le remote")) ; --- Fichiers & actions locales / Files & local actions --- GadgetToolTip(#GID_ListStatus, T("tip.files.list", "Fichiers du dépôt :\n- cochez pour préparer un commit\n- sélectionnez pour agir")) GadgetToolTip(#GID_BtnRestore, T("tip.restore", "Restaurer les fichiers sélectionnés")) GadgetToolTip(#GID_BtnRename, T("tip.rename", "Renommer les fichiers sélectionnés")) GadgetToolTip(#GID_BtnDelete, T("tip.delete", "Supprimer les fichiers sélectionnés")) GadgetToolTip(#GID_BtnIgnore, T("tip.ignore", "Ajouter/retirer les fichiers sélectionnés dans .gitignore")) ; --- Commit --- GadgetToolTip(#GID_EdMessage, T("tip.message", "Message du commit")) GadgetToolTip(#GID_BtnCommit, T("tip.commit", "Committer les fichiers cochés avec le message")) ; --- History --- GadgetToolTip(#GID_ListHistory, T("tip.history", "Historique des commits")) GadgetToolTip(#GID_BtnRestoreCommit, T("tip.history.restore", "Restaurer / checkout le commit sélectionné")) ; --- .gitignore --- GadgetToolTip(#GID_TxtGitIgnore, T("tip.gitignore.edit", "Éditeur du .gitignore")) GadgetToolTip(#GID_BtnSaveGitIgnore, T("tip.gitignore.save", "Sauvegarder le .gitignore")) ; --- Config --- GadgetToolTip(#GID_EdUserName, T("tip.cfg.username", "Nom d’utilisateur Git (user.name)")) GadgetToolTip(#GID_EdUserEmail, T("tip.cfg.useremail", "Email Git (user.email)")) GadgetToolTip(#GID_CbScope, T("tip.cfg.scope", "Portée de la configuration (Local/Global/System)")) GadgetToolTip(#GID_BtnSaveCfg, T("tip.cfg.save", "Enregistrer la configuration Git")) ; --- Langue --- GadgetToolTip(#GID_CbAppLang, T("tip.app.lang", "Langue de l’application")) ; --- Proxy --- GadgetToolTip(#GID_ChkProxy, T("tip.proxy.enable", "Activer/désactiver la configuration proxy")) GadgetToolTip(#GID_EdHttpSrv, T("tip.proxy.http.server", "Serveur HTTP proxy")) GadgetToolTip(#GID_EdHttpPort, T("tip.proxy.http.port", "Port HTTP proxy")) GadgetToolTip(#GID_EdHttpUser, T("tip.proxy.http.user", "Login HTTP proxy")) GadgetToolTip(#GID_EdHttpPass, T("tip.proxy.http.pass", "Mot de passe HTTP proxy")) GadgetToolTip(#GID_EdHttpsSrv, T("tip.proxy.https.server", "Serveur HTTPS proxy")) GadgetToolTip(#GID_EdHttpsPort, T("tip.proxy.https.port", "Port HTTPS proxy")) GadgetToolTip(#GID_EdHttpsUser, T("tip.proxy.https.user", "Login HTTPS proxy")) GadgetToolTip(#GID_EdHttpsPass, T("tip.proxy.https.pass", "Mot de passe HTTPS proxy")) GadgetToolTip(#GID_BtnApplyProxy,T("tip.proxy.apply", "Appliquer la configuration proxy dans Git")) ; --- Help (optionnel) --- GadgetToolTip(#GID_HelpWeb, T("tip.help.web", "Zone d’aide (documentation / rendu HTML)")) EndProcedure ; ----------------------------------------------------------------------------- ; BUILD GUI / CONSTRUCTION DE L’INTERFACE ; ----------------------------------------------------------------------------- Procedure OpenGUI() UseModule Translate Define repoDir$ = GetCurrentDirectory() If OpenWindow(#WinMain, 0, 0, #UI_WinStartW, #UI_WinStartH, #AppTitle$, #PB_Window_SystemMenu | #PB_Window_ScreenCentered | #PB_Window_SizeGadget) Protected panelH = WindowHeight(#WinMain) - #UI_Margin*2 ; LEFT: Panel with tabs / Panneau avec onglets PanelGadget(#GID_Panel, #UI_Margin, #UI_Margin, #UI_PanelStartW, panelH) ; =========================================================================== ; TAB 1: REPO / DÉPÔT ; =========================================================================== AddGadgetItem(#GID_Panel, -1, T("Tab.Repo", "Dépôt")) ; ---- Frame: Local repository ------------------------------------------------ FrameGadget(#GID_FrmLocal, #UI_Inset, #UI_Inset, GadgetWidth(#GID_Panel) - #UI_Inset*2, #UI_FrameHeaderH + #UI_RowH*3 + #UI_Inset*3, T("Local.FrameTitle", "Dépôt local"), #PB_Frame_Container) TextGadget(#GID_LblRepo, #UI_Inset, #UI_FrameHeaderH, 70, #UI_RowH, T("Local.Label.Repo","Dépôt :")) StringGadget(#GID_EdRepo, RightOf(#GID_LblRepo) + #UI_Inset, #UI_FrameHeaderH, GadgetWidth(#GID_FrmLocal) - #UI_Inset*4 - 70 - #UI_BtnW, #UI_RowH, repoDir$) ButtonGadget(#GID_BtnBrowseRepo, RightOf(#GID_EdRepo) + #UI_Inset, #UI_FrameHeaderH, #UI_BtnW, #UI_RowH, T("Local.Button.Browse","Parcourir…")) ButtonGadget(#GID_BtnInit, #UI_Inset, BottomOf(#GID_BtnBrowseRepo) + #UI_Inset, #UI_BtnW, #UI_RowH, T("Local.Button.Init","Init Dépôt")) ButtonGadget(#GID_BtnRefresh, RightOf(#GID_BtnInit) + #UI_Inset, GadgetY(#GID_BtnInit), #UI_BtnW, #UI_RowH, T("Local.Button.Refresh","Rafraîchir")) ; --- NEW: branche locale (sélecteur + bouton) --- Define yLocalBranch = BottomOf(#GID_BtnRefresh) + #UI_Inset TextGadget(#GID_LblLocalBranch, #UI_Inset, yLocalBranch, 120, #UI_RowH, T("Local.Label.Branch","Branche locale :")) ComboBoxGadget(#GID_CbLocalBranch, RightOf(#GID_LblLocalBranch), yLocalBranch, GadgetWidth(#GID_FrmLocal) - #UI_Inset*4 - 120 - 150, #UI_RowH, #PB_ComboBox_Editable) ButtonGadget(#GID_BtnNewLocalBranch, RightOf(#GID_CbLocalBranch) + #UI_Inset, yLocalBranch, 150, #UI_RowH, T("Local.Button.NewBranch","+ Nouvelle branche")) CloseGadgetList() ; ---- Frame: Remote / Branche ------------------------------------------------ Define yRemote = BottomOf(#GID_FrmLocal) + #UI_Inset FrameGadget(#GID_FrmRemote, #UI_Inset, yRemote, GadgetWidth(#GID_Panel) - #UI_Inset*2, #UI_FrameHeaderH + #UI_RowH*5 + #UI_Inset*5, T("Remote.FrameTitle","Distant (remote / branche)"), #PB_Frame_Container) ; Ligne 1: URL du remote TextGadget(#GID_LblRemote, #UI_Inset, #UI_FrameHeaderH, 70, #UI_RowH, T("Remote.Label.Remote","Remote :")) StringGadget(#GID_EdRemote, RightOf(#GID_LblRemote) + #UI_Inset, #UI_FrameHeaderH, 420, #UI_RowH, "") ; Ligne 2: Branche distante (sélecteur + bouton) Define yRemoteBranch = BottomOf(#GID_EdRemote) + #UI_Inset TextGadget(#GID_LblRemoteBranch, #UI_Inset, yRemoteBranch, 130, #UI_RowH, T("Remote.Label.BranchRemote","Branche distante :")) ComboBoxGadget(#GID_CbRemoteBranch, RightOf(#GID_LblRemoteBranch), yRemoteBranch, 220, #UI_RowH) ButtonGadget(#GID_BtnNewRemoteBranch, RightOf(#GID_CbRemoteBranch) + #UI_Inset, yRemoteBranch, 160, #UI_RowH, T("Remote.Button.NewRemoteBranch","+ Nouvelle branche distante")) ; Ligne 3: Actions réseau ButtonGadget(#GID_BtnClone, #UI_Inset, BottomOf(#GID_LblRemoteBranch) + #UI_Inset, #UI_BtnW, #UI_RowH, T("Remote.Button.Clone","Clone")) ButtonGadget(#GID_BtnPull, RightOf(#GID_BtnClone) + #UI_Inset, GadgetY(#GID_BtnClone), #UI_BtnW, #UI_RowH, T("Remote.Button.Pull","Pull")) ButtonGadget(#GID_BtnPush, RightOf(#GID_BtnPull) + #UI_Inset, GadgetY(#GID_BtnClone), #UI_BtnW, #UI_RowH, T("Remote.Button.Push","Push")) ; Ligne 4: Statut & dernière synchro Define yStatus = BottomOf(#GID_BtnClone) + #UI_Inset TextGadget(#GID_LblRemoteStatus, #UI_Inset, yStatus, 70, #UI_RowH, T("Remote.Status.Label","Status :")) TextGadget(#GID_TxtRemoteStatus, RightOf(#GID_LblRemoteStatus), yStatus, 200, #UI_RowH, T("Remote.Status.Checking","Vérification..."), #PB_Text_Border) TextGadget(#GID_LblLastFetch, RightOf(#GID_TxtRemoteStatus) + 15, yStatus, 110, #UI_RowH, T("Remote.LastSync.Label","Dernière sync :")) TextGadget(#GID_TxtLastFetch, RightOf(#GID_LblLastFetch), yStatus, 120, #UI_RowH, "-", #PB_Text_Border) ButtonGadget(#GID_BtnVerify, RightOf(#GID_TxtLastFetch) + 10, yStatus - 2, 90, #UI_RowH, T("Remote.Button.Verify","Vérifier")) ; Ligne 5: Action en cours TextGadget(#GID_LblAction, #UI_Inset, BottomOf(#GID_LblRemoteStatus) + #UI_Inset, 60, 20, T("Remote.Action.Label","Action :")) TextGadget(#GID_TxtAction, RightOf(#GID_LblAction), GadgetY(#GID_LblAction), 300, 20, "-", #PB_Text_Border) CloseGadgetList() ; ---- Frame: Files & changes ------------------------------------------------- Define yFiles = BottomOf(#GID_FrmRemote) + #UI_Inset Define hFiles = panelH - yFiles - #UI_Inset If hFiles < 260 : hFiles = 260 : EndIf ; hauteur mini du frame pour éviter valeurs négatives FrameGadget(#GID_FrmFiles, #UI_Inset, yFiles, GadgetWidth(#GID_Panel) - #UI_Inset*2, hFiles, T("Files.FrameTitle","Fichiers & modifications"), #PB_Frame_Container) ; Hauteur de la liste avec garde-fou (même logique que dans ResizeGUI) Define listH = hFiles - #UI_RowH*2 - #UI_Inset*4 - #UI_FrameHeaderH If listH < 100 : listH = 100 : EndIf ListIconGadget(#GID_ListStatus, #UI_Inset, #UI_FrameHeaderH, GadgetWidth(#GID_FrmFiles) - #UI_Inset*2, listH, T("Files.List.Path","Path"), 300, #PB_ListIcon_CheckBoxes | #PB_ListIcon_MultiSelect | #PB_ListIcon_FullRowSelect | #PB_ListIcon_AlwaysShowSelection) AddGadgetColumn(#GID_ListStatus, 1, T("Files.List.Status","Status"), 80) AddGadgetColumn(#GID_ListStatus, 2, T("Files.List.Description","Description"), 300) ; Ligne boutons sous la liste Define yLocalActions = #UI_FrameHeaderH + listH + #UI_Inset ButtonGadget(#GID_BtnRestore, #UI_Inset + 10, yLocalActions, 110, #UI_RowH, T("LocalActions.Button.Restore","Restaurer")) ButtonGadget(#GID_BtnRename, #UI_Inset + 130, yLocalActions, 110, #UI_RowH, T("LocalActions.Button.Rename","Renommer")) ButtonGadget(#GID_BtnDelete, #UI_Inset + 250, yLocalActions, 110, #UI_RowH, T("LocalActions.Button.Delete","Supprimer")) ButtonGadget(#GID_BtnIgnore, #UI_Inset + 370, yLocalActions, 110, #UI_RowH, T("LocalActions.Button.Ignore","Ignorer")) ; Message de commit Define yMsg = yLocalActions + #UI_RowH + #UI_Inset TextGadget(#GID_LblMessage, #UI_Inset + 10, yMsg + 4, 80, 22, T("Commit.Label.Message","Message :")) StringGadget(#GID_EdMessage, #UI_Inset + 95, yMsg, GadgetWidth(#GID_FrmFiles) - #UI_Inset*2 - 95 - 110, #UI_RowH, "") ButtonGadget(#GID_BtnCommit, GadgetWidth(#GID_FrmFiles) - #UI_Inset - 100, yMsg - 2, 100, #UI_RowH, T("Commit.Button.Commit","Commit")) CloseGadgetList() ; =========================================================================== ; TAB 2: HISTORY ; =========================================================================== AddGadgetItem(#GID_Panel, -1, T("Tabs.History","History")) ListIconGadget(#GID_ListHistory, #UI_Inset, #UI_Inset, GadgetWidth(#GID_Panel) - #UI_Inset*2, panelH - #UI_Inset*2 - #UI_RowH - 10 - 150 - 10, T("History.Headers.Header","Header"), 220, #PB_ListIcon_FullRowSelect) AddGadgetColumn(#GID_ListHistory, 1, T("History.Headers.Date","Date"), 150) AddGadgetColumn(#GID_ListHistory, 2, T("History.Headers.Author","Auteur"), 180) AddGadgetColumn(#GID_ListHistory, 3, T("History.Headers.Files","Fichiers"), 120) AddGadgetColumn(#GID_ListHistory, 4, T("History.Headers.Message","Message"), (GadgetWidth(#GID_Panel) - 2*#UI_Inset) - (220 + 150 + 180 + 120) - 20) ButtonGadget(#GID_BtnRestoreCommit, #UI_Inset, panelH - #UI_Inset - #UI_RowH - 150 - 10, 180, #UI_RowH, T("History.Button.RestoreCommit","Restore This Commit")) EditorGadget(#GID_TxtCommitInfo, #UI_Inset, panelH - #UI_Inset - 150, GadgetWidth(#GID_Panel) - #UI_Inset*2, 150, #PB_Editor_ReadOnly) ; =========================================================================== ; TAB 3: .gitignore ; =========================================================================== AddGadgetItem(#GID_Panel, -1, T("Tabs.Gitignore",".gitignore file")) EditorGadget(#GID_TxtGitIgnore, #UI_Inset, #UI_Inset, GadgetWidth(#GID_Panel) - #UI_Inset*2, panelH - #UI_Inset*4 - #UI_RowH) ButtonGadget(#GID_BtnSaveGitIgnore, #UI_Inset, GadgetY(#GID_TxtGitIgnore) + GadgetHeight(#GID_TxtGitIgnore) + #UI_Inset, 100, #UI_RowH, T("Gitignore.Button.SaveFile","Save File")) ; =========================================================================== ; TAB 4: CONFIG ; =========================================================================== AddGadgetItem(#GID_Panel, -1, T("Tabs.Config","Config")) ; --- Frame: Paramètres de l’application (LANGUE, etc.) --- FrameGadget(#GID_FrmApp, #UI_Inset, #UI_Inset, GadgetWidth(#GID_Panel) - #UI_Inset*2, 90, T("App.FrameTitle","Paramètres de l’application"), #PB_Frame_Container) TextGadget(#GID_LblAppLang, #UI_Inset + 10, #UI_Inset + 35, 120, 22, T("App.Label.Lang","Langue")) ComboBoxGadget(#GID_CbAppLang, #UI_Inset + 140, #UI_Inset + 33, 220, #UI_RowH) AddGadgetItem(#GID_CbAppLang, -1, "Français (fr)") AddGadgetItem(#GID_CbAppLang, -1, "English (en)") SetGadgetState(#GID_CbAppLang, 0) ; défaut: fr CloseGadgetList() ; --- Frame: Configuration Git (identité + portée) --- Define yCfg = BottomOf(#GID_FrmApp) + #UI_Inset FrameGadget(#GID_FrmConfig, #UI_Inset, yCfg, GadgetWidth(#GID_Panel) - #UI_Inset*2, 170, T("Config.FrameTitle","Configuration Git"), #PB_Frame_Container) TextGadget(#GID_LblUserName, #UI_Inset + 10, #UI_Inset + 35, 90, 22, "user.name") StringGadget(#GID_EdUserName, #UI_Inset + 110, #UI_Inset + 33, GadgetWidth(#GID_FrmConfig) - (#UI_Inset*2) - 120, #UI_RowH, "") TextGadget(#GID_LblUserEmail, #UI_Inset + 10, #UI_Inset + 70, 90, 22, "user.email") StringGadget(#GID_EdUserEmail, #UI_Inset + 110, #UI_Inset + 68, GadgetWidth(#GID_FrmConfig) - (#UI_Inset*2) - 120, #UI_RowH, "") TextGadget(#GID_LblScope, #UI_Inset + 10, #UI_Inset + 105, 90, 22, T("Config.Label.Scope","Portée")) ComboBoxGadget(#GID_CbScope, #UI_Inset + 110, #UI_Inset + 103, 180, #UI_RowH) AddGadgetItem(#GID_CbScope, -1, "Local") AddGadgetItem(#GID_CbScope, -1, "System") AddGadgetItem(#GID_CbScope, -1, "Global") SetGadgetState(#GID_CbScope, 0) ButtonGadget(#GID_BtnSaveCfg, GadgetWidth(#GID_FrmConfig) - #UI_Inset - 110, #UI_Inset + 100, 110, #UI_RowH, T("Config.Button.Save","Enregistrer")) CloseGadgetList() ; --- Frame: Proxy HTTP(S) --- Define yProxy = BottomOf(#GID_FrmConfig) + #UI_Inset FrameGadget(#GID_FrmProxy, #UI_Inset, yProxy, GadgetWidth(#GID_Panel) - #UI_Inset*2, 260, T("Proxy.FrameTitle","Proxy HTTP / HTTPS"), #PB_Frame_Container) ; Activation CheckBoxGadget(#GID_ChkProxy, #UI_Inset + 10, #UI_Inset + 30, 280, #UI_RowH, T("Proxy.Enable","Activer le proxy")) SetGadgetState(#GID_ChkProxy, 0) ; --- HTTP row 1 (server/port) TextGadget(#GID_LblHttpSrv, #UI_Inset + 10, #UI_Inset + 70, 100, #UI_RowH, "HTTP serveur") StringGadget(#GID_EdHttpSrv, #UI_Inset + 110, #UI_Inset + 68, 260, #UI_RowH, "") TextGadget(#GID_LblHttpPort, RightOf(#GID_EdHttpSrv) + #UI_Inset, #UI_Inset + 70, 40, #UI_RowH, "Port") StringGadget(#GID_EdHttpPort, RightOf(#GID_LblHttpPort) + #UI_Inset, #UI_Inset + 68, 80, #UI_RowH, "") ; --- HTTP row 2 (user/pass) TextGadget(#GID_LblHttpUser, #UI_Inset + 10, #UI_Inset + 105, 100, #UI_RowH, "HTTP login") StringGadget(#GID_EdHttpUser, #UI_Inset + 110, #UI_Inset + 103, 260, #UI_RowH, "") TextGadget(#GID_LblHttpPass, RightOf(#GID_EdHttpUser) + #UI_Inset, #UI_Inset + 105, 70, #UI_RowH, "Password") StringGadget(#GID_EdHttpPass, RightOf(#GID_LblHttpPass) + #UI_Inset, #UI_Inset + 103, 180, #UI_RowH, "") SetGadgetAttribute(#GID_EdHttpPass, #PB_String_Password, #True) ; --- HTTPS row 1 (server/port) TextGadget(#GID_LblHttpsSrv, #UI_Inset + 10, #UI_Inset + 145, 100, #UI_RowH, "HTTPS serveur") StringGadget(#GID_EdHttpsSrv, #UI_Inset + 110, #UI_Inset + 143, 260, #UI_RowH, "") TextGadget(#GID_LblHttpsPort, RightOf(#GID_EdHttpsSrv) + #UI_Inset, #UI_Inset + 145, 40, #UI_RowH, "Port") StringGadget(#GID_EdHttpsPort, RightOf(#GID_LblHttpsPort) + #UI_Inset, #UI_Inset + 143, 80, #UI_RowH, "") ; --- HTTPS row 2 (user/pass) TextGadget(#GID_LblHttpsUser, #UI_Inset + 10, #UI_Inset + 180, 100, #UI_RowH, "HTTPS login") StringGadget(#GID_EdHttpsUser, #UI_Inset + 110, #UI_Inset + 178, 260, #UI_RowH, "") TextGadget(#GID_LblHttpsPass, RightOf(#GID_EdHttpsUser) + #UI_Inset, #UI_Inset + 180, 70, #UI_RowH, "Password") StringGadget(#GID_EdHttpsPass, RightOf(#GID_LblHttpsPass) + #UI_Inset, #UI_Inset + 178, 180, #UI_RowH, "") SetGadgetAttribute(#GID_EdHttpsPass, #PB_String_Password, #True) ; Bouton Appliquer ButtonGadget(#GID_BtnApplyProxy, GadgetWidth(#GID_FrmProxy) - #UI_Inset - 140, #UI_Inset + 215, 140, #UI_RowH, T("Proxy.Apply","Appliquer proxy")) CloseGadgetList() ; --- End tabs --- CloseGadgetList() ; =========================================================================== ; HELP AREA (RIGHT SIDE) / ZONE AIDE (À DROITE) ; =========================================================================== FrameGadget(#GID_HelpFrame, #UI_Margin*2 + #UI_PanelStartW, #UI_Margin, 400, panelH, T("Help.FrameTitle","Aide"), #PB_Frame_Container) WebViewGadget(#GID_HelpWeb, GadgetX(#GID_HelpFrame) + #UI_Inset, GadgetY(#GID_HelpFrame) + #UI_FrameHeaderH, GadgetWidth(#GID_HelpFrame) - #UI_Inset*2, GadgetHeight(#GID_HelpFrame) - #UI_FrameHeaderH - #UI_Inset) CloseGadgetList() ; Initial placement / placement initial ResizeGUI() ; Tooltips ApplyToolTips() ProcedureReturn #True EndIf ProcedureReturn #False EndProcedure ; ----------------------------------------------------------------------------- ;-GIT FUNCTION ; ----------------------------------------------------------------------------- Procedure Git(param.s) main\gitcall\exec="git" main\gitCall\args=param main\gitCall\workdir=GetCurrentDirectory() ProcedureReturn RunExe(@main\gitCall) EndProcedure Procedure.i GetGitVersion() If Git("--version") = 0 And FindString(main\GitCall\output, "git version", 1) ProcedureReturn #True EndIf ProcedureReturn #False EndProcedure Procedure.i DoGitFetch(remote.s="origin",branch.s="main") If Git("fetch "+remote+" "+branch) = 0 ProcedureReturn #True EndIf ProcedureReturn #False EndProcedure Procedure.i DoCommit() Protected code.i,nb.l=0,i.l Protected args.s = "add" For i = 0 To CountGadgetItems(#GID_ListStatus) - 1 If GetGadgetItemState(#GID_ListStatus, i) & #PB_ListIcon_Checked nb+1 args+" "+Chr(34)+StringField(GetGadgetItemText(#GID_ListStatus, i),1,Chr(10))+Chr(34) EndIf Next i If nb=0 MessageRequester("Git add", "Échec: Vous devez selectionnez des fichiers à ajouter au commit", #PB_MessageRequester_Error) ProcedureReturn #False EndIf If Trim(GetGadgetText(#GID_EdMessage))="" MessageRequester("Git add", "Échec: Vous devez mettre un message", #PB_MessageRequester_Error) ProcedureReturn #False EndIf If GetGadgetText(#GID_CbLocalBranch)<>"" And Git("switch "+GetGadgetText(#GID_CbLocalBranch))<>0 MessageRequester("Git switch", "Échec: " + #LF$ + main\GitCall\errors, #PB_MessageRequester_Error) EndIf If Git(args)<>0 MessageRequester("Git add", "Échec: " + #LF$ + main\GitCall\errors, #PB_MessageRequester_Error) ProcedureReturn #False EndIf ; Commit with message / Valider avec un message args = "commit -m " + Chr(34) + GetGadgetText(#GID_EdMessage) + Chr(34) If Git(args)<>0 MessageRequester("Git commit", "Échec ou rien à valider: " + #LF$ + main\GitCall\errors + #LF$ + main\GitCall\output, #PB_MessageRequester_Warning) ProcedureReturn 0 Else MessageRequester("Git commit", "OK:" + #LF$ + main\GitCall\output, #PB_MessageRequester_Info) EndIf ProcedureReturn #True EndProcedure Procedure AddRemoteRepo(Url.s,name.s="origin") Url=SupTrim(Url) name=SupTrim(name) Protected add.b=#False ;Check if this remote already exists If git("remote get-url "+name) = 0 ;if yes we remove it If Url<>SupTrim(main\Gitcall\output) git("remote remove "+name) add=#True EndIf Else add=#True EndIf ; We add a new remote If add=#True If git("remote add "+name+" "+Url) = 0 ;MessageRequester("Git Remote", "OK:" + #LF$ + main\Gitcall\output, #PB_MessageRequester_Info) ProcedureReturn #True Else MessageRequester("Git config", "Échec: " + #LF$ + main\Gitcall\errors, #PB_MessageRequester_Error) ProcedureReturn #False EndIf ProcedureReturn #True EndIf EndProcedure Procedure DoPush() If Trim(GetGadgetText(#GID_EdRemote))="" MessageRequester("Git Push", "Échec: " + #LF$ + "You Must tu have a remote", #PB_MessageRequester_Error) EndIf AddRemoteRepo(Trim(GetGadgetText(#GID_EdRemote))) Protected target.s=GetGadgetText(#GID_CbRemoteBranch) Protected fp.l=FindString(target,"/",0)-1 If fp>0 target=Left(target,fp)+" "+Right(target,Len(target)-fp-1) EndIf If Git("push -u "+target)=0 MessageRequester("Git Push", "OK:" + #LF$ + main\Gitcall\output+#LF$+main\Gitcall\errors, #PB_MessageRequester_Info) ProcedureReturn #True Else MessageRequester("Git Push", "Échec: " + #LF$ + main\Gitcall\errors, #PB_MessageRequester_Error) ProcedureReturn #False EndIf EndProcedure Procedure.i GetRemoteStatusInfo() ; Récupère les informations de status du remote Protected localBranch.s = GetGadgetText(#GID_CbLocalBranch) Protected remoteBranch.s = GetGadgetText(#GID_CbRemoteBranch) ; Variables initialisées Protected ahead.l = 0 Protected behind.l = 0 Protected isUpToDate.b = #False Protected status.s = "--" Protected needsAction.s = "--" Protected color.i = RGB(60, 60, 60) ; Gris par défaut ; Vérifier que les branches sont définies If Len(Trim(localBranch)) = 0 Or Len(Trim(remoteBranch)) = 0 status = "Branches non sélectionnées" needsAction = "Sélectionner les branches" ProcedureReturn #False EndIf ; Comparer local vs remote If Git("rev-list --left-right --count " + localBranch + "..." + remoteBranch) = 0 Protected counts.s = Trim(main\GitCall\output) ; Vérifier que la sortie contient bien des données If Len(counts) > 0 ; Parser les résultats (séparés par des espaces ou tabs) Protected parts.s = ReplaceString(counts, #TAB$, " ") ; Normaliser les séparateurs parts = ReplaceString(parts, " ", " ") ; Supprimer les espaces multiples ahead = Val(StringField(parts, 1, " ")) behind = Val(StringField(parts, 2, " ")) ; Construire le status et les actions If ahead = 0 And behind = 0 isUpToDate = #True status = "À jour" needsAction = "" color = RGB(0, 128, 0) ; Vert pour "à jour" ElseIf ahead > 0 And behind = 0 status = Str(ahead) + " commit(s) en avance" needsAction = "Push recommandé" color = RGB(0, 100, 200) ; Bleu pour "en avance" ElseIf ahead = 0 And behind > 0 status = Str(behind) + " commit(s) en retard" needsAction = "Pull nécessaire" color = RGB(255, 140, 0) ; Orange pour "en retard" Else status = Str(ahead) + " en avance, " + Str(behind) + " en retard" needsAction = "Branches divergentes - Merge/Rebase requis" color = RGB(255, 69, 0) ; Rouge-orange pour divergence EndIf Else status = "Réponse vide de Git" needsAction = "Vérifier la configuration Git" EndIf Else ; Erreur dans la commande Git status = "Erreur comparaison" needsAction = "Vérifier les noms de branches" color = RGB(255, 0, 0) ; Rouge pour erreur ; Debug pour diagnostiquer l'erreur Debug "Erreur Git: " + main\GitCall\output Debug "Commande: rev-list --left-right --count " + localBranch + "..." + remoteBranch EndIf ; Mettre à jour l'interface SetGadgetText(#GID_TxtRemoteStatus, status) ; Définir la couleur si le gadget le supporte ; (Décommentez si votre version de PureBasic/OS le supporte) ; SetGadgetColor(#GID_LblRemoteStatus, #PB_Gadget_FrontColor, color) ; Action recommandée If needsAction <> "" SetGadgetText(#GID_TxtAction, needsAction) Else SetGadgetText(#GID_TxtAction, "Aucune action nécessaire") EndIf ProcedureReturn #True EndProcedure Procedure.s GetStatusDescription(status.s) Select status Case " " ProcedureReturn "Unmodified" ;Non modifié Case "M " ProcedureReturn "Modified in index";Modifié dans index seulement Case " M" ProcedureReturn "Modified in working tree";Modifié dans working tree seulement Case "MM" ProcedureReturn "Modified in index and working tree";Modifié dans index ET working tree Case "A " ProcedureReturn "Added to index";Ajouté à l'index Case "AM" ProcedureReturn "Added to index, modified in working tree";Ajouté à l'index, modifié dans working tree Case " D" ProcedureReturn "Deleted from working tree";Supprimé du working tree Case "D " ProcedureReturn "Deleted from index";Supprimé de l'index Case "AD" ProcedureReturn "Added to index, deleted from working tree";Ajouté à l'index, supprimé du working tree Case " R" ProcedureReturn "Renamed in working tree";Renommé dans working tree Case "R " ProcedureReturn "Renamed in index";Renommé dans index Case " C" ProcedureReturn "Copied in working tree";Copié dans working tree Case "C " ProcedureReturn "Copied in index";Copié dans index Case "DD" ProcedureReturn "Deleted by both (conflict)";Supprimé dans les deux (conflit) Case "AU" ProcedureReturn "Added by us (conflict)";Ajouté par eux (conflit) Case "UD" ProcedureReturn "Deleted by them (conflict)";Supprimé par eux (conflit) Case "UA" ProcedureReturn "Added by them (conflict)";Ajouté par eux (conflit) Case "DU" ProcedureReturn "Deleted by us (conflict)";Supprimé par nous (conflit) Case "AA" ProcedureReturn "Added by both (conflict)";Ajouté des deux côtés (conflit) Case "UU" ProcedureReturn "Modified by both (conflict)";Modifié des deux côtés (conflit) Case "??" ProcedureReturn "Untracked" ;Non suivi Case "!!" ProcedureReturn "Ignored" ;Ignoré Default ProcedureReturn "Unknown status ->"+status EndSelect EndProcedure Procedure.i GetStatusImportance(status.s) Protected x.s = Left(status, 1) Protected y.s = Mid(status, 2, 1) Protected score.i = 0 ; Conflits d'abord Select status Case "DD","AU","UD","UA","DU","AA","UU" ProcedureReturn 1000 EndSelect If x = "U" Or y = "U" : ProcedureReturn 1000 : EndIf ; Cas simples If status = "??" : ProcedureReturn 300 : EndIf ; Non suivis If status = "!!" : ProcedureReturn 50 : EndIf ; Ignorés If status = " " : ProcedureReturn 0 : EndIf ; Propres ; Index > Worktree If x <> " " : score + 700 : EndIf If y <> " " And y <> "?" And y <> "!" : score + 600 : EndIf ; Raffinement (X puis Y) Select x Case "D" : score + 80 Case "R" : score + 70 Case "A" : score + 60 Case "M" : score + 50 Case "C" : score + 40 Case "T" : score + 30 EndSelect Select y Case "D" : score + 40 Case "R" : score + 35 Case "A" : score + 30 Case "M" : score + 25 Case "C" : score + 20 Case "T" : score + 15 EndSelect ProcedureReturn score EndProcedure Procedure GetGitStatusPocelaine() If Git("status --porcelain --ignored") = 0 ProcedureReturn #True EndIf ProcedureReturn #False EndProcedure ; Parse la sortie de: git status --porcelain -z Procedure ParseStatusPorcelaine(output$) Protected delim$ = Chr(10) Protected total.l = CountString(output$, delim$) + 1 Protected i.l = 1, tok$, sp.l Protected xy$, path1$, path2$, name$, found.b ; Ne PAS vider main\Files(): on met à jour si existe déjà For i=1 To total tok$ = StringField(output$, i, delim$) If tok$ = "" : Continue : EndIf ; tok$ ressemble à: "XY[...score...]␠" If Mid(tok$,3,1)<>" " Continue EndIf xy$ = Left(tok$, 2) ; ex: " M", "R ", " C", "??", "UU", etc. path1$ = Mid(tok$, 4,Len(tok$)-(3)) ; 1er chemin ;TODO check this ; Renomme/copie ? (si X ou Y est R/C, le prochain champ NUL = nouveau chemin) If Left(xy$, 1) = "R" Or Right(xy$, 1) = "R" Or Left(xy$, 1) = "C" Or Right(xy$,1) = "C" If i <= total path2$ = StringField(output$, i, delim$) i + 1 EndIf If path2$ <> "" : name$ = path2$ : Else : name$ = path1$ : EndIf Else name$ = path1$ EndIf ; Normalisation des séparateurs name$ = ReplaceString(name$, "\", "/") ; MAJ si déjà présent, sinon ajout found = #False ForEach main\Files() If main\Files()\name = name$ found = #True main\Files()\status = xy$ main\Files()\statusDescription = GetStatusDescription(xy$) Break EndIf Next If Not found AddElement(main\Files()) main\Files()\name = name$ main\Files()\status = xy$ main\Files()\statusDescription = GetStatusDescription(xy$) EndIf Next Debug "Récupération des status Git (-z) réussie. " + Str(ListSize(main\Files())) + " fichiers (maj/ajout)." ProcedureReturn #True EndProcedure ; Parse la sortie de: git status --porcelain -z Procedure ParseStatusPorcelaine_new(output$) Protected delim$ = Chr(0) Protected pos.l = 1, nextPos.l Protected xy$, path1$, path2$, name$, found.b Protected line$, spacePos.l Debug "Début parsing status -z, taille output: " + Str(Len(output$)) ; Ne PAS vider main\Files(): on met à jour si existe déjà While pos <= Len(output$) ; Trouver la prochaine entrée (délimitée par Chr(0)) nextPos = FindString(output$, delim$, pos) If nextPos = 0 nextPos = Len(output$) + 1 ; Dernière entrée EndIf line$ = Mid(output$, pos, nextPos - pos) ; Passer à la prochaine entrée pos = nextPos + 1 ; Ignorer les lignes vides If Len(line$) < 3 Continue EndIf Debug "Ligne analysée: [" + ReplaceString(line$, Chr(9), "") + "]" ; Format: XY où XY = 2 caractères de status xy$ = Left(line$, 2) ; Le 3ème caractère doit être un espace If Mid(line$, 3, 1) <> " " Debug "Format invalide - pas d'espace en position 3: " + line$ Continue EndIf ; Extraire le chemin (à partir du 4ème caractère) path1$ = Mid(line$, 4) ; Initialiser path2$ pour les cas rename/copy path2$ = "" ; Vérifier si c'est un rename ou copy (R ou C dans le status) ; Format pour rename: "R100 ancien_nomnouveau_nom" If Left(xy$, 1) = "R" Or Left(xy$, 1) = "C" Or Right(xy$, 1) = "R" Or Right(xy$, 1) = "C" ; Pour les renames/copies, il peut y avoir un score (ex: R100) ; Le chemin source se termine par NUL, suivi du chemin destination Protected nullPos.l = FindString(path1$, delim$, 1) If nullPos > 0 ; Séparer les deux chemins Protected tempPath$ = path1$ path1$ = Left(tempPath$, nullPos - 1) ; Le chemin de destination est après le NUL If pos <= Len(output$) nextPos = FindString(output$, delim$, pos) If nextPos = 0 nextPos = Len(output$) + 1 EndIf path2$ = Mid(output$, pos, nextPos - pos) pos = nextPos + 1 EndIf Debug "Rename/Copy détecté: " + path1$ + " -> " + path2$ EndIf ; Pour les renames, utiliser le nouveau nom If path2$ <> "" name$ = path2$ Else name$ = path1$ EndIf Else name$ = path1$ EndIf ; Nettoyer le nom (enlever les guillemets si présents) If Left(name$, 1) = Chr(34) And Right(name$, 1) = Chr(34) ; guillemets doubles name$ = Mid(name$, 2, Len(name$) - 2) EndIf ; Normaliser les séparateurs name$ = ReplaceString(name$, "\", "/") ; Décoder les caractères échappés (\n, \t, \\, etc.) name$ = ReplaceString(name$, "\\", "\") name$ = ReplaceString(name$, "\n", Chr(10)) name$ = ReplaceString(name$, "\t", Chr(9)) Debug "Fichier traité: [" + name$ + "] Status: [" + xy$ + "]" ; Vérifier que le nom n'est pas vide If Len(Trim(name$)) = 0 Debug "Nom de fichier vide, ignoré" Continue EndIf ; MAJ si déjà présent, sinon ajout found = #False ForEach main\Files() If main\Files()\name = name$ found = #True main\Files()\status = xy$ main\Files()\statusDescription = GetStatusDescription(xy$) Debug "Fichier mis à jour: " + name$ Break EndIf Next If Not found AddElement(main\Files()) main\Files()\name = name$ main\Files()\status = xy$ main\Files()\statusDescription = GetStatusDescription(xy$) Debug "Nouveau fichier ajouté: " + name$ EndIf Wend Debug "Récupération des status Git (-z) réussie. " + Str(ListSize(main\Files())) + " fichiers (maj/ajout)." ProcedureReturn #True EndProcedure Procedure IsGitRepository() If Git("status") <> 0 ProcedureReturn #False Else ProcedureReturn #True EndIf EndProcedure Procedure GetGitRemoteUrl(name.s="origin") If Git("remote get-url "+name)=0 ProcedureReturn #True Else ProcedureReturn #False EndIf EndProcedure Procedure GetGitLocalBranch() If Git("branch")=0 ProcedureReturn #True Else ProcedureReturn #False EndIf EndProcedure Procedure.s RefreshLocalBranchesList(Gdt.i) ClearGadgetItems(Gdt) ; Parser ligne par ligne Protected n.l = CountString(main\Gitcall\output, #LF$) + 1 Protected i.l, line.s For i = 1 To n line = StringField(main\Gitcall\output, i, #LF$) line = Trim(line) If Len(line) = 0 Continue ; Ignorer les lignes vides EndIf Protected selectbranch.b=#False If Left(line,1)="*" selectbranch=#True line=Trim(StringField(line,2," ")) EndIf AddGadgetItem(Gdt,i-1,line) If selectbranch=#True SetGadgetState(Gdt,i-1) EndIf Next EndProcedure Procedure GetGitRemoteBranch() If Git("branch -r")=0 ProcedureReturn #True Else ProcedureReturn #False EndIf EndProcedure Procedure.s RefreshRemoteBranchesList(Gdt.i) ClearGadgetItems(Gdt) ; Parser ligne par ligne Protected n.l = CountString(main\Gitcall\output, #LF$) + 1 Protected i.l, line.s, cleanBranchName.s, defaultBranch.s Protected itemIndex.l ; Trouver d'abord la branche par défaut For i = 1 To n line = StringField(main\Gitcall\output, i, #LF$) line = Trim(line) If FindString(line, "->", 1) ; Extraire la branche par défaut depuis "origin/HEAD -> origin/main" defaultBranch = Trim(StringField(line, 2, "->")) If Left(defaultBranch, 8) = "remotes/" defaultBranch = Right(defaultBranch, Len(defaultBranch) - 8) EndIf Break EndIf Next ; Ajouter les branches For i = 1 To n line = StringField(main\Gitcall\output, i, #LF$) line = Trim(line) If Len(line) = 0 Continue ; Ignorer les lignes vides EndIf ; Ignorer la ligne origin/HEAD -> origin/main If FindString(line, "->", 1) Continue EndIf ; Nettoyer le nom de la branche (enlever "remotes/" si présent) cleanBranchName = line If Left(cleanBranchName, 8) = "remotes/" cleanBranchName = Right(cleanBranchName, Len(cleanBranchName) - 8) EndIf itemIndex = CountGadgetItems(Gdt) AddGadgetItem(Gdt, itemIndex, cleanBranchName) ; Sélectionner la branche par défaut If cleanBranchName = defaultBranch SetGadgetState(Gdt, itemIndex) EndIf Next EndProcedure ; --- Helper interne : scanne un dossier et alimente la liste Procedure _ScanFiles(path$, root$="") If root$="":root$=path$:EndIf Protected did.i = ExamineDirectory(#PB_Any, path$, "*") If did = 0 : ProcedureReturn : EndIf While NextDirectoryEntry(did) Protected name$ = DirectoryEntryName(did) If name$ = "." Or name$ = ".." : Continue : EndIf Protected full$ = path$ + name$ Debug full$ If DirectoryEntryType(did) = #PB_DirectoryEntry_Directory ; Ignorer le dépôt interne If LCase(name$) <> ".git" If Right(full$, 1) <> #PS$ : full$ + #PS$ : EndIf _ScanFiles(full$, root$) EndIf Else Protected rel$ = Mid(full$, Len(root$) + 1) rel$ = ReplaceString(rel$, "\", "/") ; chemins normalisés AddElement(main\Files()) main\Files()\name = rel$ main\Files()\status = " " ; 2 espaces = clean main\Files()\statusDescription = "Unmodified" EndIf Wend FinishDirectory(did) EndProcedure Procedure readDirectory() Protected path$ path$ = GetCurrentDirectory() ; Normalise avec un séparateur de fin If Right(path$, 1) <> #PS$ : path$ + #PS$ : EndIf ; On n'efface pas ici pour laisser le choix à l'appelant _ScanFiles(path$) ProcedureReturn ListSize(main\Files()) EndProcedure Procedure RefreshFiles() DoGitFetch() ;TODO add Branch ClearGadgetItems(#GID_ListStatus) ClearList(main\Files()) readDirectory() If main\IsRepository And GetGitVersion() GetGitLocalBranch() RefreshLocalBranchesList(#GID_CbLocalBranch) GetGitStatusPocelaine() ParseStatusPorcelaine(main\gitCall\output) If GetGitRemoteUrl() SetGadgetText(#GID_EdRemote,SupTrim(main\gitCall\output)) main\hasRemoteUrl=#True GetGitRemoteBranch() RefreshRemoteBranchesList(#GID_CbRemoteBranch) Debug "###################################" GetRemoteStatusInfo() Else SetGadgetText(#GID_EdRemote,"") main\hasRemoteUrl=#False ClearGadgetItems(#GID_CbRemoteBranch) EndIf EndIf ;Get Status ForEach main\Files() main\Files()\importance = GetStatusImportance(main\Files()\status) Next ;Sort by Importance SortStructuredList(main\Files(), #PB_Sort_Descending, OffsetOf(FilesStruct\importance), #PB_Integer) ;Display list Protected n.l=n-1 ForEach main\Files() n=n+1 AddGadgetItem(#GID_ListStatus,n,main\Files()\name+Chr(10)+main\Files()\status+Chr(10)+GetStatusDescription(main\Files()\status)) If Right(main\Files()\status,1)="M" SetGadgetItemState(#GID_ListStatus,n,#PB_ListIcon_Checked) EndIf Next EndProcedure ; ----------------------------------------------------------------------------- ;-MAIN ; ----------------------------------------------------------------------------- Procedure Main() SaveLanguage() ;-init Current Work Directory Protected n.l,param$ If CountProgramParameters() <> 0 ; Parse command line arguments / Analyser les arguments de ligne de commande For n = 0 To CountProgramParameters() - 1 param$ = ProgramParameter(n) Select LCase(param$) Case "--project" : If n + 1 < CountProgramParameters() : main\GitCall\workdir = ProgramParameter(n + 1) : EndIf EndSelect Next n EndIf If main\gitCall\workdir="" main\gitCall\workdir=GetCurrentDirectory() EndIf SetCurrentDirectory(main\gitCall\workdir) ;-detect if Git is installed Protected osHint$,title$,msg$ If GetGitVersion() main\gitVersion$=SupTrim(main\gitCall\output) Else CompilerSelect #PB_Compiler_OS CompilerCase #PB_OS_Windows osHint$ = T("git.notfound.win", "Windows : Vérifiez que Git for Windows est installé et que 'git.exe' est dans PATH (ex. C:\Program Files\Git\bin). " + "Dans une invite de commandes, tapez : git --version") CompilerCase #PB_OS_Linux osHint$ = T("git.notfound.linux", "Linux : Installez Git via votre gestionnaire de paquets (ex. Debian/Ubuntu : sudo apt install git, Fedora : sudo dnf install git, Arch : sudo pacman -S git). " + "Vérifiez que 'git' est accessible dans PATH (git --version).") CompilerCase #PB_OS_MacOS osHint$ = T("git.notfound.macos", "macOS : Installez les Xcode Command Line Tools (xcode-select --install) ou Homebrew (brew install git). " + "Assurez-vous que /usr/bin ou /usr/local/bin est dans PATH (git --version).") CompilerEndSelect title$ = T("git.notfound.title", "Git non détecté") msg$ = T("git.notfound.body", "Git n'a pas été détecté sur ce système.") + #CRLF$ + #CRLF$ + osHint$ + #CRLF$ + #CRLF$ +T("git.notfound.exit", "L'application va se fermer.") MessageRequester(title$, msg$, #PB_MessageRequester_Error) End ; Quitter proprement / Exit the app EndIf If OpenGUI() ;refresh main\IsRepository=IsGitRepository() SetWindowTitle(#WinMain,#AppTitle$+" (with "+main\gitVersion$+")") SetGadgetText(#GID_EdRepo, GetCurrentDirectory()) RefreshFiles() ; ----------------------------------------------------------------------------- ;-EVENT LOOP / BOUCLE D'ÉVÉNEMENTS ; ----------------------------------------------------------------------------- Protected.i ev, gid Repeat ev = WaitWindowEvent() Select ev Case #PB_Event_SizeWindow ResizeGUI() Case #PB_Event_Gadget gid = EventGadget() Select gid Case #GID_BtnBrowseRepo ; FR: Sélection dossier / EN: select folder Protected path$ = PathRequester(T("Dlg.SelectRepo","Sélectionnez le dépôt local..."), GetCurrentDirectory(),WindowID(#WinMain)) If path$ <> "" And FileSize(path$)=-2 main\GitCall\workdir=path$ SetGadgetText(#GID_EdRepo, path$) SetCurrentDirectory(path$) main\IsRepository=IsGitRepository() If main\IsRepository=#True RefreshFiles() EndIf ;CreateThread(@RefreshFileList(),0) ;TODO refresh list EndIf Case #GID_BtnInit SetGadgetText(#GID_TxtAction, T("Action.InitRepo","Init dépôt demandé")) ; TODO: call your Git init logic Case #GID_BtnRefresh RefreshFiles() Case #GID_BtnClone SetGadgetText(#GID_TxtAction, T("Action.Clone","Clone demandé")) ; TODO: git clone Case #GID_BtnPull SetGadgetText(#GID_TxtAction, T("Action.Pull","Pull demandé")) ; TODO: git pull Case #GID_BtnPush DoPush() GetRemoteStatusInfo() ; TODO: git push Case #GID_BtnVerify SetGadgetText(#GID_TxtRemoteStatus, T("Remote.Status.Checking","Vérification...")) ; TODO: check remote & update last fetch SetGadgetText(#GID_TxtLastFetch, FormatDate("%yyyy-%mm-%dd %hh:%ii:%ss", Date())) Case #GID_BtnRestore ; TODO: restore Case #GID_BtnRename ; TODO: rename Case #GID_BtnDelete ; TODO: delete Case #GID_BtnIgnore ; TODO: ignore Case #GID_BtnCommit If DoCommit()=#True SetGadgetText(#GID_EdMessage,"") RefreshFiles() EndIf Case #GID_BtnSaveGitIgnore ; TODO: save .gitignore Case #GID_BtnSaveCfg ; TODO: write config EndSelect EndSelect Until ev = #PB_Event_CloseWindow EndIf EndProcedure Main() ; IDE Options = PureBasic 6.21 (Windows - x64) ; CursorPosition = 986 ; FirstLine = 974 ; Folding = ----f-- ; EnableXP ; DPIAware