Skip to content

Commit b2d8ebf

Browse files
authored
Merge 1a872fb into 8a82bd5
2 parents 8a82bd5 + 1a872fb commit b2d8ebf

8 files changed

Lines changed: 354 additions & 288 deletions

File tree

source/argsParsing.py

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
# -*- coding: UTF-8 -*-
2+
# A part of NonVisual Desktop Access (NVDA)
3+
# Copyright (C) 2024 NV Access Limited, Cyrille Bougot
4+
# This file is covered by the GNU General Public License.
5+
# See the file COPYING for more details.
6+
7+
import argparse
8+
import sys
9+
import winUser
10+
11+
from typing import IO
12+
13+
14+
class _WideParserHelpFormatter(argparse.RawTextHelpFormatter):
15+
def __init__(self, prog: str, indent_increment: int = 2, max_help_position: int = 50, width: int = 1000):
16+
"""
17+
A custom formatter for argparse help messages that uses a wider width.
18+
:param prog: The program name.
19+
:param indent_increment: The number of spaces to indent for each level of nesting.
20+
:param max_help_position: The maximum starting column of the help text.
21+
:param width: The width of the help text.
22+
"""
23+
24+
super().__init__(prog, indent_increment, max_help_position, width)
25+
26+
27+
class NoConsoleOptionParser(argparse.ArgumentParser):
28+
"""
29+
A commandline option parser that shows its messages using dialogs,
30+
as this pyw file has no dos console window associated with it.
31+
"""
32+
33+
def print_help(self, file: IO[str] | None = None):
34+
"""Shows help in a standard Windows message dialog"""
35+
winUser.MessageBox(0, self.format_help(), "Help", 0)
36+
37+
def error(self, message: str):
38+
"""Shows an error in a standard Windows message dialog, and then exits NVDA"""
39+
out = ""
40+
out = self.format_usage()
41+
out += f"\nerror: {message}"
42+
winUser.MessageBox(0, out, "Command-line Argument Error", winUser.MB_ICONERROR)
43+
sys.exit(2)
44+
45+
46+
def stringToBool(string):
47+
"""Wrapper for configobj.validate.is_boolean to raise the proper exception for wrong values."""
48+
from configobj.validate import is_boolean, ValidateError
49+
50+
try:
51+
return is_boolean(string)
52+
except ValidateError as e:
53+
raise argparse.ArgumentTypeError(e.message)
54+
55+
56+
def stringToLang(value: str) -> str:
57+
"""Perform basic case normalization for ease of use."""
58+
import languageHandler
59+
60+
if value.casefold() == "Windows".casefold():
61+
normalizedLang = "Windows"
62+
else:
63+
normalizedLang = languageHandler.normalizeLanguage(value)
64+
possibleLangNames = languageHandler.listNVDALocales()
65+
if normalizedLang is not None and normalizedLang in possibleLangNames:
66+
return normalizedLang
67+
raise argparse.ArgumentTypeError(f"Language code should be one of:\n{', '.join(possibleLangNames)}.")
68+
69+
70+
_parser: NoConsoleOptionParser | None = None
71+
"""The arguments parser used by NVDA.
72+
"""
73+
74+
75+
def _createNVDAArgParser() -> NoConsoleOptionParser:
76+
"""Create a parser to process NVDA option arguments."""
77+
78+
parser = NoConsoleOptionParser(formatter_class=_WideParserHelpFormatter, allow_abbrev=False)
79+
quitGroup = parser.add_mutually_exclusive_group()
80+
quitGroup.add_argument(
81+
"-q",
82+
"--quit",
83+
action="store_true",
84+
dest="quit",
85+
default=False,
86+
help="Quit already running copy of NVDA",
87+
)
88+
parser.add_argument(
89+
"-k",
90+
"--check-running",
91+
action="store_true",
92+
dest="check_running",
93+
default=False,
94+
help="Report whether NVDA is running via the exit code; 0 if running, 1 if not running",
95+
)
96+
parser.add_argument(
97+
"-f",
98+
"--log-file",
99+
dest="logFileName",
100+
type=str,
101+
help="The file to which log messages should be written.\n"
102+
'Default destination is "%%TEMP%%\\nvda.log".\n'
103+
"Logging is always disabled if secure mode is enabled.\n",
104+
)
105+
parser.add_argument(
106+
"-l",
107+
"--log-level",
108+
dest="logLevel",
109+
type=int,
110+
default=0, # 0 means unspecified in command line.
111+
choices=[10, 12, 15, 20, 100],
112+
help="The lowest level of message logged (debug 10, input/output 12, debugwarning 15, info 20, off 100).\n"
113+
"Default value is 20 (info) or the user configured setting.\n"
114+
"Logging is always disabled if secure mode is enabled.\n",
115+
)
116+
parser.add_argument(
117+
"-c",
118+
"--config-path",
119+
dest="configPath",
120+
default=None,
121+
type=str,
122+
help="The path where all settings for NVDA are stored.\n"
123+
"The default value is forced if secure mode is enabled.\n",
124+
)
125+
parser.add_argument(
126+
"-n",
127+
"--lang",
128+
dest="language",
129+
default=None,
130+
type=stringToLang,
131+
help=(
132+
"Override the configured NVDA language.\n"
133+
'Set to "Windows" for current user default, "en" for English, etc.'
134+
),
135+
)
136+
parser.add_argument(
137+
"-m",
138+
"--minimal",
139+
action="store_true",
140+
dest="minimal",
141+
default=False,
142+
help="No sounds, no interface, no start message etc",
143+
)
144+
# --secure is used to force secure mode.
145+
# Documented in the userGuide in #SecureMode.
146+
parser.add_argument(
147+
"-s",
148+
"--secure",
149+
action="store_true",
150+
dest="secure",
151+
default=False,
152+
help="Starts NVDA in secure mode",
153+
)
154+
parser.add_argument(
155+
"-d",
156+
"--disable-addons",
157+
action="store_true",
158+
dest="disableAddons",
159+
default=False,
160+
help="Disable all add-ons",
161+
)
162+
parser.add_argument(
163+
"--debug-logging",
164+
action="store_true",
165+
dest="debugLogging",
166+
default=False,
167+
help="Enable debug level logging just for this run.\n"
168+
"This setting will override any other log level (--loglevel, -l) argument given, "
169+
"as well as no logging option.",
170+
)
171+
parser.add_argument(
172+
"--no-logging",
173+
action="store_true",
174+
dest="noLogging",
175+
default=False,
176+
help="Disable logging completely for this run.\n"
177+
"This setting can be overwritten with other log level (--loglevel, -l) "
178+
"switch or if debug logging is specified.",
179+
)
180+
parser.add_argument(
181+
"--no-sr-flag",
182+
action="store_false",
183+
dest="changeScreenReaderFlag",
184+
default=True,
185+
help="Don't change the global system screen reader flag",
186+
)
187+
installGroup = parser.add_mutually_exclusive_group()
188+
installGroup.add_argument(
189+
"--install",
190+
action="store_true",
191+
dest="install",
192+
default=False,
193+
help="Installs NVDA (starting the new copy after installation)",
194+
)
195+
installGroup.add_argument(
196+
"--install-silent",
197+
action="store_true",
198+
dest="installSilent",
199+
default=False,
200+
help="Installs NVDA silently (does not start the new copy after installation).",
201+
)
202+
installGroup.add_argument(
203+
"--create-portable",
204+
action="store_true",
205+
dest="createPortable",
206+
default=False,
207+
help="Creates a portable copy of NVDA (and starts the new copy).\n"
208+
"Requires `--portable-path` to be specified.\n",
209+
)
210+
installGroup.add_argument(
211+
"--create-portable-silent",
212+
action="store_true",
213+
dest="createPortableSilent",
214+
default=False,
215+
help="Creates a portable copy of NVDA (without starting the new copy).\n"
216+
"This option suppresses warnings when writing to non-empty directories "
217+
"and may overwrite files without warning.\n"
218+
"Requires --portable-path to be specified.\n",
219+
)
220+
parser.add_argument(
221+
"--portable-path",
222+
dest="portablePath",
223+
default=None,
224+
type=str,
225+
help="The path where a portable copy will be created",
226+
)
227+
parser.add_argument(
228+
"--launcher",
229+
action="store_true",
230+
dest="launcher",
231+
default=False,
232+
help="Started from the launcher",
233+
)
234+
parser.add_argument(
235+
"--enable-start-on-logon",
236+
metavar="True|False",
237+
type=stringToBool,
238+
dest="enableStartOnLogon",
239+
default=None,
240+
help="When installing, enable NVDA's start on the logon screen",
241+
)
242+
parser.add_argument(
243+
"--copy-portable-config",
244+
action="store_true",
245+
dest="copyPortableConfig",
246+
default=False,
247+
help=(
248+
"When installing, copy the portable configuration "
249+
"from the provided path (--config-path, -c) to the current user account"
250+
),
251+
)
252+
# This option is passed by Ease of Access so that if someone downgrades without uninstalling
253+
# (despite our discouragement), the downgraded copy won't be started in non-secure mode on secure desktops.
254+
# (Older versions always required the --secure option to start in secure mode.)
255+
# If this occurs, the user will see an obscure error,
256+
# but that's far better than a major security hazzard.
257+
# If this option is provided, NVDA will not replace an already running instance (#10179)
258+
parser.add_argument(
259+
"--ease-of-access",
260+
action="store_true",
261+
dest="easeOfAccess",
262+
default=False,
263+
help="Started by Windows Ease of Access",
264+
)
265+
return parser
266+
267+
268+
def getParser() -> NoConsoleOptionParser:
269+
global _parser
270+
if not _parser:
271+
_parser = _createNVDAArgParser()
272+
return _parser

