feat(windows): kmconfig console app

This app uses the new `TKeymanSettings` class to present a standardised
interface for all Keyman settings -- debug, development and
compatibility. This is a console app, intended for use in a variety of
scenarios, including development tools, in the future.

Settings can be set, exported/imported to/from json, reset, shown.

Important in this PR is establishing the command line parameter
consistency and usage into the future.
This commit is contained in:
Marc Durdin 2020-10-21 16:38:26 +11:00
parent 1418a0ebc0
commit aa8fecf385
12 changed files with 1491 additions and 4 deletions

View file

@ -2,7 +2,7 @@
# Keyman Desktop Makefile
#
TARGETS=kmshell kmbrowserhost setup insthelp
TARGETS=kmshell kmconfig kmbrowserhost setup insthelp
RELEASE_TARGETS=help
CLEANS=inst clean-desktop
MANIFESTS=kmshell setup insthelp
@ -15,6 +15,10 @@ kmshell:
cd $(ROOT)\src\desktop\kmshell
$(MAKE) $(TARGET)
kmconfig:
cd $(ROOT)\src\desktop\kmconfig
$(MAKE) $(TARGET)
kmbrowserhost:
cd $(ROOT)\src\desktop\kmbrowserhost
$(MAKE) $(TARGET)

View file

@ -12,6 +12,9 @@
<Projects Include="setup\setup.dproj">
<Dependencies/>
</Projects>
<Projects Include="kmconfig\kmconfig.dproj">
<Dependencies/>
</Projects>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Default.Personality.12</Borland.Personality>
@ -47,14 +50,23 @@
<Target Name="setup:Make">
<MSBuild Projects="setup\setup.dproj" Targets="Make"/>
</Target>
<Target Name="kmconfig">
<MSBuild Projects="kmconfig\kmconfig.dproj"/>
</Target>
<Target Name="kmconfig:Clean">
<MSBuild Projects="kmconfig\kmconfig.dproj" Targets="Clean"/>
</Target>
<Target Name="kmconfig:Make">
<MSBuild Projects="kmconfig\kmconfig.dproj" Targets="Make"/>
</Target>
<Target Name="Build">
<CallTarget Targets="kmshell;kmbrowserhost;setup"/>
<CallTarget Targets="kmshell;kmbrowserhost;setup;kmconfig"/>
</Target>
<Target Name="Clean">
<CallTarget Targets="kmshell:Clean;kmbrowserhost:Clean;setup:Clean"/>
<CallTarget Targets="kmshell:Clean;kmbrowserhost:Clean;setup:Clean;kmconfig:Clean"/>
</Target>
<Target Name="Make">
<CallTarget Targets="kmshell:Make;kmbrowserhost:Make;setup:Make"/>
<CallTarget Targets="kmshell:Make;kmbrowserhost:Make;setup:Make;kmconfig:Make"/>
</Target>
<Import Project="$(BDS)\Bin\CodeGear.Group.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Group.Targets')"/>
</Project>

View file

@ -145,6 +145,10 @@
<File Id="kmbrowserhost.exe" Name="kmbrowserhost.exe" KeyPath="yes" />
</Component>
<Component>
<File Id="kmconfig.exe" Name="kmconfig.exe" KeyPath="yes" />
</Component>
<Component Id="Reg_RootPath" Guid="*">
<RegistryValue KeyPath="yes" Root="HKLM" Key="SOFTWARE\Keyman\Keyman Desktop" Name="root path" Type="string" Value="[INSTALLDIR]" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Keyman\Keyman Desktop" Name="version" Type="string" Value="$(var.VERSION)" />

View file

@ -0,0 +1,333 @@
unit Keyman.System.KMConfigMain;
interface
uses
Keyman.System.Settings;
type
TKMConfig = class
private
class var IsAdmin: Boolean;
class var Settings: TKeymanSettings;
class function Import(const Filename: string): Boolean; static;
class function Export(const Filename: string): Boolean; static;
class function Reset(const ID: string): Boolean; static;
class function SetValue(const ID, Value: string): Boolean; static;
class function Show(const P2: string): Boolean; static;
class procedure Log(const message: string);
class procedure LogError(const message: string);
class procedure LogWarning(const message: string);
public
class constructor ClassCreate;
class destructor ClassDestroy;
class procedure Run; static;
end;
implementation
uses
System.SysUtils,
System.Win.Registry,
Winapi.Windows,
Keyman.System.SettingsManager,
Keyman.System.SettingsManagerFile,
RegistryKeys;
type
TANSIColor = record
Green, Grey, Default, Yellow, Red, White: string;
end;
var
ANSIColor: TANSIColor;
class constructor TKMConfig.ClassCreate;
var
r: TRegistry;
begin
inherited;
r := TRegistry.Create;
try
r.RootKey := HKEY_LOCAL_MACHINE;
IsAdmin := r.OpenKey(SRegKey_KeymanEngine_LM, True);
finally
r.Free;
end;
Settings := TKeymanSettings.Create;
end;
class destructor TKMConfig.ClassDestroy;
begin
Settings.Free;
inherited;
end;
class procedure TKMConfig.Run;
var
Command: string;
Result: Boolean;
begin
Settings := TKeymanSettings.Create;
Command := ParamStr(1).ToLower;
if Command = 'import' then Result := Import(ParamStr(2))
else if Command = 'export' then Result := Export(ParamStr(2))
else if Command = 'reset' then Result := Reset(ParamStr(2))
else if Command = 'set' then Result := SetValue(ParamStr(2), ParamStr(3))
else if Command = 'show' then Result := Show(ParamStr(2))
else
begin
Result := False;
writeln('Usage: kmconfig command [options]');
writeln('Commands:');
writeln(' import <settings.json> Imports settings into local machine registry');
writeln(' export <settings.json> Exports settings from local machine registry to file');
writeln(' reset [id] Resets one or all settings to default');
writeln(' set id value Sets setting identified by id to value');
writeln(' show [-a] [id] Lists one or all settings; -a to show all, even empty settings');
writeln('Note: registry changes may require elevation to succeed.');
end;
if Result
then ExitCode := 0
else ExitCode := 1;
end;
class procedure TKMConfig.Log(const message: string);
begin
writeln(message);
end;
class procedure TKMConfig.LogError(const message: string);
begin
Log(AnsiColor.Red+'ERROR: '+message+AnsiColor.Default);
end;
class procedure TKMConfig.LogWarning(const message: string);
begin
Log(AnsiColor.Yellow+'WARNING: '+message+AnsiColor.Default);
end;
class function TKMConfig.Export(const Filename: string): Boolean;
begin
TKeymanSettingsManager.Load(Settings);
TKeymanSettingsManagerFile.Export(Settings, Filename);
Log('All Keyman settings have been exported to '+Filename);
Result := True;
end;
class function TKMConfig.Import(const Filename: string): Boolean;
begin
TKeymanSettingsManager.Load(Settings);
TKeymanSettingsManagerFile.Import(Settings, Filename);
TKeymanSettingsManager.Save(Settings, IsAdmin);
Log('Keyman settings in '+Filename+' have been imported');
if not IsAdmin then
LogWarning('Some settings that require Administrator permissions may not have been updated');
Result := True;
end;
class function TKMConfig.Reset(const ID: string): Boolean;
var
Setting: TKeymanSetting;
begin
if ID = '' then
begin
Settings.Reset;
TKeymanSettingsManager.Load(Settings);
if Settings.Modified then
begin
TKeymanSettingsManager.Save(Settings, IsAdmin);
Log('Keyman settings have been reset to default');
if not IsAdmin then
LogWarning('Some settings that require Administrator permissions may not have been updated');
end
else
begin
Log('All Keyman settings were already default');
end;
end
else
begin
TKeymanSettingsManager.Load(Settings);
Setting := Settings.Find(ID);
if not Assigned(Setting) then
begin
// Custom value, do we support it?
if ID.StartsWith(CustomKeymanSetting_TSFApp.ID, True) then
begin
LogWarning('Setting '+ID+' does not exist; not updating.');
Exit(True);
end
else
begin
LogError('Setting '+ID+' is not a valid Keyman setting.');
Exit(False);
end;
end;
if Setting.IsEmpty then
begin
LogWarning('Setting '+ID+' is already default; not updating.');
Exit(True);
end;
if (Setting.Base.RootKey = HKEY_LOCAL_MACHINE) and not IsAdmin then
begin
LogError('Setting '+ID+' requires Administrator permissions to update.');
Exit(False);
end;
Setting.Reset;
if Setting.Modified then
begin
TKeymanSettingsManager.Save(Settings, IsAdmin);
Log('Keyman setting '+ID+' has been reset to default');
end
else
begin
LogWarning('Keyman setting '+ID+' was already default; not updating');
end;
end;
Result := True;
end;
class function TKMConfig.SetValue(const ID, Value: string): Boolean;
var
Setting: TKeymanSetting;
begin
Setting := Settings.Find(ID);
if not Assigned(Setting) then
begin
// Custom value, do we support it?
if ID.StartsWith(CustomKeymanSetting_TSFApp.ID, True) then
begin
Setting := TKeymanSetting.CreateCustom_TSFApp(ID);
Settings.Add(Setting);
end
else
begin
LogError(ID+' is not a valid Keyman setting.');
Exit(False);
end;
end;
if (Setting.Base.RootKey = HKEY_LOCAL_MACHINE) and not IsAdmin then
begin
LogError('Setting '+ID+' requires Administrator permissions to update.');
Exit(False);
end;
case Setting.Base.ValueType of
kstString: Setting.ValueStr := Value;
kstInteger: Setting.ValueInt := StrToInt(Value);
end;
TKeymanSettingsManager.Save(Settings, IsAdmin);
Log('Keyman setting '+ID+' has been updated to '+Value);
Result := True;
end;
class function TKMConfig.Show(const P2: string): Boolean;
function ShowOneSetting(const ID: string): Boolean;
var
Setting: TKeymanSetting;
v: string;
begin
Setting := Settings.Find(ID);
if not Assigned(Setting) then
begin
LogError('Setting '+ID+' is not a valid Keyman setting.');
Exit(False);
end;
case Setting.Base.ValueType of
kstString: v := Setting.ValueStr;
kstInteger: v := Setting.ValueInt.ToString;
end;
if Setting.IsEmpty
then Log(AnsiColor.Green+Setting.Base.ID+AnsiColor.Grey+'='+AnsiColor.Grey+v+AnsiColor.Default)
else Log(AnsiColor.Green+Setting.Base.ID+AnsiColor.Grey+'='+AnsiColor.White+v+AnsiColor.Default);
Result := True;
end;
procedure ShowAllSettings(ShowAll: Boolean);
var
Setting: TKeymanSetting;
begin
for Setting in Settings do
begin
if ShowAll or not Setting.IsEmpty then
ShowOneSetting(Setting.Base.ID);
end;
end;
begin
Result := True;
TKeymanSettingsManager.Load(Settings);
if SameText(P2, '-a') then
ShowAllSettings(True)
else if P2 = '' then
ShowAllSettings(False)
else
Result := ShowOneSetting(P2);
end;
function DetectColorMode: Boolean;
var
mode: DWORD;
hConsole: THandle;
const
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
begin
Result := False;
mode := 0;
hConsole := GetStdHandle(STD_OUTPUT_HANDLE);
if hConsole = INVALID_HANDLE_VALUE then
begin
//writeln(Format('GetStdHandle failed with %d %s', [GetLastError, SysErrorMessage(GetLastError)]));
Exit;
end;
if GetEnvironmentVariable('MSYSTEM') = 'MINGW64' then
begin
// MinGW64 test
// Use colour mode only with a non-redirected console. This test fails with pipes.
// For pipe use, explicitly use -no-color parameter
Result := GetFileType(hConsole) = 3;
end
else
begin
// Win32 console color mode test
if not GetConsoleMode(hConsole, mode) then
Exit;
mode := mode or ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if not SetConsoleMode(hConsole, mode) then
Exit;
Result := True;
end;
end;
const
ESC=#$1b;
ESC_BRIGHT_YELLOW=ESC+'[38;2;255;255;0m';
ESC_RED=ESC+'[38;2;255;0;0m';
ESC_GREEN=ESC+'[38;2;0;255;0m';
ESC_GREY=ESC+'[38;2;128;132;128m';
ESC_WHITE=ESC+'[38;2;255;255;255m';
ESC_DEFAULT=ESC+'[0m';
initialization
if DetectColorMode then
begin
ANSIColor.Green := ESC_GREEN;
ANSIColor.Default := ESC_DEFAULT;
ANSIColor.Red := ESC_RED;
ANSIColor.Yellow := ESC_BRIGHT_YELLOW;
ANSIColor.Grey := ESC_GREY;
ANSIColor.White := ESC_WHITE;
end;
end.

View file

@ -0,0 +1,32 @@
#
# KMConfig Makefile
#
!include ..\..\Defines.mak
build: version.res manifest.res #icons
$(DELPHI_MSBUILD) kmconfig.dproj /p:Platform=Win32
$(TDS2DBG) $(WIN32_TARGET_PATH)\kmconfig.exe
$(SENTRYTOOL_DELPHIPREP) $(WIN32_TARGET_PATH)\kmconfig.exe -dpr kmconfig.exe
$(COPY) $(WIN32_TARGET_PATH)\kmconfig.exe $(PROGRAM)\desktop
if exist $(WIN32_TARGET_PATH)\kmconfig.exe $(COPY) $(WIN32_TARGET_PATH)\kmconfig.exe $(DEBUGPATH)\desktop
#icons:
#rc icons.rc
clean: def-clean
signcode:
$(SIGNCODE) /d "Keyman for Windows" $(PROGRAM)\desktop\kmconfig.exe
backup:
$(WZZIP) $(BUILD)\desktop\kmconfig.zip $(BACKUPDEFAULTS) kmconfig.exe
test-manifest:
# test that (a) linked manifest exists and correct, and (b) has uiAccess=true
$(MT) -nologo -inputresource:$(PROGRAM)\desktop\kmconfig.exe -validate_manifest
install:
copy $(ROOT)\bin\desktop\kmconfig.exe "$(INSTALLPATH_KEYMANDESKTOP)"
!include ..\..\Target.mak

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

