Repository: nvaccess/nvda

Issue:
Set start and end point in a logfile via a hotkey and save this marked part on the Windows Desktop automatically
### Original suggestion by @AbhitejJohn:
Quote from [his comment on issue #9275](https://github.com/nvaccess/nvda/issues/9275#issuecomment-463547835):
> Also I wish I could clear/reset the logs from the UI :)

### Steps to reproduce:
1. Change the log level to "debug" in the General NVDA Settings.
2. Open the Log Viewer via the NVDA menu.
3. Try to remember the actual last line.
4. Open the start menu. Or just do anything else. It doesn't matter. But its logs are very, very important for you.
5. Switch back to the Log Viewer.
6. Have fun by finding the previous remembered line.
### Actual behaviour:
It isn't possible to clear the logged content until step 4. It has to deleted with Notepad++ or with any other useful source code editor manually, after the logfile was saved first.
### Expected behaviour:
Add the following hotkeys to NVDA:
+ NVDA+Shift+F2: Set start point in the logfile.
+ NVDA+CTRL+F2: Set end point in the logfile.

After NVDA+CTRL+F2 was pressed, the content between the start and the end point is selected and copied into a new logfile which is automatically saved on the Windows Desktop. The filename for this new logfile is based on the current timestamp, e.g. "NVDA_2019-02-15-012345.log".
### System configuration:
#### NVDA installed/portable:
Both
#### NVDA version:
2018.4.1
#### Windows version:
Win7 to Win10-1903