source/core.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import logHandler
2525
import languageHandler
2626
import globalVars
27+
import argsParsing
2728
from logHandler import log
2829
import addonHandler
2930
import extensionPoints
@@ -214,6 +215,28 @@ class NewNVDAInstance:
214215
directory: Optional[str] = None
215216

216217

218+
def computeRestartCLIArgs(removeArgsList: list[str] | None = None) -> list[str]:
219+
"""Generate an equivalent list of CLI arguments from the values in globalVars.appArgs.
220+
:param removeArgsList: A list of values to ignore when looking in globalVars.appArgs.
221+
"""
222+
223+
parser = argsParsing.getParser()
224+
if not removeArgsList:
225+
removeArgsList = []
226+
args = []
227+
for arg, val in globalVars.appArgs._get_kwargs():
228+
if val == parser.get_default(arg):
229+
continue
230+
if arg in removeArgsList:
231+
continue
232+
flag = [a.option_strings[0] for a in parser._actions if a.dest == arg][0]
233+
args.append(flag)
234+
if isinstance(val, bool):
235+
continue
236+
args.append(f"{val}")
237+
return args
238+
239+
217240
def restartUnsafely():
218241
"""Start a new copy of NVDA immediately.
219242
Used as a last resort, in the event of a serious error to immediately restart NVDA without running any
@@ -239,13 +262,16 @@ def restartUnsafely():
239262
sys.argv.remove(paramToRemove)
240263
except ValueError:
241264
pass
265+
restartCLIArgs = computeRestartCLIArgs(
266+
removeArgsList=["easeOfAccess"],
267+
)
242268
options = []
243269
if NVDAState.isRunningAsSource():
244270
options.append(os.path.basename(sys.argv[0]))
245271
_startNewInstance(
246272
NewNVDAInstance(
247273
sys.executable,
248-
subprocess.list2cmdline(options + sys.argv[1:]),
274+
subprocess.list2cmdline(options + restartCLIArgs),
249275
globalVars.appDir,
250276
),
251277
)
@@ -260,15 +286,9 @@ def restart(disableAddons=False, debugLogging=False):
260286
return
261287
import subprocess
262288

263-
for paramToRemove in (
264-
"--disable-addons",
265-
"--debug-logging",
266-
"--ease-of-access",
267-
) + languageHandler.getLanguageCliArgs():
268-
try:
269-
sys.argv.remove(paramToRemove)
270-
except ValueError:
271-
pass
289+
restartCLIArgs = computeRestartCLIArgs(
290+
removeArgsList=["disableAddons", "debugLogging", "language", "easeOfAccess"],
291+
)
272292
options = []
273293
if NVDAState.isRunningAsSource():
274294
options.append(os.path.basename(sys.argv[0]))
@@ -280,7 +300,7 @@ def restart(disableAddons=False, debugLogging=False):
280300
if not triggerNVDAExit(
281301
NewNVDAInstance(
282302
sys.executable,
283-
subprocess.list2cmdline(options + sys.argv[1:]),
303+
subprocess.list2cmdline(options + restartCLIArgs),
284304
globalVars.appDir,
285305
),
286306
):

source/globalVars.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2006-2022 NV Access Limited, Łukasz Golonka, Leonard de Ruijter, Babbage B.V.,
2+
# Copyright (C) 2006-2024 NV Access Limited, Łukasz Golonka, Leonard de Ruijter, Babbage B.V.,
33
# Aleksey Sadovoy, Peter Vágner
44
# This file may be used under the terms of the GNU General Public License, version 2 or later.
55
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
@@ -41,7 +41,7 @@ class DefaultAppArgs(argparse.Namespace):
4141
logFileName: Optional[os.PathLike] = ""
4242
logLevel: int = 0
4343
configPath: Optional[os.PathLike] = None
44-
language: str = "en"
44+
language: str | None = None
4545
minimal: bool = False
4646
secure: bool = False
4747
"""

source/gui/settingsDialogs.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -800,16 +800,9 @@ def makeSettings(self, settingsSizer):
800800
self.languageNames = languageHandler.getAvailableLanguages(presentational=True)
801801
languageChoices = [x[1] for x in self.languageNames]
802802
if languageHandler.isLanguageForced():
803-
try:
804-
cmdLangDescription = next(
805-
ld for code, ld in self.languageNames if code == globalVars.appArgs.language
806-
)
807-
except StopIteration:
808-
# In case --lang=Windows is passed to the command line, globalVars.appArgs.language is the current
809-
# Windows language,, which may not be in the list of NVDA supported languages, e.g. Windows language may
810-
# be 'fr_FR' but NVDA only supports 'fr'.
811-
# In this situation, only use language code as a description.
812-
cmdLangDescription = globalVars.appArgs.language
803+
cmdLangDescription = next(
804+
ld for code, ld in self.languageNames if code == globalVars.appArgs.language
805+
)
813806
languageChoices.append(
814807
# Translators: Shown for a language which has been provided from the command line
815808
# 'langDesc' would be replaced with description of the given locale.

0 commit comments

Comments
 (0)