Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions dev/pyRevitLabs.PyRevit.Runtime/ScriptConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,64 @@ public string GetFullHtml() {
return ScriptConsoleConfigs.DOCTYPE + head.OuterHtml + ActiveDocument.Body.OuterHtml;
}

private static (bool closeOthers, string closeMode) ReadCloseOtherOutputsSetting() {
Copy link
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 :)

try {
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var cfg = System.IO.Path.Combine(appData, "pyRevit", "pyRevit_config.ini");
if (!System.IO.File.Exists(cfg))
return (false, "current_command");

bool inCore = false;
bool close = false;
string mode = "current_command";
foreach (var raw in System.IO.File.ReadAllLines(cfg)) {
var line = raw.Trim();
if (line.Length == 0 || line.StartsWith("#") || line.StartsWith(";"))
continue;

if (line == "[core]") { inCore = true; continue; }
if (line.StartsWith("[") && line.EndsWith("]")) { inCore = false; continue; }

if (!inCore) continue;

if (line.StartsWith("close_other_outputs", StringComparison.InvariantCultureIgnoreCase))
close = line.IndexOf("true", StringComparison.InvariantCultureIgnoreCase) >= 0;
Copy link
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.

Copy link
Preview

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.


else if (line.StartsWith("close_mode", StringComparison.InvariantCultureIgnoreCase)) {
var parts = line.Split(new[] { '=' }, 2);
if (parts.Length == 2)
mode = parts[1].Trim().Trim('"');
}
}
return (close, mode);
}
catch {
Copy link
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.

return (false, "current_command");
}
}

public void CloseOtherOutputs(bool filterByCommandId = true) {
try {
var filterId = filterByCommandId ? this.OutputId : null;
ScriptConsoleManager.CloseActiveOutputWindows(excludeOutputWindow: this, filterOutputWindowId: filterId);
}
catch {
Copy link
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.

Copy link
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.

}
}

private void ApplyCloseOtherOutputsIfConfigured() {
var (close, mode) = ReadCloseOtherOutputsSetting();
if (!close) return;

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
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
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
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)

}