Discussion:
NVDA+Ctrl+F2 is already used by test_navigatorDisplayModelText
@lukaszgo1: Thanks for the hint – I didn't know this. Well, then just replace F2 with F4 (or any other useful key). Or just change NVDA+CTRL+F2 to NVDA+Shift+F4. NVDA+Shift+F3 is already used to search backwards.
[**@lukaszgo1**](https://github.com/lukaszgo1) commented on [15 Feb 2019, 13:14 CET](https://github.com/nvaccess/nvda/issues/9280#issuecomment-464027229 "2019-02-15T12:14:36Z - Replied by Github Reply Comments"):
> NVDA+Ctrl+F2 is already used by test_navigatorDisplayModelText

Wow, this script is actually entirely new for me.

Personally, I agree with the proposal to have a way to extract certain parts of the log for easy recording of log data. However, saving the log to the desktop by default sounds a bit too much overkill for me, it could easily lead up to a desktop full of these log snippets. I think that for such functionality, I prefer the clipboard.
> I think that for such functionality, I prefer the clipboard.

Well, sounds logical to me too. In the end the user always has to save the logfile on a different place than the Windows Desktop. So it doesn't matter if he moves the logfile itself or if he first has to paste the content of the copied section of the logfile into a new, blank text file. (Hopefully he isn't using Microsoft Word for it, like those who are saving screenshots within a Word document. Okay, that's a completely different topic. 😉)

But there is one issue with the clipboard method: The user could save the file in the wrong encoding. The encoding of the "nvda.log" is UTF-8 (without BOM) with CRLF line ending. The format of the line ending wouldn't be a problem, but not using UTF-8 could lead into one.

Resolving patch:
diff --git a/source/globalCommands.py b/source/globalCommands.py
--- a/source/globalCommands.py
+++ b/source/globalCommands.py
@@ -3,7 +3,8 @@
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
 # Copyright (C) 2006-2020 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Rui Batista, Joseph Lee,
-# Leonard de Ruijter, Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Łukasz Golonka
+# Leonard de Ruijter, Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Łukasz Golonka, Accessolutions,
+# Julien Cochuyt
 
 import time
 import itertools
@@ -1714,6 +1715,42 @@ def script_navigatorObject_devInfo(self,gesture):
 	script_navigatorObject_devInfo.__doc__ = _("Logs information about the current navigator object which is useful to developers and activates the log viewer so the information can be examined.")
 	script_navigatorObject_devInfo.category=SCRCAT_TOOLS
 
+	@script(
+		description=_(
+			# Translators: Input help mode message for a command to delimit then
+			# copy a fragment of the log to clipboard
+			"Mark the current end of the log as the start of the fragment to be"
+			" copied to clipboard by pressing again."
+		),
+		category=SCRCAT_TOOLS,
+		gesture="kb:NVDA+control+shift+f1"
+	)
+	def script_log_markStartThenCopy(self, gesture):
+		if globalVars.appArgs.secure:
+			return
+		if log.fragmentStart is None:
+			if log.markFragmentStart():
+				# Translators: Message when marking the start of a fragment of the log file for later copy
+				# to clipboard
+				ui.message(_("Log fragment start position marked, press again to copy to clipboard"))
+			else:
+				# Translators: Message when failed to mark the start of a
+				# fragment of the log file for later copy to clipboard
+				ui.message(_("Unable to mark log position"))
+			return
+		text = log.getFragment()
+		if not text:
+			# Translators: Message when attempting to copy an empty fragment of the log file
+			ui.message(_("No new log entry to copy"))
+			return
+		if api.copyToClip(text):
+			# Translators: Message when a fragment of the log file has been
+			# copied to clipboard
+			ui.message(_("Log fragment copied to clipboard"))
+		else:
+			# Translators: Presented when unable to copy to the clipboard because of an error.
+			ui.message(_("Unable to copy"))
+
 	@script(
 		# Translators: Input help mode message for Open user configuration directory command.
 		description=_("Opens NVDA configuration directory for the current user."),
diff --git a/source/logHandler.py b/source/logHandler.py
--- a/source/logHandler.py
+++ b/source/logHandler.py
@@ -1,5 +1,6 @@
 # A part of NonVisual Desktop Access (NVDA)
-# Copyright (C) 2007-2020 NV Access Limited, Rui Batista, Joseph Lee, Leonard de Ruijter, Babbage B.V.
+# Copyright (C) 2007-2020 NV Access Limited, Rui Batista, Joseph Lee, Leonard de Ruijter, Babbage B.V.,
+# Accessolutions, Julien Cochuyt
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
 
@@ -113,6 +114,11 @@ class Logger(logging.Logger):
 	DEBUGWARNING = 15
 	OFF = 100
 
+	#: The start position of a fragment of the log file as marked with
+	#: L{markFragmentStart} for later retrieval using L{getFragment}.
+	#: @type: C{long}
+	fragmentStart = None
+
 	def _log(self, level, msg, args, exc_info=None, extra=None, codepath=None, activateLogViewer=False, stack_info=None):
 		if not extra:
 			extra={}
@@ -189,6 +195,48 @@ def exception(self, msg="", exc_info=True, **kwargs):
 			return
 		self._log(level, msg, (), exc_info=exc_info, **kwargs)
 
+	def markFragmentStart(self):
+		"""Mark the current end of the log file as the start position of a
+		fragment to be later retrieved by L{getFragment}.
+		@returns: Whether a log file is in use and a position could be marked
+		@rtype: bool
+		"""
+		if (
+			not globalVars.appArgs
+			or globalVars.appArgs.secure
+			or not globalVars.appArgs.logFileName
+			or not isinstance(logHandler, FileHandler)
+		):
+			return False
+		with open(globalVars.appArgs.logFileName, "r", encoding="UTF-8") as f:
+			# _io.TextIOWrapper.seek: whence=2 -- end of stream
+			f.seek(0, 2)
+			self.fragmentStart = f.tell()
+			return True
+
+	def getFragment(self):
+		"""Retrieve a fragment of the log starting from the position marked using
+		L{markFragmentStart}.
+		If L{fragmentStart} does not point to the current end of the log file, it
+		is reset to C{None} after reading the fragment.
+		@returns: The text of the fragment, or C{None} if L{fragmentStart} is None.
+		@rtype: str
+		"""
+		if (
+			self.fragmentStart is None
+			or not globalVars.appArgs
+			or globalVars.appArgs.secure
+			or not globalVars.appArgs.logFileName
+			or not isinstance(logHandler, FileHandler)
+		):
+			return None
+		with open(globalVars.appArgs.logFileName, "r", encoding="UTF-8") as f:
+			f.seek(self.fragmentStart)
+			fragment = f.read()
+			if fragment:
+				self.fragmentStart = None
+			return fragment
+
 class RemoteHandler(logging.Handler):
 
 	def __init__(self):


Test patch: