') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Settings command by msftrubengu · Pull Request #436 · microsoft/winget-cli · GitHub
Skip to content

Settings command - #436

Merged
Ruben Guerrero (msftrubengu) merged 15 commits into
microsoft:masterfrom
msftrubengu:settings_cmd
Jun 22, 2020
Merged

Settings command#436
Ruben Guerrero (msftrubengu) merged 15 commits into
microsoft:masterfrom
msftrubengu:settings_cmd

Conversation

@msftrubengu

@msftrubenguRuben Guerrero (msftrubengu) commented Jun 17, 2020

Copy link
Copy Markdown
Contributor

This changes introduce the settings command and two settings that can be configured.

When the setting command is executed the first time a settings file with empty values will be created. It will also open in the default text editor that is configured for json extensions. When there is no file type association for json files, older versions of Windows will fail to open. In this scenario notepad will be used as the text editor. Newer version of windows will show the "choose app" dialog where the user can select the text editor of their preference.

progressBar
This settings allows to modify the default color of the progress bar. Currently it supports three values, but it can be expanded to include more colors.

autoUpdateIntervalInMinutes
This is the value in minutes in which the source will auto update. The default value is 5 minutes. A user can set 0 to disable auto updates.

See this spec for more information.

Validated manually and created several unittest for new functionality.

Microsoft Reviewers: Open in CodeFlow

Comment threaddoc/Settings.md Outdated

### autoUpdateIntervalInMinutes

Positive integer that represents the interval in minutes on how often to update the WinGet source automatically. For no updates use 0.

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Positive integer that represents the interval in minutes on how often to update the WinGet source automatically [](start = 0, length = 111)

Positive integer that represents the interval in minutes of how often to automatically check for updates to a WinGet source. The check for updates only happens when a source is used, and if no update is available the interval will be reset. #Closed

Comment threadsrc/AppInstallerCLICore/Core.cpp Outdated
std::unique_ptr<Command> command = std::make_unique<RootCommand>();

// Load the settings file and warn if necessary.
auto warnings = Settings::UserSettings::Instance().GetWarnings();

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settings::UserSettings::Instance() [](start = 24, length = 34)

I would suggest a standalone function that returns this, such as:

Settings::User()

Or similar. I really hate cluttered call sites. #Closed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this could just be directly in the for init.


In reply to: 442408327 [](ancestors = 442408327)

ListVersions, // Used in Show command to list all available versions of an app
NoVT, // Disable VirtualTerminal outputs
PlainStyle, // Makes progress display as plain
RetroStyle, // Makes progress display as plain

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plain [](start = 53, length = 5)

Your vendetta against plain is not complete. #Closed

Accent,
Rainbow,
};
using namespace Settings;

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using namespace Settings; [](start = 4, length = 25)

Not in headers, unless it is a string literals namespace. #Closed


std::string SettingsCommand::HelpLink() const
{
return "https://aka.ms/winget-settings";

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://aka.ms/winget-settings [](start = 16, length = 30)

Did someone create this? #Closed

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did, is not pointing to docs right now, but will update it once settings.md is on master


In reply to: 442413601 [](ancestors = 442413601)

{
// Settings file was loaded correctly, create backup.
UserSettings::Instance().CreateBackup();
}

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole block feels like it should be in:

UserSettings::PrepareToShellExecuteFile()

or someting. I don't think the command should be the one responsible for understanding this process. #Closed


std::string filePathUTF8Str = UserSettings::Instance().SettingsFilePath().u8string();

// Kudos to the terminal team for this work around.

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Kudos to the terminal team for this work around. [](start = 8, length = 51)

Does one have to do something special to invoke the "choose which app to use" dialog? I expected that would just be automatic. #Closed

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It really depends on the version of Windows. Previous version will fail and that's where notepad will be open. Newer versions will show the dialog.


In reply to: 442416924 [](ancestors = 442416924)

return result;
}

std::string ToString(const std::string_view key)

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToString [](start = 16, length = 8)

this is 2 chars less than std::string, but a bit more confusing as one doesn't know exactly what is going to happen without reading it.

Please remove and just use std::string() #Closed

#include <optional>
#include <string>

namespace AppInstaller::Utility

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this file need to be public? Probably fine for it to be for potential future use, but it shouldn't be need right now. #Closed

#include "AppInstallerStrings.h"
#include "winget/settings/Source.h"
#include "winget/settings/Visual.h"
#include <json.h>

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A consumer of this header should not need to include JSON. That should all be implementation in the cpp file. #Closed