private void SetupDefaultPage(string styleSheetFilePath = null) {
string cssFilePath;
if (styleSheetFilePath != null)
Expand Down Expand Up @@ -448,7 +506,7 @@ public void FocusOutput() {

public System.Windows.Forms.HtmlElement ComposeEntry(string contents, string HtmlElementType) {
WaitReadyBrowser();

// order is important
// "<" ---> &lt;
contents = ScriptConsoleConfigs.EscapeForHtml(contents);
Expand Down Expand Up @@ -552,7 +610,7 @@ public string GetInput() {
dbgMode = true;
}
}

// if no debugger, find other patterns
if (!dbgMode &&
new string[] { "select", "file" }.All(x => lastLine.Contains(x)))
Expand Down Expand Up @@ -782,6 +840,7 @@ public void SelfDestructTimer(int seconds) {
private void Window_Loaded(object sender, System.EventArgs e) {
var outputWindow = (ScriptConsole)sender;
ScriptConsoleManager.AppendToOutputWindowList(this);
ApplyCloseOtherOutputsIfConfigured();
}

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@
<system:String x:Key="CoreSettings.Development.Description">Misc options for pyRevit development</system:String>
<system:String x:Key="CoreSettings.Development.LoadBeta">Load Beta Tools (Scripts with __beta__ = True, Reload is required)</system:String>

<system:String x:Key="CoreSettings.CloseOtherConsoles">Close other open consoles</system:String>
<system:String x:Key="CoreSettings.CloseOtherConsoles.Description">
If enabled, pyRevit will close other output consoles when a new script is run.
This helps to reduce the number of open output windows. Activate this option, unless you want to compare
multiple output data.
</system:String>

<system:String x:Key="CoreSettings.CloseOtherConsoles.Current">Current command consoles</system:String>
<system:String x:Key="CoreSettings.CloseOtherConsoles.Current.Description">
Close all console windows associated with the current running command.
</system:String>

<system:String x:Key="CoreSettings.CloseOtherConsoles.CloseAll">All consoles</system:String>
<system:String x:Key="CoreSettings.CloseOtherConsoles.CloseAll.Description">
Close all currently open console windows, even those that are not associated with the running command.
</system:String>

<system:String x:Key="CoreSettings.Caching">Caching</system:String>
<system:String x:Key="CoreSettings.Caching.Button">Reset Caching to default</system:String>
<system:Double x:Key="CoreSettings.Caching.Button.With">200</system:Double>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@
<system:String x:Key="CoreSettings.Development.Description">Options diverses pour le développement de pyRevit</system:String>
<system:String x:Key="CoreSettings.Development.LoadBeta">Charger les outils bêta (Scripts avec __beta__ = True, le rechargement est requis)</system:String>

<system:String x:Key="CoreSettings.CloseOtherConsoles">Fermer les consoles ouvertes</system:String>
<system:String x:Key="CoreSettings.CloseOtherConsoles.Description">
Si activé, pyRevit fermera les autres consoles de sortie lors de l'exécution d'un nouveau script.
Cela permet de réduire le nombre de fenêtres de sortie ouvertes. Activez cette option, sauf si vous
voulez comparez les sorties de plusieurs scripts.
</system:String>

<system:String x:Key="CoreSettings.CloseOtherConsoles.Current">Consoles de la commande actuelle</system:String>
<system:String x:Key="CoreSettings.CloseOtherConsoles.Current.Description">
Fermer toutes les fenêtres de console associées à la commande en cours d'exécution.
</system:String>

<system:String x:Key="CoreSettings.CloseOtherConsoles.CloseAll">Toutes les consoles</system:String>
<system:String x:Key="CoreSettings.CloseOtherConsoles.CloseAll.Description">
Fermer toutes les fenêtres de console actuellement ouvertes, même celles qui ne sont pas associées à la commande en cours.
</system:String>

<system:String x:Key="CoreSettings.Caching">Mise en cache</system:String>
<system:String x:Key="CoreSettings.Caching.Button">Reset la mise en cache par défaut</system:String>
<system:Double x:Key="CoreSettings.Caching.Button.With">200</system:Double>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@
<system:String x:Key="CoreSettings.Development.Description">Разные настройки разработки pyRevit</system:String>
<system:String x:Key="CoreSettings.Development.LoadBeta">Загружать бета-инструменты (Скрипты с __beta__ = True, требуется перезапуск)</system:String>

<system:String x:Key="CoreSettings.CloseOtherConsoles">Закрывать другие открытые консоли</system:String>
<system:String x:Key="CoreSettings.CloseOtherConsoles.Description">
Если включено, pyRevit будет закрывать другие консоли вывода при запуске нового скрипта.
Это помогает уменьшить количество открытых окон вывода. Активируйте эту опцию, если вам
не требуется сравнивать данные из нескольких окон вывода.
</system:String>

<system:String x:Key="CoreSettings.CloseOtherConsoles.Current">Консоли текущей команды</system:String>
<system:String x:Key="CoreSettings.CloseOtherConsoles.Current.Description">
Закрыть все окна консоли, связанные с текущей выполняемой командой.
</system:String>

<system:String x:Key="CoreSettings.CloseOtherConsoles.CloseAll">Все консоли</system:String>
<system:String x:Key="CoreSettings.CloseOtherConsoles.CloseAll.Description">
Закрыть все открытые в данный момент окна консоли, даже те, которые не связаны с выполняемой командой.
</system:String>

<system:String x:Key="CoreSettings.Caching">Кеширование</system:String>
<system:String x:Key="CoreSettings.Caching.Button">Сбросить настройки кеширования</system:String>
<system:Double x:Key="CoreSettings.Caching.Button.With">220</system:Double>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,23 @@
<ToggleButton Style="{StaticResource AnimatedSwitch}" Height="24" x:Name="loadbetatools_cb" IsChecked="False"/>
<TextBlock Margin="30,4,0,0" Text="{DynamicResource CoreSettings.Development.LoadBeta}"/>
</WrapPanel>

<StackPanel Margin="10,5,10,10">
<TextBlock FontWeight="Medium" Margin="0,5,0,0" Text="{DynamicResource CoreSettings.CloseOtherConsoles}"/>
<TextBlock TextWrapping="WrapWithOverflow" Margin="0,5,0,0" Text="{DynamicResource CoreSettings.CloseOtherConsoles.Description}"/>
<WrapPanel Margin="0,15,0,0">
<ToggleButton Style="{StaticResource AnimatedSwitch}" Height="24" x:Name="minimize_consoles_cb" IsChecked="False"/>
<TextBlock Margin="30,4,0,0" Text="Minimize the number of open consoles:" />
</WrapPanel>
</StackPanel>

<StackPanel Margin="26,0,0,10" IsEnabled="{Binding ElementName=minimize_consoles_cb, Path=IsChecked}">
<RadioButton x:Name="closewindows_current_rb" GroupName="console_close_mode" Margin="0,5,0,5" IsChecked="False" Content="{DynamicResource CoreSettings.CloseOtherConsoles.Current}"/>
<TextBlock TextWrapping="WrapWithOverflow" Margin="20,0,0,5" Text="{DynamicResource CoreSettings.CloseOtherConsoles.Current.Description}"/>

<RadioButton x:Name="closewindows_close_all_rb" GroupName="console_close_mode" Margin="0,10,0,5" IsChecked="False" Content="{DynamicResource CoreSettings.CloseOtherConsoles.CloseAll}"/>
<TextBlock TextWrapping="WrapWithOverflow" Margin="20,0,0,0" Text="{DynamicResource CoreSettings.CloseOtherConsoles.CloseAll.Description}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<GroupBox Header="{DynamicResource CoreSettings.Caching}" Margin="0,10,0,0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ tooltip:

Zeigt die Konfigurationsdatei im Explorer an.
context: zero-doc
engine:
clean: true
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ def _setup_core_options(self):

self.loadbetatools_cb.IsChecked = user_config.load_beta

self.minimize_consoles_cb.IsChecked = user_config.output_close_others

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

self.closewindows_current_rb.IsChecked = True
self.closewindows_close_all_rb.IsChecked = False
else: # 'close_all'
self.closewindows_current_rb.IsChecked = False
self.closewindows_close_all_rb.IsChecked = True

def _setup_engines(self):
"""Sets up the list of available engines."""
attachment = user_config.get_current_attachment()
Expand Down Expand Up @@ -846,6 +855,12 @@ def _save_core_options(self):

user_config.load_beta = self.loadbetatools_cb.IsChecked

user_config.output_close_others = self.minimize_consoles_cb.IsChecked
if self.closewindows_current_rb.IsChecked:
user_config.output_close_mode = 'current_command'
else:
user_config.output_close_mode = 'close_all'

def _save_engines(self):
# set active cpython engine
engine_cfg = self.cpythonEngines.SelectedItem
Expand Down
30 changes: 15 additions & 15 deletions pyrevitlib/pyrevit/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ def get_data_file(file_id, file_ext, add_cmd_name=False):
script.get_data_file('mydata', 'data', add_cmd_name=True)
```
'/pyRevit_2018_Command Name_mydata.data'


Data files are not cleaned up at pyRevit startup.
Script should manage cleaning up these files.
Expand Down Expand Up @@ -547,7 +547,7 @@ def load_ui(ui_instance, ui_file='ui.xaml', handle_esc=True, set_owner=True):
"""Load xaml file into given window instance.

If window instance defines a method named `setup` it
will be called after loading
will be called after loading

Args:
ui_instance (forms.WPFWindow): ui form instance
Expand Down Expand Up @@ -679,29 +679,29 @@ def store_data(slot_name, data, this_project=True):
```python
from pyrevit import revit
from pyrevit import script


class CustomData(object):
def __init__(self, count, element_ids):
self._count = count
# serializes the Revit native objects
self._elmnt_ids = [revit.serialize(x) for x in element_ids]

@property
def count(self):
return self._count

@property
def element_ids(self):
# de-serializes the Revit native objects
return [x.deserialize() for x in self._elmnt_ids]


mydata = CustomData(
count=3,
element_ids=[<DB.ElementId>, <DB.ElementId>, <DB.ElementId>]
)

script.store_data("Selected Elements", mydata)
```

Expand Down Expand Up @@ -740,24 +740,24 @@ def load_data(slot_name, this_project=True):
```python
from pyrevit import revit
from pyrevit import script


class CustomData(object):
def __init__(self, count, element_ids):
self._count = count
# serializes the Revit native objects
self._elmnt_ids = [revit.serialize(x) for x in element_ids]

@property
def count(self):
return self._count

@property
def element_ids(self):
# de-serializes the Revit native objects
return [x.deserialize() for x in self._elmnt_ids]


mydata = script.load_data("Selected Elements")
mydata.element_ids
```
Expand Down
44 changes: 44 additions & 0 deletions pyrevitlib/pyrevit/userconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,21 @@
from pyrevit.coreutils import configparser
from pyrevit.coreutils import logger
from pyrevit.versionmgr import upgrade
# pylint: disable=C0103,C0413,W0703
import os
import os.path as op

from pyrevit import EXEC_PARAMS, HOME_DIR, HOST_APP
from pyrevit import EXTENSIONS_DEFAULT_DIR, THIRDPARTY_EXTENSIONS_DEFAULT_DIR
from pyrevit import PYREVIT_ALLUSER_APP_DIR, PYREVIT_APP_DIR
from pyrevit import PyRevitException
from pyrevit import coreutils
from pyrevit.compat import winreg as wr
from pyrevit.coreutils import appdata
from pyrevit.coreutils import configparser
from pyrevit.coreutils import logger
from pyrevit.labs import PyRevit
from pyrevit.versionmgr import upgrade

DEFAULT_CSV_SEPARATOR = ','

Expand Down Expand Up @@ -302,6 +316,36 @@ def load_beta(self, state):
value=state
)

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

default_value=False,
)

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

value=state
)

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

default_value='current_command',
)

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

value=mode
)

@property
def cpython_engine_version(self):
"""CPython engine version to use."""
Expand Down