View file

@ -0,0 +1,43 @@
program kmconfig;
uses
System.SysUtils,
Keyman.System.SettingsManager in '..\..\global\delphi\general\Keyman.System.SettingsManager.pas',
RegistryKeys in '..\..\global\delphi\general\RegistryKeys.pas',
KeymanVersion in '..\..\global\delphi\general\KeymanVersion.pas',
Keyman.System.Settings in '..\..\global\delphi\general\Keyman.System.Settings.pas',
Keyman.System.KeymanSentryClient in '..\..\global\delphi\general\Keyman.System.KeymanSentryClient.pas',
Sentry.Client.Console in '..\..\ext\sentry\Sentry.Client.Console.pas',
Sentry.Client in '..\..\ext\sentry\Sentry.Client.pas',
sentry in '..\..\ext\sentry\sentry.pas',
KeymanPaths in '..\..\global\delphi\general\KeymanPaths.pas',
DebugPaths in '..\..\global\delphi\general\DebugPaths.pas',
utilexecute in '..\..\global\delphi\general\utilexecute.pas',
unicode in '..\..\global\delphi\general\unicode.pas',
ErrorControlledRegistry in '..\..\global\delphi\vcl\ErrorControlledRegistry.pas',
Keyman.System.KMConfigMain in 'Keyman.System.KMConfigMain.pas',
Keyman.System.SettingsManagerFile in '..\..\global\delphi\general\Keyman.System.SettingsManagerFile.pas';
{$R manifest.res}
{$R version.res}
{$APPTYPE CONSOLE}
const
LOGGER_DESKTOP_KMCONFIG = TKeymanSentryClient.LOGGER_DESKTOP + '.kmconfig';
begin
TKeymanSentryClient.Start(TSentryClientConsole, kscpDesktop, LOGGER_DESKTOP_KMCONFIG);
try
try
TKeymanSentryClient.Validate;
TKMConfig.Run;
except
on E: Exception do
begin
SentryHandleException(E);
ExitCode := 1;
end;
end;
finally
TKeymanSentryClient.Stop;
end;
end.

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity type="win32" name="com.keyman.windows.kmconfig" version="$VersionWin" processorArchitecture="x86"/>
<description>Keyman for Windows Config</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<!-- Switch on various compatibility values -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/><!-- Windows Vista and Windows Server 2008 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/><!-- Windows 7 and Windows Server 2008 R2 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/><!-- Windows 8 and Windows Server 2012 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/><!-- Windows 8.1 and Windows Server 2012 R2 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/><!-- Windows 10 -->
</application>
</compatibility>
<!-- Runtime themes v6.0 common controls -->
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" publicKeyToken="6595b64144ccf1df" language="*" processorArchitecture="*"/>
</dependentAssembly>
</dependency>
</assembly>

View file

@ -0,0 +1 @@
1 24 manifest.xml

View file

@ -0,0 +1,30 @@
1 VERSIONINFO
FILEVERSION $VERSIONNUM
PRODUCTVERSION $VERSIONNUM
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x4L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "0C0904E4"
BEGIN
VALUE "CompanyName", "SIL International\0"
VALUE "FileDescription", "Keyman Config\0"
VALUE "FileVersion", "$VERSION\0"
VALUE "InternalName", "KMCONFIG\0"
VALUE "LegalCopyright", "© SIL International\0"
VALUE "LegalTrademarks", "\0"
VALUE "OriginalFilename", "KMCONFIG.EXE\0"
VALUE "ProductName", "Keyman for Windows\0"
VALUE "ProductVersion", "$VERSION\0"
VALUE "Comments", "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0xc09, 1252
END
END