{
static UserSettings& Instance()
{
// If we go multi-threaded secure this.

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// If we go multi-threaded secure this. [](start = 12, length = 39)

It already is secured by what STL (the person) likes to call magic statics. Basically auto-thread safe init of function local statics. #Closed


private:
UserSettings();
~UserSettings() {};

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{}; [](start = 23, length = 4)

= default; #Closed

}

UserSettings(const UserSettings&) = delete;
UserSettings& operator=(const UserSettings&) = delete;

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also delete the moves, the only one should be Instance. #Closed

// Representation of the parsed settings file.
struct UserSettings
{
static UserSettings& Instance()

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserSettings& [](start = 15, length = 13)

const&? We don't really want anyone changing anything, do we? #Closed

UserSettings& operator=(const UserSettings&) = delete;

UserSettingsType GetType() const { return m_type; }
std::vector<std::string> GetWarnings() const { return m_warnings; }

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::vectorstd::string [](start = 8, length = 24)

const& #Closed

UserSettings();
~UserSettings() {};

std::optional<Json::Value> ParseFile(const std::string_view& fileName);

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::optionalJson::Value ParseFile(const std::string_view& fileName); [](start = 8, length = 71)

Move to unnamed namespace in cpp.
Use std::filesystem::path. #Closed

UserSettingsType GetType() const { return m_type; }
std::vector<std::string> GetWarnings() const { return m_warnings; }

void Reload();

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reload [](start = 13, length = 6)

When would this be used? #Closed


void Reload();
void CreateFileIfNeeded();
void CreateBackup();

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These feel like they should be static. #Closed

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed anymore


In reply to: 442426732 [](ancestors = 442426732)

void CreateBackup();

std::filesystem::path SettingsFilePath();
std::filesystem::path SettingsBackupFilePath();

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also static. #Closed

static constexpr std::string_view s_SettingBackupFileName = "settings.json.backup"sv;

static constexpr std::string_view s_SettingEmpty =
R"""({

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"" [](start = 10, length = 2)

" is an interesting choice for a raw string delimiter. #Closed

#include "winget/settings/Source.h"
#include "winget/settings/Visual.h"

#include <iostream>

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not in the pch, add it there. #Closed


// Settings
inline const Source& GetSource() const { return *m_source; }
inline const Visual& GetVisual() const { return *m_visual; }

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of having to create classes and functions for every setting. I would prefer a data driven model, thus requiring probably two files to be touched. Will look at the code for parsing and see if that still seems like a reasonable position. #Closed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you kind of already do this, just not to the level that would make it easier to add new things.

There are many ways that it could be done, but for type safety I think it would need to be similar to the Execution::Context.

enum for settings
templated struct with specializations per enum
templated get and parse functions

To add a new setting:

add new enum value
add new specialization
add parse call in main Parse method

Templated struct contains:

json "path"
json data type
internal data type
default value
parse function to turn json type into internal type

Parsed data gets stored in a map similar to Execution::Context. Get() looks there first, or returns struct::default if not present.


In reply to: 442441037 [](ancestors = 442441037)


std::stringstream settingsContent;
settingsContent << stream->rdbuf();
std::string settingsContentStr = settingsContent.str();

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a utility function to read the entire stream already (ReadEntireStream). I don't know if its implementation is better than this, but you should just use that function and replace the body if warranted. #Closed

THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND), !std::filesystem::exists(SettingsFilePath()));

auto from = SettingsFilePath();
auto to = GetPathTo(PathName::UserFileSettings) / s_SettingBackupFileName;

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetPathTo(PathName::UserFileSettings) / s_SettingBackupFileName [](start = 18, length = 63)

SettingsBackupFilePath? #Closed

const char* const Value = " Value: ";
const char* const InvalidFieldValue = "Invalid field value.";
const char* const InvalidFieldFormat = "Invalid field format.";
const char* const LoadedBackupSettings = "Loaded settings from backup file.";

@JohnMcPMSJohnMcPMSJun 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constexpr std::string_view #Resolved

Comment threadsrc/AppInstallerCLICore/Core.cpp Outdated
auto warnings = Settings::UserSettings::Instance().GetWarnings();
for (const auto& warning : warnings)
{
context.Reporter.Warn() << warning << std::endl;

@msftrubenguRuben Guerrero (msftrubengu)Jun 19, 2020

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context.Reporter.Warn() << warning << std::endl; [](start = 12, length = 48)

Print only a warning message and only print the syntax error when winget settings #Resolved

@msftrubengu
Ruben Guerrero (msftrubengu) marked this pull request as ready for review June 19, 2020 20:13
@msftrubenguRuben Guerrero (msftrubengu) linked an issue Jun 19, 2020 that may be closed by this pull request

void SettingsCommand::ExecuteInternal(Execution::Context& context) const
{
// Show warnings only when the setting command is executed.

@JohnMcPMSJohnMcPMSJun 19, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Show warnings only when the setting command is executed. [](start = 8, length = 59)

You might want to have some text beforehand that will set up the warnings that are about to come out, rather than just showing them. #Closed

// Some versions of windows will fail if no file extension association exists, other will pop up the dialog
// to make the user pick their default.
// Kudos to the terminal team for this work around.
HINSTANCE res = ShellExecuteA(nullptr, nullptr, filePathUTF8Str.c_str(), nullptr, nullptr, SW_SHOW);

@JohnMcPMSJohnMcPMSJun 19, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShellExecuteA [](start = 24, length = 13)

I don't think we have done anything to set our process to UTF-8 defaults, enabling UTF-8 *A apis. So this should probably be use the W function. Or potentially figure out how we do set ourself as UTF-8 aware (although I suspect that older OS versions would still need to use W). #Closed

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed. We are using ShellExecuteExA in ShellExecuteInstallerHandler.cpp, is that expected?


In reply to: 443074390 [](ancestors = 443074390)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That has its own issue, and yes, it should be changed (but not in the PR).


In reply to: 443093477 [](ancestors = 443093477,443074390)

<value>Open settings</value>
</data>
<data name="SettingsWarnings" xml:space="preserve">
<value>Unexpected error while loading settings. Please verify your settings by running settings command </value>

@JohnMcPMSJohnMcPMSJun 19, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by running settings command [](start = 80, length = 28)

by running the settings command. #Closed

using value_t = VisualStyle;

static constexpr value_t DefaultValue = VisualStyle::Accent;
static constexpr std::string_view Path = ".visual.progressBar";

@JohnMcPMSJohnMcPMSJun 19, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static constexpr std::string_view Path = ".visual.progressBar" [](start = 12, length = 62)

Do you actually get constexpr from this constructor? It claims to, but I suppose I'm wary of that. That's why I always use the string literal tag (sv). But maybe it isn't necessary. #Closed

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess. Added just in case


In reply to: 443076363 [](ancestors = 443076363)

static constexpr value_t DefaultValue = VisualStyle::Accent;
static constexpr std::string_view Path = ".visual.progressBar";

static std::optional<value_t> Validate(json_t value);

@JohnMcPMSJohnMcPMSJun 19, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json_t [](start = 51, length = 6)

const json_t& as a general rule. #Resolved

struct SettingMapping<Setting::AutoUpdateTimeInMinutes>
{
using json_t = uint32_t;
using value_t = uint32_t;

@JohnMcPMSJohnMcPMSJun 19, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uint32_t [](start = 28, length = 8)

std::chrono::minutes? #Closed

Validate<
Setting::AutoUpdateTimeInMinutes,
Setting::ProgressBarVisualStyle
>(settingsRoot, m_settings, m_warnings);

@JohnMcPMSJohnMcPMSJun 19, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably just make Validate take a single template param and have multiple lines. #Resolved

User().PrepareToShellExecuteFile();


auto filePathUTF16 = ConvertToUTF16(UserSettings::SettingsFilePath().u8string());

@JohnMcPMSJohnMcPMSJun 22, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConvertToUTF16(UserSettings::SettingsFilePath().u8string()); [](start = 29, length = 60)

UserSettings::SettingsFilePath().c_str() [I think... the point is there is a method that just gives back the wide version] #Resolved

{
// Use folding to call each setting validate function.
// Do not change this expression without understanding the implications to the bind order.
// See: https://en.cppreference.com/w/cpp/language/fold for more details.

@JohnMcPMSJohnMcPMSJun 22, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines are not relevant; order should not matter here and it certainly doesn't have anything to do with binding (that is SQLite related, no C++). #Resolved

@JohnMcPMSJohnMcPMS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@JohnMcPMSJohnMcPMS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@msftrubengu
Ruben Guerrero (msftrubengu) merged commit 884ad7f into microsoft:masterJun 22, 2020
@msftrubengu
Ruben Guerrero (msftrubengu) deleted the settings_cmd branch June 22, 2020 19:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Settings command

2 participants

@msftrubengu@JohnMcPMS