feat(kanban): detect and merge external file changes in TUI
Add file-change monitoring to the kanban TUI so that tasks added via CLI (or any external process) while the TUI is running are automatically merged into the live view instead of being silently overwritten on save. The implementation has three layers: 1. New FileAge built-in (compiler + runtime): returns the file's mtime as Int64 via stat(), or -1 if the file does not exist. Follows the same pattern as FileExists — symbol table registration, semantic analysis, codegen, and POSIX ABI stub. 2. Merge-aware TBoard (kanban.data): tracks last-known mtime, deleted IDs (so re-reads don't resurrect user-deleted tasks), and provides HasExternalChanges/MergeFromDisk. Save() now auto-merges before writing. ID conflicts (same ID, different task) are resolved by reassigning the in-memory task to a fresh ID. 3. Idle polling in TKanbanUI.Run: every ~2 seconds (20 idle ReadKey cycles at VTIME=1), checks HasExternalChanges and merges new tasks with a status bar notification.
This commit is contained in:
parent
34a1c4feab
commit
43f5e49d9e
|
|
@ -8043,6 +8043,14 @@ begin
|
||||||
Exit(T);
|
Exit(T);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
if SameText(FC.Name,'FileAge') then
|
||||||
|
begin
|
||||||
|
L := EmitExpr(TASTExpr(FC.Args.Items[0]));
|
||||||
|
T := AllocTemp;
|
||||||
|
EmitLine(Format(' %s =l call $_FileAge(l %s)', [T, L]));
|
||||||
|
Exit(T);
|
||||||
|
end;
|
||||||
|
|
||||||
{ Environment and process }
|
{ Environment and process }
|
||||||
if SameText(FC.Name,'GetEnvVar') or SameText(FC.Name,'GetEnvironmentVariable') then
|
if SameText(FC.Name,'GetEnvVar') or SameText(FC.Name,'GetEnvironmentVariable') then
|
||||||
begin
|
begin
|
||||||
|
|
|
||||||
|
|
@ -7238,6 +7238,16 @@ begin
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
if SameText(AExpr.Name, 'FileAge') then
|
||||||
|
begin
|
||||||
|
if AExpr.Args.Count <> 1 then
|
||||||
|
SemanticError('FileAge requires exactly 1 argument', AExpr.Line, AExpr.Col);
|
||||||
|
AnalyseExpr(TASTExpr(AExpr.Args.Items[0]));
|
||||||
|
Result := FTable.TypeInt64;
|
||||||
|
AExpr.ResolvedType := Result;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
if SameText(AExpr.Name, 'ForceDirectories') then
|
if SameText(AExpr.Name, 'ForceDirectories') then
|
||||||
begin
|
begin
|
||||||
if AExpr.Args.Count <> 1 then
|
if AExpr.Args.Count <> 1 then
|
||||||
|
|
|
||||||
|
|
@ -1593,6 +1593,7 @@ begin
|
||||||
Sym := TSymbol.Create('WriteFile', skProcedure, nil); Define(Sym);
|
Sym := TSymbol.Create('WriteFile', skProcedure, nil); Define(Sym);
|
||||||
Sym := TSymbol.Create('AppendFile', skProcedure, nil); Define(Sym);
|
Sym := TSymbol.Create('AppendFile', skProcedure, nil); Define(Sym);
|
||||||
Sym := TSymbol.Create('FileExists', skFunction, FTypeBoolean); Define(Sym);
|
Sym := TSymbol.Create('FileExists', skFunction, FTypeBoolean); Define(Sym);
|
||||||
|
Sym := TSymbol.Create('FileAge', skFunction, FTypeInt64); Define(Sym);
|
||||||
Sym := TSymbol.Create('DeleteFile', skProcedure, nil); Define(Sym);
|
Sym := TSymbol.Create('DeleteFile', skProcedure, nil); Define(Sym);
|
||||||
Sym := TSymbol.Create('CurrentExceptionMessage', skFunction, FTypeString); Define(Sym);
|
Sym := TSymbol.Create('CurrentExceptionMessage', skFunction, FTypeString); Define(Sym);
|
||||||
{ Environment and process }
|
{ Environment and process }
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ type
|
||||||
procedure TestRun_BoolToStr_TrueAndFalse;
|
procedure TestRun_BoolToStr_TrueAndFalse;
|
||||||
procedure TestRun_PlatformConstants;
|
procedure TestRun_PlatformConstants;
|
||||||
procedure TestRun_GetCurrentDir_ReturnsNonEmpty;
|
procedure TestRun_GetCurrentDir_ReturnsNonEmpty;
|
||||||
|
procedure TestRun_FileAge_ReturnsTimestamp;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
@ -551,6 +552,17 @@ const
|
||||||
end.
|
end.
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
SrcFileAge =
|
||||||
|
'''
|
||||||
|
program P;
|
||||||
|
var Age: Int64;
|
||||||
|
begin
|
||||||
|
Age := FileAge(ParamStr(1));
|
||||||
|
WriteLn(Age > 0);
|
||||||
|
WriteLn(FileAge('__no_such_file_xyz__'))
|
||||||
|
end.
|
||||||
|
''';
|
||||||
|
|
||||||
procedure TE2ESysUtilsTests.TestRun_RenameFile_Works;
|
procedure TE2ESysUtilsTests.TestRun_RenameFile_Works;
|
||||||
var
|
var
|
||||||
Output: string;
|
Output: string;
|
||||||
|
|
@ -676,6 +688,35 @@ begin
|
||||||
AssertEquals('GetCurrentDir non-empty', 'True', Trim(Output));
|
AssertEquals('GetCurrentDir non-empty', 'True', Trim(Output));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TE2ESysUtilsTests.TestRun_FileAge_ReturnsTimestamp;
|
||||||
|
var
|
||||||
|
Output: string;
|
||||||
|
RCode: Integer;
|
||||||
|
TmpFile: string;
|
||||||
|
Lines: TStringList;
|
||||||
|
begin
|
||||||
|
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
|
||||||
|
TmpFile := GetTempFileName('', 'blaise_fileage_test');
|
||||||
|
Lines := TStringList.Create;
|
||||||
|
Lines.Add('x');
|
||||||
|
Lines.SaveToFile(TmpFile);
|
||||||
|
Lines.Free;
|
||||||
|
try
|
||||||
|
AssertTrue('compile+run',
|
||||||
|
CompileAndRun(SrcFileAge, Output, RCode, [TmpFile]));
|
||||||
|
Lines := TStringList.Create;
|
||||||
|
try
|
||||||
|
Lines.Text := Trim(Output);
|
||||||
|
AssertEquals('existing file age > 0', 'True', Lines.Strings[0]);
|
||||||
|
AssertEquals('missing file age = -1', '-1', Lines.Strings[1]);
|
||||||
|
finally
|
||||||
|
Lines.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
if FileExists(TmpFile) then DeleteFile(TmpFile);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
initialization
|
initialization
|
||||||
RegisterTest(TE2ESysUtilsTests);
|
RegisterTest(TE2ESysUtilsTests);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ type
|
||||||
procedure TestCodegen_ReadFile_CallsRTL;
|
procedure TestCodegen_ReadFile_CallsRTL;
|
||||||
procedure TestCodegen_WriteFile_CallsRTL;
|
procedure TestCodegen_WriteFile_CallsRTL;
|
||||||
procedure TestCodegen_FileExists_CallsRTL;
|
procedure TestCodegen_FileExists_CallsRTL;
|
||||||
|
procedure TestSemantic_FileAge_ReturnsInt64;
|
||||||
|
procedure TestCodegen_FileAge_CallsRTL;
|
||||||
|
|
||||||
{ ------------------------------------------------------------------ }
|
{ ------------------------------------------------------------------ }
|
||||||
{ GetEnvVar / Exec / Halt (Gap 2 — environment and process) }
|
{ GetEnvVar / Exec / Halt (Gap 2 — environment and process) }
|
||||||
|
|
@ -211,6 +213,15 @@ const
|
||||||
end.
|
end.
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
SrcFileAge =
|
||||||
|
'''
|
||||||
|
program P;
|
||||||
|
var A: Int64;
|
||||||
|
begin
|
||||||
|
A := FileAge('test.txt')
|
||||||
|
end.
|
||||||
|
''';
|
||||||
|
|
||||||
{ Gap 2: environment and process }
|
{ Gap 2: environment and process }
|
||||||
SrcGetEnvVar =
|
SrcGetEnvVar =
|
||||||
'''
|
'''
|
||||||
|
|
@ -501,6 +512,36 @@ begin
|
||||||
AssertTrue('FileExists calls _FileExists', Pos('_FileExists', IR) > 0);
|
AssertTrue('FileExists calls _FileExists', Pos('_FileExists', IR) > 0);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TSelfHostingTests.TestSemantic_FileAge_ReturnsInt64;
|
||||||
|
var
|
||||||
|
Lex: TLexer;
|
||||||
|
Par: TParser;
|
||||||
|
SA: TSemanticAnalyser;
|
||||||
|
Prog: TProgram;
|
||||||
|
Ass: TAssignment;
|
||||||
|
begin
|
||||||
|
Lex := TLexer.Create(SrcFileAge);
|
||||||
|
Par := TParser.Create(Lex);
|
||||||
|
Prog := Par.Parse;
|
||||||
|
Par.Free;
|
||||||
|
Lex.Free;
|
||||||
|
SA := TSemanticAnalyser.Create;
|
||||||
|
SA.Analyse(Prog);
|
||||||
|
SA.Free;
|
||||||
|
Ass := TAssignment(Prog.Block.Stmts[0]);
|
||||||
|
AssertEquals('FileAge returns Int64',
|
||||||
|
Ord(tyInt64), Ord(Ass.Expr.ResolvedType.Kind));
|
||||||
|
Prog.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSelfHostingTests.TestCodegen_FileAge_CallsRTL;
|
||||||
|
var
|
||||||
|
IR: string;
|
||||||
|
begin
|
||||||
|
IR := GenIR(SrcFileAge);
|
||||||
|
AssertTrue('FileAge calls _FileAge', Pos('_FileAge', IR) > 0);
|
||||||
|
end;
|
||||||
|
|
||||||
{ ------------------------------------------------------------------ }
|
{ ------------------------------------------------------------------ }
|
||||||
{ Gap 2: environment and process }
|
{ Gap 2: environment and process }
|
||||||
{ ------------------------------------------------------------------ }
|
{ ------------------------------------------------------------------ }
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ type
|
||||||
function ReadFile(const APath: string): string; virtual; abstract;
|
function ReadFile(const APath: string): string; virtual; abstract;
|
||||||
procedure WriteFile(const APath, AContent: string); virtual; abstract;
|
procedure WriteFile(const APath, AContent: string); virtual; abstract;
|
||||||
procedure AppendFile(const APath, AContent: string); virtual; abstract;
|
procedure AppendFile(const APath, AContent: string); virtual; abstract;
|
||||||
|
function FileAge(const APath: string): Int64; virtual; abstract;
|
||||||
|
|
||||||
{ Directory operations }
|
{ Directory operations }
|
||||||
function DirectoryExists(const APath: string): Boolean; virtual; abstract;
|
function DirectoryExists(const APath: string): Boolean; virtual; abstract;
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ type
|
||||||
function ReadFile(const APath: string): string; override;
|
function ReadFile(const APath: string): string; override;
|
||||||
procedure WriteFile(const APath, AContent: string); override;
|
procedure WriteFile(const APath, AContent: string); override;
|
||||||
procedure AppendFile(const APath, AContent: string); override;
|
procedure AppendFile(const APath, AContent: string); override;
|
||||||
|
function FileAge(const APath: string): Int64; override;
|
||||||
|
|
||||||
{ Directory operations }
|
{ Directory operations }
|
||||||
function DirectoryExists(const APath: string): Boolean; override;
|
function DirectoryExists(const APath: string): Boolean; override;
|
||||||
|
|
@ -220,6 +221,7 @@ function _RenameFile(OldPath, NewPath: Pointer): Integer;
|
||||||
function _ReadFile(Path: Pointer): Pointer;
|
function _ReadFile(Path: Pointer): Pointer;
|
||||||
procedure _WriteFile(Path, Content: Pointer);
|
procedure _WriteFile(Path, Content: Pointer);
|
||||||
procedure _AppendFile(Path, Content: Pointer);
|
procedure _AppendFile(Path, Content: Pointer);
|
||||||
|
function _FileAge(Path: Pointer): Int64;
|
||||||
|
|
||||||
{ Directory operations }
|
{ Directory operations }
|
||||||
function _DirectoryExists(Path: Pointer): Integer;
|
function _DirectoryExists(Path: Pointer): Integer;
|
||||||
|
|
@ -499,6 +501,14 @@ begin
|
||||||
libc_close(Fd);
|
libc_close(Fd);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TRtlPlatformPosix.FileAge(const APath: string): Int64;
|
||||||
|
var
|
||||||
|
St: TStatBuf;
|
||||||
|
begin
|
||||||
|
if libc_stat(StrData(Pointer(APath)), @St) <> 0 then begin Result := -1; Exit end;
|
||||||
|
Result := St.Mtime;
|
||||||
|
end;
|
||||||
|
|
||||||
{ ================================================================== }
|
{ ================================================================== }
|
||||||
{ TRtlPlatformPosix — Directory operations }
|
{ TRtlPlatformPosix — Directory operations }
|
||||||
{ ================================================================== }
|
{ ================================================================== }
|
||||||
|
|
@ -1205,6 +1215,11 @@ begin
|
||||||
GRtlPlatform.AppendFile(string(PChar(Path)), string(PChar(Content)));
|
GRtlPlatform.AppendFile(string(PChar(Path)), string(PChar(Content)));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function _FileAge(Path: Pointer): Int64;
|
||||||
|
begin
|
||||||
|
Result := GRtlPlatform.FileAge(string(PChar(Path)));
|
||||||
|
end;
|
||||||
|
|
||||||
function _DirectoryExists(Path: Pointer): Integer;
|
function _DirectoryExists(Path: Pointer): Integer;
|
||||||
begin
|
begin
|
||||||
if GRtlPlatform.DirectoryExists(string(PChar(Path))) then Result := 1 else Result := 0;
|
if GRtlPlatform.DirectoryExists(string(PChar(Path))) then Result := 1 else Result := 0;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ uses Classes, Contnrs, StrUtils, DateUtils;
|
||||||
type
|
type
|
||||||
TTaskStatus = (tsTodo, tsInProgress, tsDone);
|
TTaskStatus = (tsTodo, tsInProgress, tsDone);
|
||||||
|
|
||||||
|
TIntHolder = class
|
||||||
|
FValue: Integer;
|
||||||
|
end;
|
||||||
|
|
||||||
TTask = class
|
TTask = class
|
||||||
FId: Integer;
|
FId: Integer;
|
||||||
FTitle: string;
|
FTitle: string;
|
||||||
|
|
@ -32,9 +36,12 @@ type
|
||||||
FTasks: TObjectList;
|
FTasks: TObjectList;
|
||||||
FNextId: Integer;
|
FNextId: Integer;
|
||||||
FFilePath: string;
|
FFilePath: string;
|
||||||
|
FLastMtime: Int64;
|
||||||
|
FDeletedIds: TObjectList;
|
||||||
function GetCount: Integer;
|
function GetCount: Integer;
|
||||||
procedure ParseLine(const Line: string; CurrentStatus: TTaskStatus);
|
procedure ParseLine(const Line: string; CurrentStatus: TTaskStatus);
|
||||||
function FormatTask(Task: TTask): string;
|
function FormatTask(Task: TTask): string;
|
||||||
|
function IsDeleted(AId: Integer): Boolean;
|
||||||
public
|
public
|
||||||
constructor Create(const AFilePath: string);
|
constructor Create(const AFilePath: string);
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
|
|
@ -50,6 +57,8 @@ type
|
||||||
function HasDetail(AId: Integer): Boolean;
|
function HasDetail(AId: Integer): Boolean;
|
||||||
function LoadDetail(AId: Integer): string;
|
function LoadDetail(AId: Integer): string;
|
||||||
procedure SaveDetail(AId: Integer; const AContent: string);
|
procedure SaveDetail(AId: Integer; const AContent: string);
|
||||||
|
function HasExternalChanges: Boolean;
|
||||||
|
function MergeFromDisk: Integer;
|
||||||
property Count: Integer read GetCount;
|
property Count: Integer read GetCount;
|
||||||
property FilePath: string read FFilePath;
|
property FilePath: string read FFilePath;
|
||||||
end;
|
end;
|
||||||
|
|
@ -63,11 +72,14 @@ begin
|
||||||
inherited Create;
|
inherited Create;
|
||||||
FFilePath := AFilePath;
|
FFilePath := AFilePath;
|
||||||
FTasks := TObjectList.Create(True);
|
FTasks := TObjectList.Create(True);
|
||||||
FNextId := 1
|
FDeletedIds := TObjectList.Create(True);
|
||||||
|
FNextId := 1;
|
||||||
|
FLastMtime := -1
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TBoard.Destroy;
|
destructor TBoard.Destroy;
|
||||||
begin
|
begin
|
||||||
|
FDeletedIds.Free;
|
||||||
FTasks.Free;
|
FTasks.Free;
|
||||||
inherited Destroy
|
inherited Destroy
|
||||||
end;
|
end;
|
||||||
|
|
@ -209,7 +221,8 @@ begin
|
||||||
end
|
end
|
||||||
finally
|
finally
|
||||||
Lines.Free
|
Lines.Free
|
||||||
end
|
end;
|
||||||
|
FLastMtime := FileAge(FFilePath)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TBoard.Save;
|
procedure TBoard.Save;
|
||||||
|
|
@ -218,6 +231,8 @@ var
|
||||||
I: Integer;
|
I: Integer;
|
||||||
Task: TTask;
|
Task: TTask;
|
||||||
begin
|
begin
|
||||||
|
if Self.HasExternalChanges then
|
||||||
|
Self.MergeFromDisk;
|
||||||
Lines := TStringList.Create;
|
Lines := TStringList.Create;
|
||||||
try
|
try
|
||||||
Lines.Add('#next-id: ' + IntToStr(FNextId));
|
Lines.Add('#next-id: ' + IntToStr(FNextId));
|
||||||
|
|
@ -257,7 +272,8 @@ begin
|
||||||
Lines.SaveToFile(FFilePath)
|
Lines.SaveToFile(FFilePath)
|
||||||
finally
|
finally
|
||||||
Lines.Free
|
Lines.Free
|
||||||
end
|
end;
|
||||||
|
FLastMtime := FileAge(FFilePath)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TBoard.AddTask(const ATitle: string; AStatus: TTaskStatus): TTask;
|
function TBoard.AddTask(const ATitle: string; AStatus: TTaskStatus): TTask;
|
||||||
|
|
@ -277,11 +293,26 @@ begin
|
||||||
FTasks.Add(Result)
|
FTasks.Add(Result)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TBoard.IsDeleted(AId: Integer): Boolean;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
I := 0;
|
||||||
|
while I < FDeletedIds.Count do
|
||||||
|
begin
|
||||||
|
if TIntHolder(FDeletedIds.Get(I)).FValue = AId then
|
||||||
|
Exit(True);
|
||||||
|
I := I + 1
|
||||||
|
end;
|
||||||
|
Result := False
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TBoard.DeleteTask(AId: Integer);
|
procedure TBoard.DeleteTask(AId: Integer);
|
||||||
var
|
var
|
||||||
I: Integer;
|
I: Integer;
|
||||||
Task: TTask;
|
Task: TTask;
|
||||||
DetailPath: string;
|
DetailPath: string;
|
||||||
|
Holder: TIntHolder;
|
||||||
begin
|
begin
|
||||||
I := 0;
|
I := 0;
|
||||||
while I < FTasks.Count do
|
while I < FTasks.Count do
|
||||||
|
|
@ -293,6 +324,9 @@ begin
|
||||||
if FileExists(DetailPath) then
|
if FileExists(DetailPath) then
|
||||||
DeleteFile(DetailPath);
|
DeleteFile(DetailPath);
|
||||||
FTasks.Delete(I);
|
FTasks.Delete(I);
|
||||||
|
Holder := TIntHolder.Create;
|
||||||
|
Holder.FValue := AId;
|
||||||
|
FDeletedIds.Add(Holder);
|
||||||
Exit
|
Exit
|
||||||
end;
|
end;
|
||||||
I := I + 1
|
I := I + 1
|
||||||
|
|
@ -395,6 +429,73 @@ begin
|
||||||
WriteFile(Self.GetDetailPath(AId), AContent)
|
WriteFile(Self.GetDetailPath(AId), AContent)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TBoard.HasExternalChanges: Boolean;
|
||||||
|
var
|
||||||
|
CurrentMtime: Int64;
|
||||||
|
begin
|
||||||
|
CurrentMtime := FileAge(FFilePath);
|
||||||
|
Result := (CurrentMtime <> -1) and (CurrentMtime <> FLastMtime)
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TBoard.MergeFromDisk: Integer;
|
||||||
|
var
|
||||||
|
DiskBoard: TBoard;
|
||||||
|
I, Merged, MaxId: Integer;
|
||||||
|
DiskTask, MemTask, NewTask: TTask;
|
||||||
|
begin
|
||||||
|
Merged := 0;
|
||||||
|
DiskBoard := TBoard.Create(FFilePath);
|
||||||
|
try
|
||||||
|
DiskBoard.Load;
|
||||||
|
|
||||||
|
MaxId := FNextId;
|
||||||
|
if DiskBoard.FNextId > MaxId then
|
||||||
|
MaxId := DiskBoard.FNextId;
|
||||||
|
|
||||||
|
I := 0;
|
||||||
|
while I < DiskBoard.FTasks.Count do
|
||||||
|
begin
|
||||||
|
DiskTask := TTask(DiskBoard.FTasks.Get(I));
|
||||||
|
if Self.IsDeleted(DiskTask.FId) then
|
||||||
|
begin
|
||||||
|
I := I + 1;
|
||||||
|
Continue
|
||||||
|
end;
|
||||||
|
MemTask := Self.FindById(DiskTask.FId);
|
||||||
|
if MemTask = nil then
|
||||||
|
begin
|
||||||
|
NewTask := TTask.Create;
|
||||||
|
NewTask.FId := DiskTask.FId;
|
||||||
|
NewTask.FTitle := DiskTask.FTitle;
|
||||||
|
NewTask.FStatus := DiskTask.FStatus;
|
||||||
|
NewTask.FCreated := DiskTask.FCreated;
|
||||||
|
NewTask.FPriority := DiskTask.FPriority;
|
||||||
|
FTasks.Add(NewTask);
|
||||||
|
Merged := Merged + 1
|
||||||
|
end
|
||||||
|
else if MemTask.FTitle <> DiskTask.FTitle then
|
||||||
|
begin
|
||||||
|
MemTask.FId := MaxId;
|
||||||
|
MaxId := MaxId + 1;
|
||||||
|
NewTask := TTask.Create;
|
||||||
|
NewTask.FId := DiskTask.FId;
|
||||||
|
NewTask.FTitle := DiskTask.FTitle;
|
||||||
|
NewTask.FStatus := DiskTask.FStatus;
|
||||||
|
NewTask.FCreated := DiskTask.FCreated;
|
||||||
|
NewTask.FPriority := DiskTask.FPriority;
|
||||||
|
FTasks.Add(NewTask);
|
||||||
|
Merged := Merged + 1
|
||||||
|
end;
|
||||||
|
I := I + 1
|
||||||
|
end;
|
||||||
|
FNextId := MaxId
|
||||||
|
finally
|
||||||
|
DiskBoard.Free
|
||||||
|
end;
|
||||||
|
FLastMtime := FileAge(FFilePath);
|
||||||
|
Result := Merged
|
||||||
|
end;
|
||||||
|
|
||||||
function CreatedToLocalDate(const AUtcStr: string): string;
|
function CreatedToLocalDate(const AUtcStr: string): string;
|
||||||
var
|
var
|
||||||
UtcDT: TDateTime;
|
UtcDT: TDateTime;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ type
|
||||||
FInputPrompt: string;
|
FInputPrompt: string;
|
||||||
FStatusMsg: string;
|
FStatusMsg: string;
|
||||||
FNeedsRedraw: Boolean;
|
FNeedsRedraw: Boolean;
|
||||||
|
FPollCounter: Integer;
|
||||||
procedure DrawBoard;
|
procedure DrawBoard;
|
||||||
procedure DrawColumn(ColIndex: Integer; Row, Col, Width, Height: Integer);
|
procedure DrawColumn(ColIndex: Integer; Row, Col, Width, Height: Integer);
|
||||||
procedure DrawStatusBar;
|
procedure DrawStatusBar;
|
||||||
|
|
@ -64,7 +65,8 @@ begin
|
||||||
FInputBuf := '';
|
FInputBuf := '';
|
||||||
FInputPrompt := '';
|
FInputPrompt := '';
|
||||||
FStatusMsg := 'q:quit a:add d:delete Enter:details h/l:move task ?:help';
|
FStatusMsg := 'q:quit a:add d:delete Enter:details h/l:move task ?:help';
|
||||||
FNeedsRedraw := True
|
FNeedsRedraw := True;
|
||||||
|
FPollCounter := 0
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TKanbanUI.ColumnTitle(ColIndex: Integer): string;
|
function TKanbanUI.ColumnTitle(ColIndex: Integer): string;
|
||||||
|
|
@ -343,6 +345,7 @@ begin
|
||||||
FTerm.ShowCursor;
|
FTerm.ShowCursor;
|
||||||
FTerm.BufFlush;
|
FTerm.BufFlush;
|
||||||
FTerm.DisableRawMode;
|
FTerm.DisableRawMode;
|
||||||
|
FBoard.MergeFromDisk;
|
||||||
FBoard.Save;
|
FBoard.Save;
|
||||||
Halt(0)
|
Halt(0)
|
||||||
end;
|
end;
|
||||||
|
|
@ -594,7 +597,7 @@ end;
|
||||||
|
|
||||||
procedure TKanbanUI.Run;
|
procedure TKanbanUI.Run;
|
||||||
var
|
var
|
||||||
Key: Integer;
|
Key, Merged: Integer;
|
||||||
begin
|
begin
|
||||||
FTerm.EnableRawMode;
|
FTerm.EnableRawMode;
|
||||||
try
|
try
|
||||||
|
|
@ -611,6 +614,27 @@ begin
|
||||||
|
|
||||||
Key := FTerm.ReadKey;
|
Key := FTerm.ReadKey;
|
||||||
|
|
||||||
|
if Key = KEY_NONE then
|
||||||
|
begin
|
||||||
|
FPollCounter := FPollCounter + 1;
|
||||||
|
if FPollCounter >= 20 then
|
||||||
|
begin
|
||||||
|
FPollCounter := 0;
|
||||||
|
if FBoard.HasExternalChanges then
|
||||||
|
begin
|
||||||
|
Merged := FBoard.MergeFromDisk;
|
||||||
|
if Merged > 0 then
|
||||||
|
begin
|
||||||
|
FStatusMsg := 'Reloaded ' + IntToStr(Merged) + ' new task(s) from disk';
|
||||||
|
Self.ClampRow;
|
||||||
|
FNeedsRedraw := True
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
FPollCounter := 0;
|
||||||
|
|
||||||
if FViewMode = vmBoard then
|
if FViewMode = vmBoard then
|
||||||
Self.HandleBoardKey(Key)
|
Self.HandleBoardKey(Key)
|
||||||
else if FViewMode = vmDetail then
|
else if FViewMode = vmDetail then
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue