Skip to content

feat: Allow for an option to close other outputs for all scripts in pyrevit settings#2793

Merged
jmcouffin merged 14 commits intopyrevitlabs:developfrom
MohamedAsli:feature-2065
Oct 23, 2025
Merged

feat: Allow for an option to close other outputs for all scripts in pyrevit settings#2793
jmcouffin merged 14 commits intopyrevitlabs:developfrom
MohamedAsli:feature-2065

Conversation

@MohamedAsli
Copy link
Copy Markdown
Contributor

@MohamedAsli MohamedAsli commented Sep 5, 2025

Description

This PR introduces a new configuration option to automatically close Script Consoles across multiple script runs.

Users can now enable Minimize the number of open consoles and choose between:

  • Current command consoles → close all console windows associated with the current running command.
  • All consoles → close all currently open console windows, even those not associated with the running command.

I initially tried to implement this at the Python level (script.get_output()), but that only worked for scripts explicitly using that variable, meaning print_md, print_html, print_table, etc. were not caught. To ensure all outputs are handled, I implemented the logic in the C# ScriptConsole file (Would appreciate review on this part as I’m not a C# guy)

@jmcouffin It's the first time I push C# code, I didn't include the compiled dlls, is that right ?

I've also added a clean engine to the Settings bundle file in order to avoid inconsistent option displays across Revit instances, since configs are cached in memory in the rocket mode.


Checklist

Before submitting your pull request, ensure the following requirements are met:

  • Code follows the PEP 8 style guide.
  • Code has been formatted with [Black]
  • Changes are tested and verified to work as expected.

Related Issues

It was initially requested by @gtalarico in the #211, but @eirannejad only implemented it in the backend side and not it in the Settings window

Copy link
Copy Markdown
Contributor

@devloai devloai bot left a comment

Choose a reason for hiding this comment

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

PR Summary:

This PR adds a new configuration option to automatically close Script Consoles across multiple script runs. Users can enable "Minimize the number of open consoles" and choose between closing current command consoles or all consoles. The implementation is done at the C# ScriptConsole level to catch all output types, with corresponding UI changes in the Settings window and clean engine run for consistency.

Review Summary:

The PR implements a valuable feature with a solid architectural approach. The C# implementation ensures all output types are handled, not just those using Python's script.get_output(). However, there are several critical code quality issues that need to be addressed, particularly duplicate imports causing maintenance problems, empty catch blocks violating repository standards, and fragile configuration parsing logic that could cause incorrect behavior.

