Documentation History
-
I think a feature that kept a total history of document creation and editing, storing all keypresses. Effectively this is a key logger but would be limited to the creation and editing of a document. I apologise if this is already available and if it isn’t would it be possible for me to create an addon to do this?
-
I don’t know of any plugins that actually log that or give the user access (though that doesn’t mean it doesn’t exist). I know that Scintilla (the underlying editor I.P.) does have some change-history tracking (undo/redo is one piece of evidence; the new change-history margins are another), but I don’t believe either Scintilla or Notepad++ gives direct access to those details of what all the changes were.
“Would it be possible for” you could be interpreted two different ways: 1) Do you have permission? Yes; the Plugin Communication is published at https://npp-user-manual.org/docs/plugin-communication/ and anyone is allowed to make a plugin for Notepad++ using that Communication; after making it, you can either keep it to yourself, do your own distribution, or submit it to the nppPluginList project to request that it be added to the Plugins Admin inside Notepad++ for easy installation. 2) Is it technically possible and do you have the skills? The first is uncertain to me (it depends on how one might hook into the process, but it wouldn’t surprise me if given enough effort it were feasible); the second is unknowable to me, because I don’t know your skills.
-
Here’s a simple PythonScript script that does something reasonably similar to what you’re looking for.
from Npp import * import datetime import json from pathlib import Path # create a subdirectory of the PythonScript scripts directory called mod_logger LOGDIR = Path(__file__).parent / 'mod_logger' LOGFILE = LOGDIR / 'mod.log' MSG = '{time}@{fname}: {modlog}\n' FNAME = None def on_bufferactivate(notif): global FNAME FNAME = notepad.getCurrentFilename() def on_filesaved(notif): global FNAME formatted_msg = MSG.format( time=datetime.datetime.now().ctime(), fname=FNAME, modlog = '"fileSaved"' ) with LOGFILE.open('a') as f: f.write(formatted_msg) def on_keypress(notif): global FNAME # see https://www.scintilla.org/ScintillaDoc.html#SCN_MODIFIED mod_type = notif['modificationType'] keylog = {'position': notif['position']} if mod_type & 0x01 != 0: keylog['textAdded'] = notif['text'] elif mod_type & 0x02 != 0: keylog['textRemoved'] = notif['text'] else: return formatted_msg = MSG.format( time=datetime.datetime.now().ctime(), fname=FNAME, modlog=json.dumps(keylog) ) with LOGFILE.open('a') as f: f.write(formatted_msg) if __name__ == '__main__': try: INITIALIZED except NameError: INITIALIZED = True if not LOGDIR.exists(): LOGDIR.mkdir() if not LOGFILE.exists(): with LOGFILE.open('w') as f: pass # I *WANT* to use SCINTILLANOTIFICATION.KEY, which would *actually* # log keypresses, # but for some reason that notification never fires, # and SCINTILLANOTIFICATION.MODIFIED is the closest we've got FNAME = notepad.getCurrentFilename() editor.callback(on_keypress, [SCINTILLANOTIFICATION.MODIFIED]) notepad.callback(on_bufferactivate, [NOTIFICATION.BUFFERACTIVATED]) notepad.callback(on_filesaved, [NOTIFICATION.FILEBEFORESAVE])
And here’s the log it generated from inserting a newline and then the text
asdf
, and then selecting what I just added and also anf
on the previous line and deleting it, then saving the file.Note that all the messages have the same format: the date, the character
@
, the filename, and then some description of the event. The description is always syntactically valid JSON, which is a useful property.Tue Jun 6 15:23:36 2023@Path\To\AppData\Roaming\Notepad++\plugins\config\PythonScript\scripts\asdf.nroern: {"position": 4, "textAdded": "\r\n"} Tue Jun 6 15:23:36 2023@Path\To\AppData\Roaming\Notepad++\plugins\config\PythonScript\scripts\asdf.nroern: {"position": 6, "textAdded": "a"} Tue Jun 6 15:23:37 2023@Path\To\AppData\Roaming\Notepad++\plugins\config\PythonScript\scripts\asdf.nroern: {"position": 7, "textAdded": "s"} Tue Jun 6 15:23:37 2023@Path\To\AppData\Roaming\Notepad++\plugins\config\PythonScript\scripts\asdf.nroern: {"position": 8, "textAdded": "d"} Tue Jun 6 15:23:37 2023@Path\To\AppData\Roaming\Notepad++\plugins\config\PythonScript\scripts\asdf.nroern: {"position": 9, "textAdded": "f"} Tue Jun 6 15:23:40 2023@Path\To\AppData\Roaming\Notepad++\plugins\config\PythonScript\scripts\asdf.nroern: {"position": 4, "textRemoved": "\r\nasdf"} Tue Jun 6 15:23:40 2023@Path\To\AppData\Roaming\Notepad++\plugins\config\PythonScript\scripts\asdf.nroern: {"position": 3, "textRemoved": "f"} Tue Jun 6 15:23:41 2023@Path\To\AppData\Roaming\Notepad++\plugins\config\PythonScript\scripts\asdf.nroern: "fileSaved"
-
I felt like my previous script was suboptimal as a keylogger because:
- it could only be parsed by regular expressions
- it generated a pretty verbose log with every keystroke, which seemed like an easy way to rapidly create a multi-megabyte log file.
I’ve updated the keylogger so that:
- the log file is a JSON Lines document, which is very easy to parse (just split it into lines and use your favorite JSON parser to parse each line)
- the date is only logged when the date changes or when the logger first starts up
- The current filename is only logged when the file is saved or opened.
- key logs have the format
{"+": <text added>, "t": hours:minutes:seconds, "pos": position}
, with"-": <text removed>
for text removal operations.
While the user must later re-associate filenames and dates with each modification log, this is a very simple task.
Below is an example log:
{"date":"2023-06-07"} {"fileOpened":"Path\\To\\AppData\\Roaming\\Notepad++\\plugins\\config\\PythonScript\\scripts\\keylogger.py","t":"14:38:16"} {"fileOpened":"Path\\To\\Python311\\example_silly.txt","t":"14:38:16"} {"+":"a","pos":0,"t":"14:38:18"} {"+":"s","pos":1,"t":"14:38:18"} {"+":"d","pos":2,"t":"14:38:18"} {"+":"f","pos":3,"t":"14:38:18"} {"+":"\r\n","pos":4,"t":"14:38:19"} {"-":"asdf\r\n","pos":0,"t":"14:38:20"} {"fileSaved":"Path\\To\\Python311\\example_silly.txt","t":"14:38:21"}
And here’s the new script:
from Npp import * from datetime import datetime import json from pathlib import Path # create a subdirectory of the PythonScript scripts directory called mod_logger LOGDIR = Path(__file__).parent / 'mod_logger' LOGFILE = LOGDIR / 'mod.jsonl' # create a JSON Lines document in that directory def compact_json_dump(obj): return json.dumps(obj, separators=(',', ':')) def date_to_int(date): return (date.year << 16) | (date.month << 8) | date.day class ModLogger: def __init__(self): self.FNAME = notepad.getCurrentFilename() self.date_int = 0 self.log_date_change() def log(self, obj): with LOGFILE.open('a') as f: f.write(compact_json_dump(obj)) f.write('\n') def log_date_change(self): now = datetime.now() dint = date_to_int(now) if dint > self.date_int: self.date_int = dint self.log({'date': '%d-%02d-%02d' % (now.year, now.month, now.day)}) return now def on_bufferactivate(self, notif): self.FNAME = notepad.getCurrentFilename() now = self.log_date_change() self.log({'fileOpened': self.FNAME, 't': '%02d:%02d:%02d' % (now.hour, now.minute, now.second)}) def on_filesaved(self, notif): now = self.log_date_change() self.log({'fileSaved': self.FNAME, 't': '%02d:%02d:%02d' % (now.hour, now.minute, now.second)}) def on_modification(self, notif): # see https://www.scintilla.org/ScintillaDoc.html#SCN_MODIFIED mod_type = notif['modificationType'] keylog = {} if mod_type & 0x01 != 0: keylog['+'] = notif['text'] elif mod_type & 0x02 != 0: keylog['-'] = notif['text'] else: return keylog['pos'] = notif['position'] now = self.log_date_change() keylog['t'] = '%02d:%02d:%02d' % (now.hour, now.minute, now.second) self.log(keylog) if __name__ == '__main__': try: MOLO except NameError: MOLO = ModLogger() if not LOGDIR.exists(): LOGDIR.mkdir() if not LOGFILE.exists(): with LOGFILE.open('w') as f: pass # I *WANT* to use SCINTILLANOTIFICATION.KEY, which would *actually* # log keypresses, # but for some reason that notification never fires, # and SCINTILLANOTIFICATION.MODIFIED is the closest we've got editor.callback(MOLO.on_modification, [SCINTILLANOTIFICATION.MODIFIED]) notepad.callback(MOLO.on_bufferactivate, [NOTIFICATION.BUFFERACTIVATED]) notepad.callback(MOLO.on_filesaved, [NOTIFICATION.FILEBEFORESAVE])
-