Follow-up suggestions:

  • @devloai fix the identified critical issues
  • @devloai add unit tests for the configuration parsing logic

}
return (close, mode);
}
catch {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Empty catch block without logging. This makes debugging difficult if config reading fails. Consider adding logging or at least a comment explaining why exceptions are silently ignored.

catch (Exception ex) {
    // Log configuration parsing error for troubleshooting
    System.Diagnostics.Debug.WriteLine($"Failed to read pyRevit config: {ex.Message}");
    return (false, "current_command");
}
actions

Feedback: Rate this comment to help me improve future code reviews:

  • 👍 Good - Helpful and accurate
  • 👎 Poor - Wrong, unclear, or unhelpful
  • Skip if you don't have any strong opinions either way.

var filterId = filterByCommandId ? this.OutputId : null;
ScriptConsoleManager.CloseActiveOutputWindows(excludeOutputWindow: this, filterOutputWindowId: filterId);
}
catch {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Empty catch block without logging. UI operations can fail for various reasons and silent failures make troubleshooting difficult.

catch (Exception ex) {
    // Log UI operation error for troubleshooting
    System.Diagnostics.Debug.WriteLine($"Failed to close other outputs: {ex.Message}");
}
actions

Feedback: Rate this comment to help me improve future code reviews:

  • 👍 Good - Helpful and accurate
  • 👎 Poor - Wrong, unclear, or unhelpful
  • Skip if you don't have any strong opinions either way.

if (!inCore) continue;

if (line.StartsWith("close_other_outputs", StringComparison.InvariantCultureIgnoreCase))
close = line.IndexOf("true", StringComparison.InvariantCultureIgnoreCase) >= 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Config parsing logic is fragile and could match unintended lines. Using IndexOf to find "true" anywhere in the line could match comments or other values. Should parse the key=value format properly.

if (line.StartsWith("close_other_outputs", StringComparison.InvariantCultureIgnoreCase)) {
    var parts = line.Split(new[] { '=' }, 2);
    if (parts.Length == 2)
        close = string.Equals(parts[1].Trim().Trim('"'), "true", StringComparison.InvariantCultureIgnoreCase);
}
actions

Feedback: Rate this comment to help me improve future code reviews:

  • 👍 Good - Helpful and accurate
  • 👎 Poor - Wrong, unclear, or unhelpful
  • Skip if you don't have any strong opinions either way.

@jmcouffin
Copy link
Copy Markdown
Contributor

@MohamedAsli
I am not a confident c# dev either, but I'll review Monday.
Once reviewed I will add the DLLs. This is necessary whenever people use the clone function of the cli to install and use pyrevit

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces a new configuration option to automatically close Script Consoles across multiple script runs. The feature allows users to minimize console clutter by automatically closing existing output windows when new scripts are executed.

  • Adds configuration properties output_close_others and output_close_mode to userconfig
  • Implements UI controls in the Settings window with localization support
  • Integrates console closing logic into the C# ScriptConsole class

Reviewed Changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pyrevitlib/pyrevit/userconfig.py Adds duplicate imports and new configuration properties for console closing behavior
extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py Implements UI binding logic for the new console closing options
extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/bundle.yaml Adds clean engine configuration to prevent cached option displays
extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.xaml Adds UI controls for console closing options with data binding
extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/SettingsWindow.ResourceDictionary.*.xaml Provides localization strings for the new console closing feature
dev/pyRevitLabs.PyRevit.Runtime/ScriptConsole.cs Implements configuration reading and console closing logic in C#

Comment on lines +399 to +400
if (line.StartsWith("close_other_outputs", StringComparison.InvariantCultureIgnoreCase))
close = line.IndexOf("true", StringComparison.InvariantCultureIgnoreCase) >= 0;
Copy link

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

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

The configuration value parsing is unreliable. Using IndexOf to check for 'true' could match partial strings like 'untrue' or comments containing 'true'. Should parse the actual value after the '=' separator like the close_mode parsing below.

Suggested change
if (line.StartsWith("close_other_outputs", StringComparison.InvariantCultureIgnoreCase))
close = line.IndexOf("true", StringComparison.InvariantCultureIgnoreCase) >= 0;
if (line.StartsWith("close_other_outputs", StringComparison.InvariantCultureIgnoreCase)) {
var parts = line.Split(new[] { '=' }, 2);
if (parts.Length == 2)
close = parts[1].Trim().Trim('"').Equals("true", StringComparison.InvariantCultureIgnoreCase);
}

Copilot uses AI. Check for mistakes.
@jmcouffin jmcouffin requested a review from dosymep September 5, 2025 18:43
jmcouffin and others added 4 commits September 9, 2025 10:19
Copy link
Copy Markdown
Member

@dosymep dosymep left a comment

Choose a reason for hiding this comment

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

general recommendations:

  • add the implementation of getting and setting values ​​in the PyRevitConfigs.cs class
  • add constants of names and possible values ​​to the PyRevitConsts.cs class,
  • add the implementation of getting and setting values ​​to the userconfig.py class
  • optional add support for the new configuration to the pyrevit.exe console application

return ScriptConsoleConfigs.DOCTYPE + head.OuterHtml + ActiveDocument.Body.OuterHtml;
}

private static (bool closeOthers, string closeMode) ReadCloseOtherOutputsSetting() {
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 recommend using the configuration API, which is already in the pyRevit library. It is also worth adding these settings to the command line utility :)

Comment on lines +428 to +434
this.Dispatcher.BeginInvoke(new Action(() => {
if (string.Equals(mode, "current_command", StringComparison.InvariantCultureIgnoreCase))
CloseOtherOutputs(filterByCommandId: true);
else if (string.Equals(mode, "close_all", StringComparison.InvariantCultureIgnoreCase)) {
CloseOtherOutputs(filterByCommandId: false);
}
}));
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.

It is better to put the operating modes in an enum or create a class hierarchy

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks a lot!
Here, would you add extension methods to map enum and string from Consts file ?
I'm a beginner in C# so plz don't be implicit :)

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.

after writing the review, I think it will be enough to create text constants with which to conduct comparisons

but you can still make an enum Enum.TryParse<TEnum>(string value, out TEnum result)


self.minimize_consoles_cb.IsChecked = user_config.output_close_others

if user_config.output_close_mode == 'current_command':
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.

It is better to put the operating modes in an enum or create a class hierarchy

def output_close_others(self):
"""Whether to close other output windows."""
return self.core.get_option(
'close_other_outputs',
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.

It is recommended to add configuration constants to the constants class

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 think it's also worth moving these settings to this class

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think it's also worth moving these settings to this class

Didn't understand
They already are in the PyRevitConfig class

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.

write an implementation of access to these parameters inside a class specially made for access to the pyRevit_Config.ini parameters

sample:

@property
def close_other_outputs(self):
    return self.core.get_option(
        CONSTS.CloseOtherOutputs,
        default_value=CONSTS. CloseOtherOutputsDefault,
    )

@close_other_outputs.setter
def close_other_outputs(self, state):
    self.core.set_option(
        CONSTS.CloseOtherOutputs,
        value=state

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.

Sorry, I didn't notice that you wrote the code where it should be :D

@output_close_others.setter
def output_close_others(self, state):
self.core.set_option(
'close_other_outputs',
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.

It is recommended to add configuration constants to the constants class

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 think it's also worth moving these settings to this class

def output_close_mode(self):
"""Output window closing mode: 'current_command' or 'close_all'."""
return self.core.get_option(
'close_mode',
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.

It is recommended to add configuration constants to the constants class

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 think it's also worth moving these settings to this class

@output_close_mode.setter
def output_close_mode(self, mode):
self.core.set_option(
'close_mode',
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.

It is recommended to add configuration constants to the constants class

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 think it's also worth moving these settings to this class

@MohamedAsli MohamedAsli requested a review from dosymep September 12, 2025 08:27
var cfg = GetConfigFile();
var raw = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey);

var s = (raw ?? PyRevitConsts.ConfigsCloseOutputModeDefault).Trim().Trim('"', '\'');
Copy link
Copy Markdown
Contributor Author

@MohamedAsli MohamedAsli Sep 12, 2025

Choose a reason for hiding this comment

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

@dosymep is there a better way than trimming the config value ?

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.

As far as I remember, you don't need to trim it yourself, it should do it automatically

Comment on lines +320 to +348
def output_close_mode_enum(self):
"""Output window closing mode as enum (CurrentCommand | CloseAll)."""
value = self.core.get_option(
CONSTS.ConfigsCloseOutputModeKey,
default_value=CONSTS.ConfigsCloseOutputModeDefault,
)
if not value:
value = CONSTS.ConfigsCloseOutputModeDefault

@output_close_mode.setter
def output_close_mode(self, mode):
self.core.set_option(
'close_mode',
value=mode
)
value_lc = str(value).lower()

if value_lc == str(CONSTS.ConfigsCloseOutputModeCloseAll).lower():
return PyRevit.OutputCloseMode.CloseAll
else:
return PyRevit.OutputCloseMode.CurrentCommand

@output_close_mode_enum.setter
def output_close_mode_enum(self, mode):
"""Store string in INI, mapped from enum."""
if mode == PyRevit.OutputCloseMode.CloseAll:
self.core.set_option(
CONSTS.ConfigsCloseOutputModeKey,
value=CONSTS.ConfigsCloseOutputModeCloseAll
)
else:
self.core.set_option(
CONSTS.ConfigsCloseOutputModeKey,
value=CONSTS.ConfigsCloseOutputModeCurrentCommand
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@dosymep With enum, I had to deal with mapping somewhere.
Feel free to redirect me, if you see a better way to do plz

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 think it's good, I hope in the future I will rewrite all the code for working with settings, and there will be a more convenient option

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.

remove only the enum suffix in name, I don't think users need to see it

@jmcouffin
Copy link
Copy Markdown
Contributor

@devloai review again taking into account all the comments from Dosymep and MohamedAsli and latest changes

Copy link
Copy Markdown
Contributor

@devloai devloai bot left a comment

Choose a reason for hiding this comment

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

PR Summary:

This PR introduces a new configuration option to automatically close Script Console windows when new scripts run. Users can enable "Minimize the number of open consoles" and choose between closing current command consoles or all consoles. The implementation spans C# configuration management, UI settings integration, and multi-language localization.

Review Summary:

Reviewed the cross-language implementation covering C# configuration layer, Python user config integration, and XAML UI components. Applied pyRevit's exception handling standards from the validation checklist and evaluated performance implications of UI marshalling patterns. The feature architecture follows established pyRevit configuration patterns well.

Follow-up suggestions:

  • @devloai fix the identified exception handling and review the UI marshalling approach
  • @devloai test the feature with multiple concurrent script executions to validate timing behavior

var filterId = filterByCommandId ? this.OutputId : null;
ScriptConsoleManager.CloseActiveOutputWindows(excludeOutputWindow: this, filterOutputWindowId: filterId);
}
catch {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Empty catch block violates exception handling standards. According to the review validation checklist, empty catch blocks should log exceptions at minimum. Consider logging the exception or using more specific exception types.

catch (Exception ex) {
    logger.Debug("Failed to close other output windows: " + ex.Message);
}
actions

Feedback: Rate this comment to help me improve future code reviews:

  • 👍 Good - Helpful and accurate
  • 👎 Poor - Wrong, unclear, or unhelpful
  • Skip if you don't have any strong opinions either way.

if (PyRevitConfigs.GetCloseOtherOutputs())
{
var mode = PyRevitConfigs.GetCloseOutputMode();
this.Dispatcher.BeginInvoke(new Action(() =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Using BeginInvoke for UI marshalling may introduce unnecessary delay. Consider whether the console closing operation needs to be asynchronous or if Invoke (synchronous) would be more appropriate for immediate feedback.

The current implementation may cause timing issues where the new console opens before others finish closing.

actions

Feedback: Rate this comment to help me improve future code reviews:

  • 👍 Good - Helpful and accurate
  • 👎 Poor - Wrong, unclear, or unhelpful
  • Skip if you don't have any strong opinions either way.

var cfg = GetConfigFile();
var raw = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey);

var s = (raw ?? PyRevitConsts.ConfigsCloseOutputModeDefault).Trim().Trim('"', '\'');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The string trimming logic with both quotation marks seems unusual and may indicate a configuration parsing issue. The standard pyRevit configuration pattern doesn't typically require quote trimming. Consider whether this complexity is necessary or if it masks an underlying issue.

var s = (raw ?? PyRevitConsts.ConfigsCloseOutputModeDefault).Trim();
actions

Feedback: Rate this comment to help me improve future code reviews:

  • 👍 Good - Helpful and accurate
  • 👎 Poor - Wrong, unclear, or unhelpful
  • Skip if you don't have any strong opinions either way.

@jmcouffin jmcouffin added the New Feature New feature request [class->Implemented #{number}: {title}] label Sep 21, 2025
@MohamedAsli
Copy link
Copy Markdown
Contributor Author

@dosymep your expertise is still needed here 😊

var cfg = GetConfigFile();
var raw = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey);

var s = (raw ?? PyRevitConsts.ConfigsCloseOutputModeDefault).Trim().Trim('"', '\'');
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.

As far as I remember, you don't need to trim it yourself, it should do it automatically

def output_close_others(self):
"""Whether to close other output windows."""
return self.core.get_option(
'close_other_outputs',
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.

Sorry, I didn't notice that you wrote the code where it should be :D

Comment on lines +320 to +348
def output_close_mode_enum(self):
"""Output window closing mode as enum (CurrentCommand | CloseAll)."""
value = self.core.get_option(
CONSTS.ConfigsCloseOutputModeKey,
default_value=CONSTS.ConfigsCloseOutputModeDefault,
)
if not value:
value = CONSTS.ConfigsCloseOutputModeDefault

@output_close_mode.setter
def output_close_mode(self, mode):
self.core.set_option(
'close_mode',
value=mode
)
value_lc = str(value).lower()

if value_lc == str(CONSTS.ConfigsCloseOutputModeCloseAll).lower():
return PyRevit.OutputCloseMode.CloseAll
else:
return PyRevit.OutputCloseMode.CurrentCommand

@output_close_mode_enum.setter
def output_close_mode_enum(self, mode):
"""Store string in INI, mapped from enum."""
if mode == PyRevit.OutputCloseMode.CloseAll:
self.core.set_option(
CONSTS.ConfigsCloseOutputModeKey,
value=CONSTS.ConfigsCloseOutputModeCloseAll
)
else:
self.core.set_option(
CONSTS.ConfigsCloseOutputModeKey,
value=CONSTS.ConfigsCloseOutputModeCurrentCommand
)
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 think it's good, I hope in the future I will rewrite all the code for working with settings, and there will be a more convenient option

Comment on lines +320 to +348
def output_close_mode_enum(self):
"""Output window closing mode as enum (CurrentCommand | CloseAll)."""
value = self.core.get_option(
CONSTS.ConfigsCloseOutputModeKey,
default_value=CONSTS.ConfigsCloseOutputModeDefault,
)
if not value:
value = CONSTS.ConfigsCloseOutputModeDefault

@output_close_mode.setter
def output_close_mode(self, mode):
self.core.set_option(
'close_mode',
value=mode
)
value_lc = str(value).lower()

if value_lc == str(CONSTS.ConfigsCloseOutputModeCloseAll).lower():
return PyRevit.OutputCloseMode.CloseAll
else:
return PyRevit.OutputCloseMode.CurrentCommand

@output_close_mode_enum.setter
def output_close_mode_enum(self, mode):
"""Store string in INI, mapped from enum."""
if mode == PyRevit.OutputCloseMode.CloseAll:
self.core.set_option(
CONSTS.ConfigsCloseOutputModeKey,
value=CONSTS.ConfigsCloseOutputModeCloseAll
)
else:
self.core.set_option(
CONSTS.ConfigsCloseOutputModeKey,
value=CONSTS.ConfigsCloseOutputModeCurrentCommand
)
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.

remove only the enum suffix in name, I don't think users need to see it

@jmcouffin jmcouffin requested a review from dosymep October 7, 2025 01:53
@jmcouffin
Copy link
Copy Markdown
Contributor

@dosymep do you mind taking a last look at it and let us know if it is good enough to be tested and merged?

Copy link
Copy Markdown
Member

@dosymep dosymep left a comment

Choose a reason for hiding this comment

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

I suggest several possible solutions.

  1. Use an enum for the operating modes (but my solution needs to be double-checked).
  2. Write the entire code without using an enum, only for string comparisons (if the first option doesn't work for some reason).

var cfg = GetConfigFile();
var raw = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey);

var s = (raw ?? PyRevitConsts.ConfigsCloseOutputModeDefault).Trim().Trim('"', '\'');
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.

remove trim, the pyRevit library trimmed values by default

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.

public static OutputCloseMode GetCloseOutputMode()
{
    var cfg = GetConfigFile();
    var raw = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey);

    return Enum.TryParse<OutputCloseMode>(raw, true, out OutputCloseMode outCloseMode) 
        ? outCloseMode 
        : OutputCloseMode.CurrentCommand;
}

Comment on lines +543 to +551
public static void SetCloseOutputMode(OutputCloseMode mode)
{
var cfg = GetConfigFile();
var value = (mode == OutputCloseMode.CloseAll)
? PyRevitConsts.ConfigsCloseOutputModeCloseAll
: PyRevitConsts.ConfigsCloseOutputModeCurrentCommand;

cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey, value);
}
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.

public static void SetCloseOutputMode(OutputCloseMode outputCloseMode)
{
    var cfg = GetConfigFile();
    cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey, outputCloseMode.ToString());
}

Comment on lines +91 to +93
public const string ConfigsCloseOutputModeDefault = "currentcommand";
public const string ConfigsCloseOutputModeCurrentCommand = "currentcommand";
public const string ConfigsCloseOutputModeCloseAll = "closeall";
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.

can remove I think

Comment on lines +336 to +347
@output_close_mode_enum.setter
def output_close_mode_enum(self, mode):
"""Store string in INI, mapped from enum."""
if mode == PyRevit.OutputCloseMode.CloseAll:
self.core.set_option(
CONSTS.ConfigsCloseOutputModeKey,
value=CONSTS.ConfigsCloseOutputModeCloseAll
)
else:
self.core.set_option(
CONSTS.ConfigsCloseOutputModeKey,
value=CONSTS.ConfigsCloseOutputModeCurrentCommand
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.

@property
def output_close_mode(self):
    """Output window closing mode as enum (CurrentCommand | CloseAll)."""
    value = self.core.get_option(
        CONSTS.ConfigsCloseOutputModeKey,
        default_value=CONSTS.ConfigsCloseOutputModeDefault,
    )

   if System.Enum.TryParse[PyRevit.OutputCloseMode](value, true, out PyRevit.OutputCloseMode output_close_mode):
        return output_close_mode
        
   return PyRevit.OutputCloseMode.CurrentCommand

@output_close_mode.setter
def output_close_mode(self, output_close_mode):
    """Store string in INI, mapped from enum."""
    self.core.set_option(
        CONSTS.ConfigsCloseOutputModeKey, 
        value=str(output_close_mode)
    )

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 renamed methods (remove enum suffix)

@MohamedAsli
Copy link
Copy Markdown
Contributor Author

@jmcouffin, I’ll test this as soon as I have a moment to breathe

@jmcouffin
Copy link
Copy Markdown
Contributor

I did test it, It works fine
Image for the release
image
@MohamedAsli If you can test it today, I have some free time and would like to make a new release while including this PR

@jmcouffin jmcouffin changed the title Allow for an option to close other outputs for all scripts in pyrevit settings feat: Allow for an option to close other outputs for all scripts in pyrevit settings Oct 16, 2025
@jmcouffin jmcouffin self-assigned this Oct 20, 2025
Copy link
Copy Markdown
Contributor

@jmcouffin jmcouffin left a comment

Choose a reason for hiding this comment

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

Tested again this morning. It works as expected and the thread is already a pain to load in github. I will approve and merge and ask you guys to make the improvements in a future PR

@jmcouffin jmcouffin merged commit a9a44fc into pyrevitlabs:develop Oct 23, 2025
@jmcouffin
Copy link
Copy Markdown
Contributor

Some AI review that my be useful for future changes:

Code Review and fixes

Here are the before/after code sections based on the review feedback:

1. GetCloseOutputMode() - Remove unnecessary trimming

Before:

public static OutputCloseMode GetCloseOutputMode()
{
    var cfg = GetConfigFile();
    var raw = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey);
    
    var s = (raw ?? PyRevitConsts.ConfigsCloseOutputModeDefault).Trim().Trim('"', '\'');
    
    if (string.Equals(s, PyRevitConsts.ConfigsCloseOutputModeCloseAll, StringComparison.InvariantCultureIgnoreCase))
        return OutputCloseMode.CloseAll;
    else
        return OutputCloseMode.CurrentCommand;
}

After:

public static OutputCloseMode GetCloseOutputMode()
{
    var cfg = GetConfigFile();
    var raw = cfg.GetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey);
    
    return Enum.TryParse<OutputCloseMode>(raw, true, out OutputCloseMode outCloseMode)
        ? outCloseMode
        : OutputCloseMode.CurrentCommand;
}

2. SetCloseOutputMode() - Simplify enum conversion

Before:

public static void SetCloseOutputMode(OutputCloseMode mode)
{
    var cfg = GetConfigFile();
    var value = (mode == OutputCloseMode.CloseAll)
        ? PyRevitConsts.ConfigsCloseOutputModeCloseAll
        : PyRevitConsts.ConfigsCloseOutputModeCurrentCommand;
    
    cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey, value);
}

After:

public static void SetCloseOutputMode(OutputCloseMode outputCloseMode)
{
    var cfg = GetConfigFile();
    cfg.SetValue(PyRevitConsts.ConfigsCoreSection, PyRevitConsts.ConfigsCloseOutputModeKey, outputCloseMode.ToString());
}

3. PyRevitConsts.cs - Remove unnecessary constants

Before:

public const string ConfigsCloseOutputModeDefault = "currentcommand";
public const string ConfigsCloseOutputModeCurrentCommand = "currentcommand";
public const string ConfigsCloseOutputModeCloseAll = "closeall";

After:

// Remove these constants - enum ToString() handles it

4. Python userconfig.py - Simplify enum handling

Before:

@property
def output_close_mode_enum(self):
    """Output window closing mode as enum (CurrentCommand | CloseAll)."""
    value = self.core.get_option(
        CONSTS.ConfigsCloseOutputModeKey,
        default_value=CONSTS.ConfigsCloseOutputModeDefault,
    )
    if not value:
        value = CONSTS.ConfigsCloseOutputModeDefault
    
    value_lc = str(value).lower()
    
    if value_lc == str(CONSTS.ConfigsCloseOutputModeCloseAll).lower():
        return PyRevit.OutputCloseMode.CloseAll
    else:
        return PyRevit.OutputCloseMode.CurrentCommand

@output_close_mode_enum.setter
def output_close_mode_enum(self, mode):
    """Store string in INI, mapped from enum."""
    if mode == PyRevit.OutputCloseMode.CloseAll:
        self.core.set_option(
            CONSTS.ConfigsCloseOutputModeKey,
            value=CONSTS.ConfigsCloseOutputModeCloseAll
        )
    else:
        self.core.set_option(
            CONSTS.ConfigsCloseOutputModeKey,
            value=CONSTS.ConfigsCloseOutputModeCurrentCommand
        )

After:

@property
def output_close_mode(self):
    """Output window closing mode as enum (CurrentCommand | CloseAll)."""
    value = self.core.get_option(
        CONSTS.ConfigsCloseOutputModeKey,
        default_value=CONSTS.ConfigsCloseOutputModeDefault,
    )
    if System.Enum.TryParse[PyRevit.OutputCloseMode](value, True, out PyRevit.OutputCloseMode output_close_mode):
        return output_close_mode
    return PyRevit.OutputCloseMode.CurrentCommand

@output_close_mode.setter
def output_close_mode(self, output_close_mode):
    """Store string in INI, mapped from enum."""
    self.core.set_option(
        CONSTS.ConfigsCloseOutputModeKey,
        value=str(output_close_mode)
    )

5. Empty catch blocks - Add logging

Before:

try
{
    var filterId = filterByCommandId ? this.OutputId : null;
    ScriptConsoleManager.CloseActiveOutputWindows(excludeOutputWindow: this, filterOutputWindowId: filterId);
}
catch { }

After:

try
{
    var filterId = filterByCommandId ? this.OutputId : null;
    ScriptConsoleManager.CloseActiveOutputWindows(excludeOutputWindow: this, filterOutputWindowId: filterId);
}
catch (Exception ex)
{
    logger.Debug("Failed to close other output windows: " + ex.Message);
}

@github-actions
Copy link
Copy Markdown
Contributor

📦 New work-in-progress (wip) builds are available for 5.2.0.25296+0837-wip

@github-actions
Copy link
Copy Markdown
Contributor

📦 New work-in-progress (wip) builds are available for 5.2.0.25296+0923-wip

@github-actions
Copy link
Copy Markdown
Contributor

📦 New work-in-progress (wip) builds are available for 5.2.0.25300+1037-wip

@github-actions
Copy link
Copy Markdown
Contributor

📦 New work-in-progress (wip) builds are available for 5.2.0.25301+1412-wip

@github-actions
Copy link
Copy Markdown
Contributor

📦 New work-in-progress (wip) builds are available for 5.2.0.25301+1521-wip

@github-actions
Copy link
Copy Markdown
Contributor

📦 New work-in-progress (wip) builds are available for 5.2.0.25302+0921-wip

@github-actions
Copy link
Copy Markdown
Contributor

📦 New work-in-progress (wip) builds are available for 5.2.0.25302+0949-wip

@github-actions
Copy link
Copy Markdown
Contributor

📦 New work-in-progress (wip) builds are available for 5.2.0.25302+1503-wip

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 3, 2025

📦 New work-in-progress (wip) builds are available for 5.2.0.25307+1644-wip

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 3, 2025

📦 New work-in-progress (wip) builds are available for 5.2.0.25307+1935-wip

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 3, 2025

📦 New work-in-progress (wip) builds are available for 5.2.0.25307+2011-wip

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 3, 2025

📦 New work-in-progress (wip) builds are available for 5.2.0.25307+2045-wip

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 3, 2025

📦 New work-in-progress (wip) builds are available for 5.3.0.25307+2237-wip

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 4, 2025

📦 New work-in-progress (wip) builds are available for 5.3.0.25308+0721-wip

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 4, 2025

📦 New public release are available for 5.3.0.25307+2146

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

New Feature New feature request [class->Implemented #{number}: {title}]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow for an option to 'close other outputs' for all scripts in pyrevit settings

4 participants