Auto Logger data capture script with AutoHotKey- part 2
In Auto Logger and timesheet exploration with AutoHotKey and Python Part 1 I gave an overview of the project, in this article I want to go into the development of the background process of auto-logging computer activity on the PC.
My initial start, as mentioned in the previous article was to start with the code in article Find Where You Waste Your Time on Windows With AutoHotKey by Odysseas Kourafalos, the code is as below.
#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn ; Enable warnings to assist with detecting common errors.
SendMode Input ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
; Variables
; ---------
AppLoggingRate = 10 ; Time interval (in seconds) between active window title captures.
SleepTime := AppLoggingRate * 1000
LogPath = %A_ScriptDir%
LastActiveWindow =
; Logic
; -----
Loop
{
Sleep %SleepTime%
WinGetActiveTitle, ActiveWindow
StoreActiveWindow = %ActiveWindow%
If ActiveWindow != %LastActiveWindow%
{
FormatTime, LogTime,, HH:mm:ss
FormatTime, LogFilename, , yyyy-MM-dd
LogWindow := Regexreplace(ActiveWindow, "[^a-zA-Z0-9]", " ")
LogFilename = %LogFilename%_AppLog.md
LogFile = %LogPath%\%LogFilename%
FileContent = `n%LogTime% - %LogWindow%`n`n- - -`n
sleep 50
FileAppend, %FileContent%, %LogFile%
LastActiveWindow = %ActiveWindow%
}
}
Exit
The output is a text based markdown file and so not easy to use afterwards, so straight away changed the output to: LogFilename = %LogFilename%_AppLog.csv
Developing the code to see what could use AutoHotKey to get with its own internal variables
At first glance it seems simple, at its most basic level you just record the current Active Window and when you switch windows you stop recording the old window and create a new log line for the new window. But what do you record?
As I decided that I was going to do this project in AutoHotKey there are some internal AutoHotKey variables we can use for leverage such as window title, window class, window ahk_ID and Process ID. We can obtain these items with either Windows Spy or using Pulover’s Macro creator Window button


So my immediate action was to log these details to see what the output would be.
I also noted that the original time was for start time of when an active window was started and it was in H:M:S format. I decided to also get Epoch or UTC time setup as well, as this is better for time addition and subtraction,as you are only adding/subtracting time in one variable, seconds, rather than having to do more complex arithmatic to add/subtract H:M:S from H:M:S. At the end we can always do the conversion to the time format that we want after doing the maths.
#NoEnv
#SingleInstance, Force
SendMode, Input
SetBatchLines, -1
SetWorkingDir, %A_ScriptDir%
; Variables
; ---------
AppLoggingRate = 30 ; Time interval (in seconds) between active window title captures.
SleepTime := AppLoggingRate * 1000
LogPath = %A_ScriptDir%
LastActiveWindow =
; Logic
; -----
Loop
{
Sleep %SleepTime%
WinGetActiveTitle, ActiveWindow
StoreActiveWindow = %ActiveWindow%
WinGet, activePID, PID, A
WinGet, activeID, ID, A
WinGetClass, activeClass, A
If ActiveWindow != %LastActiveWindow%
{
FormatTime, LogTime,, HH:mm:ss
FormatTime, LogFilename, , yyyy-MM-dd
LogWindow := Regexreplace(ActiveWindow, "[^a-zA-Z0-9]", " ")
LogFilename = %LogFilename%_AppLog.csv
LogFile = %LogPath%\%LogFilename%
FileContent = `n%A_NowUTC% , %LogTime% , %LogWindow% , %activeClass%, %activeID%, %activePID%
;FileContent = `n%A_NowUTC% , %LogTime% , %LogWindow% , %ahk_class%, %ahk_id%, %ahk_pid%
sleep 50
FileAppend, %FileContent%, %LogFile%
LastActiveWindow = %ActiveWindow%
}
}
Exit
So the raw output became as below (I added headers in file for clarity)
UTC Start Time, H:M:S start time, Windows Title, Win Class, AHK_ID, PID (Process)
20230129192931 , 08:29:31 , AutoHotKey get active window class at DuckDuckGo Mozilla Firefox , 0x304de, 0x304de, 9876
20230129193001 , 08:30:01 , ScreenLogger ahk AHK Working Visual Studio Code , 0x50b70, 0x50b70, 17268
20230129193049 , 08:30:49 , WinGetClass Syntax Usage AutoHotkey Mozilla Firefox , MozillaWindowClass, 0x304de, 9876
20230129193119 , 08:31:19 , Scoop New Zealand News Mozilla Firefox , MozillaWindowClass, 0x304de, 9876
20230129193149 , 08:31:49 , Four dead or missing after flooding Police Scoop News Mozilla Firefox , MozillaWindowClass, 0x304de, 9876
On looking at the output of a much larger data set a few things became obvious:
- The Win Classes were not unique to specific programs, I use Firefox (Browser) and Thunderbird (Email management) and they both have the same winclass “MozillaWindowClass”. Also VS Code & Obsidian have the same win class “Chrome_WidgetWin_1”
- The ahk_ID & Process ID can change as soon as you open a new instance of a program. Also when you close an instance down of a program and open it again it creates new ahk_ID’s & Process ID’s.
The only thing that seems to be unique to a logging of an active window is the win title that is broken into a number of elements. For a browser, it states the program , maybe the main site and a particular title on that site. For a program, VS Code in this instance, it will show program, directory that is active and the filename that is actively open.
This is not very satisfactory, there are too many things that are transient and which can change, especially from computer to computer. The one thing we could use is the Win Title with a lot of string manipulation to extract program being used and activity, but this will require a lot of coding and for loops. Is there a better way?
We go back to the original purpose of the AutoLogger which is for Time Sheet information and to see where we are wasting time. Things look in too much of a state of flux to help us in this matter, we need to do something else.
If you look at Task Manager you can see Process ID’s of Programs , there can be several relating to the same program, such as the 6 AutoHotKey ones (I’ running 6 different scripts in the background) and lots of VSCode ones (one for main program and one for each tab/file open & if I was to close any one down and start again it would change its PID :

AutoHotKey WinGet using Programme.exe ID
So we are starting to define the issues, we only have start times of the activities, we have variables for the programs that change and we need unique identifiers so that we can classify what program we are using for activities.
If we go back to the origina l code there is a loop that is activated at specific intervals, initially set at 10 seconds, where it gets the ActiveWindowTitle, and then the next part of the code is an IF statement where it compares the current Active Window with the previous active window, if they are the same then it does nothing, but if they are different then it creates a new log with a start time of A_now(), which gives the current time.
Loop
{
Sleep %SleepTime%
WinGetActiveTitle, ActiveWindow
StoreActiveWindow = %ActiveWindow%
If ActiveWindow != %LastActiveWindow%
{
So we can use the area in the loop, before the IF statement to go and find what the ahk_ID & Process ID of a specific program is with the following AutoHotKey function, where we give it the program name that we want to find the active, but not necessarily current Active window (ie it can be minimised or not actively open (left pane or right pane in a split screen)) of that particular program :
WinGet, ff_hWind, PID, ahk_exe Firefox.exe
So what this will return is the PID, which will be the same if you have 2 or 3 instances open of the same program, unlike ahk_ID which is different for every open instance of a program.
One program that doesn’t work this way is explorer.exe, the reason being, if you have no instances of File Explorer open there will still be a PID. So when you call the PID it gives you the underlying Process, not the current open windows. So the way to manage this is by class as it is “CabinetWClass”. Explorer is a unique case.
At the end of the day, we are not really interested in the PID or ahk_ID, as these will change across computers and active programs, what we want is to identify the correct active program, and we use the PID to find this, and then use a logic such as :
if activePID = %tb_hWind%
{
prog = tb ; thunderbird
FileContent = `n%Duration%,%LogTime%,%LogWindow%,%prog%,
}
if activePID = %ob_hWind%
{
prog = ob ; obsidian
FileContent = `n%Duration%,%LogTime%,%LogWindow%,%prog%,
}
if (activeClass != CabinetWClass) and (activePID = )
{
prog = ??
FileContent = `n%Duration%,%LogTime%,%LogWindow%,%prog%,
}
Where we compare the activeWindowPID with ones we have obtained for specific programs , and if they match, then we add an abbreviation to indicate what program that is. If there isn’t a match then we can just put a different marker to indicate its not one of the specific programs we are monitoring, say “Paint.exe” that you use very rarely. By giving them a specific mark, in this case “??” we can cluster these together and review them later to see if we want to add the specific activity to the timesheet. Or see if we are stating to use other programs more actively, in which case we can specifically monitor them. Their only unique identifier will be by Win Title.
Duration of Activities
We are not really interested in Start Time of an activity, but rather start AND end times, so we can calculate duration of how long we were doing a task. So we need some method of getting the end time of a project. To do this in the loop in the program we need to hold the Time Variable A_Now() that gives current time ( In fact we will use A_NowUTC() that gives us the information in seconds-(we could use something like SetTimer but I’ve been using Epoch time and it has more information in it)).
So every 10 seconds the program repeats the loop of checking to see if currentActiveWindow is the same as LastActiveWindow, and as soon as that changes then it logs a new line to file, and adds the current A_Now() to the new line in the log as the start time. So it only gets the A_Now() if it meets the IF statement requirements to create a new line.
So in that inner IF statement we can also add a variable, EndTime = %A_NowUTC% , which was the start time of the current log line. Because its inside of the IF statement, it’s a local variable and its reset every time that IF statement is exited, so we have to convert it into a GLOBAL variable, as we do in the code, by assigning global EndTime before the loop begins.
global EndTime
Loop
{
Sleep %SleepTime%
WinGetActiveTitle, ActiveWindow
StoreActiveWindow = %ActiveWindow%
If ActiveWindow != %LastActiveWindow%
{
FormatTime, LogTime,, HH:mm:ss
;Other stuff happens here ........
;We write the times to be appended to the file here
FileContent = `n%A_NowUTC% ,%EndTime% ,%Duration%, %Dur%, %LogTime% , %LogWindow% , %activeClass%, %activeID%, %activePID%,
; NOTE- We write EndTime to file before we define it below! Otherwise Start & ;End Times would be the same
FileAppend, %FileContent%, %LogFile%
LastActiveWindow = %ActiveWindow%
EndTime = %A_NowUTC% ; assign start time to EndTime var for next loop, so know when previous time completed
;msgbox, EndTime =%EndTime%
}
}
Exit
For duration and time conversion from Seconds to Hours:Minutes:Seconds we can use the following
Duration := % A_NowUTC - EndTime
if (Duration == "")
Duration := 0
Duration := Floor(Duration)
Hours := Floor(Duration/3600)
Minutes := Floor((Duration - (Hours * 3600)) / 60)
Seconds := Duration - (Hours * 3600) - (Minutes * 60)
Dur = %Hours%:%Minutes%:%Seconds%
This will calculate the Duration in seconds of time spent on that particular activity and also Dur in H:M:S for that activity, we don’t actually need the Dur as we will be doing some more maths, summing up categories of activities later, so we will convert to H:M:S at a later time, its just added here for an example on how to convert seconds to H:M:S.
A little curly thing about this is the Duration relates to the activity that has already been logged into the file, and we only calculate it when we are about to write the new line in the file for the new activity. So how to we attach the duration to the correct activity?
The way we do this is when we write the next activity we put the DURATION first, and then start a new line. That way the duration is tagged onto the end of the previous activity.
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,%sURL%,%prog%,
Not adding headers to file at this point
When I transfer file to be processed with Python Pandas it comes into Dataframe without a header, and so header needs to be added for columns so that the data can be better manipulated.
If I tried adding Headers while logging, after writing the header I’d need to start on a new line to ensure that the duration is not added as an extra column in the dataframe.
Regex stuff
In original code there is a line of regex conversion, see below, that I removed, I have since replaced it. The reason for this is that in the titles of articles etc there can be a “comma” which will add another column to a file so it becomes messy to process in Python. Maybe there is a way to filter out just the “comma”. This may need exploring.
LogWindow := Regexreplace(ActiveWindow, "[^a-zA-Z0-9]", " ")
At some point I expect I’ll have to go in and do some string editing on titles to get further information for the timesheet.
Getting URL for Browser
One thing that I saw in the Python logger video was that if you were in browser Hallden also got the URL of the site he was visiting. He used some string editing to get the specific site, so if it was something like ‘stackOverflow” he would classify that as an activity to add to his timesheet, but if it was say “Fox News” he would ingnore it.
This brings about 2 issues:
1/ You are adding another column of data to one program, but not to others, this has an impact when transferring across to Python Panda’s dataframes which will give an error with uneven column numbers, so for other progams you have to add another empty comma for the added column. This is only picked up when I tested the data by trying to process it in Python.
2/ Getting the URL programmatically from the browser. This I had several attempts at, as it can be different for other browsers. In the end I found, on the AHK forum a class that could be used to do this and so I used that. Prior to that I’d have to go to active window and, for firefox use F6 or !d to focus the cursor in the address bar, then copy the contents to clipboard and later paste the clipboard contents into the variable for appending to the file, it worked, to an extent but it was a specific firefox solution. The class method was much better.
Switching between programs to test output
After I’d roughly sorted out the output that gets written to a daily file, I was able to look at importing it into Python Dataframes to look at how I could themn process the output.
I had quite a lot of debugging to do to get the format right, to ensure that the data was clean and uploaded neatly into Pandas dataframes. I’m still working on this.
Currently, as “DURATION” is written on a line and then a new line created, when starting the file at the beginning there is a “0” on the top line. So importing into dataFrames it reads the file as having only one column and this is not correct. So I’ll have to find a method to either add a header at the beginning of the file or find a method to delete the first row of the file at some point.
The Current code script
#NoEnv
#SingleInstance, Force
SendMode, Input
SetBatchLines, -1
SetWorkingDir, %A_ScriptDir%
; Variables
; ---------
AppLoggingRate = 10 ; Time interval (in seconds) between active window title captures.
SleepTime := AppLoggingRate * 1000
LogPath = %A_ScriptDir%
LastActiveWindow =
global EndTime
global prog
ModernBrowsers := "ApplicationFrameWindow,Chrome_WidgetWin_0,Chrome_WidgetWin_1,Maxthon3Cls_MainFrm,MozillaWindowClass,Slimjet_WidgetWin_1"
LegacyBrowsers := "IEFrame,OperaWindowClass"
FormatTime, LogFilename, , yyyy-MM-dd
LogFilename = %LogFilename%_AppLog.csv
LogFile = %LogPath%\%LogFilename%
if !FileExist(LogFile)
FileAppend, %FileHeader%, %LogFile%
; Logic
; -----
Loop
{
WinGetActiveTitle, ActiveWindow
StoreActiveWindow = %ActiveWindow%
WinGet, activePID, PID, A
WinGet, activeID, ID, A
WinGetClass, activeClass, A
WinGet, ff_hWind, PID, ahk_exe firefox.exe
WinGet, co_hWind, PID, ahk_exe code.exe
WinGet, tb_hWind, PID, ahk_exe thunderbird.exe
WinGet, ob_hWind, PID, ahk_exe obsidian.exe
WinGet, npp_hWind, PID, ahk_exe notepad++.exe
WinGet, np_hWind, PID, ahk_exe notepad.exe
WinGet, xl_hWind, PID, ahk_exe excel.exe
WinGet, wd_hWind, PID, ahk_exe word.exe
;msgbox, co_hWind %co_hWind% tb_hWind %tb_hWind% ob_hWind %ob_hWind% npp_hWind %npp_hWind% xl_hWind %xl_hWind%
If ActiveWindow != %LastActiveWindow%
{
FormatTime, LogTime,, HH:mm:ss
FormatTime, LogFilename, , yyyy-MM-dd
LogWindow := Regexreplace(ActiveWindow, "[^a-zA-Z0-9]", " ")
LogFilename = %LogFilename%_AppLog.csv
LogFile = %LogPath%\%LogFilename%
Duration := % A_NowUTC - EndTime
if (Duration == "")
Duration := 0
Duration := Floor(Duration)
Hours := Floor(Duration/3600)
Minutes := Floor((Duration - (Hours * 3600)) / 60)
Seconds := Duration - (Hours * 3600) - (Minutes * 60)
Dur = %Hours%:%Minutes%:%Seconds%
if activePID = %ff_hWind%
{
sURL := GetActiveBrowserURL()
prog=ff
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,%sURL%,%prog%,
}
if activeClass = CabinetWClass
{
prog = ex
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,,%prog%,
}
if activePID = %co_hWind%
{
prog = cd
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,,%prog%,
}
if activePID = %tb_hWind%
{
prog = tb ; thunderbird
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,,%prog%,
}
if activePID = %ob_hWind%
{
prog = ob ; obsidian
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,,%prog%,
}
if activePID = %xl_hWind%
{
prog =xl ;Excel
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,,%prog%,
}
if activePID = %wd_hWind%
{
prog = wd
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,,%prog%,
}
if activePID = %npp_hWind%
{
prog = pn ;plus notes , notepad++
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,,%prog%,
}
if activePID = %np_hWind%
{
prog = np
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,,%prog%,
}
if activePID =
{
prog = 00
FileContent = %Duration% `n%A_NowUTC%,%LogTime%,%LogWindow%,,%prog%,
}
sleep 50
FileAppend, %FileContent%, %LogFile%
LastActiveWindow = %ActiveWindow%
EndTime = %A_NowUTC% ; assign start time to EndTime var for next loop, so know when previous time completed
}
}
Exit
;CLASSESfor Browser
GetActiveBrowserURL() {
global ModernBrowsers, LegacyBrowsers
WinGetClass, sClass, A
If sClass In % ModernBrowsers
Return GetBrowserURL_ACC(sClass)
Else If sClass In % LegacyBrowsers
Return GetBrowserURL_DDE(sClass) ; empty string if DDE not supported (or not a browser)
Else
Return ""
}
; "GetBrowserURL_DDE" adapted from DDE code by Sean, (AHK_L version by maraskan_user)
; Found at http://autohotkey.com/board/topic/17633-/?p=434518
GetBrowserURL_DDE(sClass) {
WinGet, sServer, ProcessName, % "ahk_class " sClass
StringTrimRight, sServer, sServer, 4
iCodePage := A_IsUnicode ? 0x04B0 : 0x03EC ; 0x04B0 = CP_WINUNICODE, 0x03EC = CP_WINANSI
DllCall("DdeInitialize", "UPtrP", idInst, "Uint", 0, "Uint", 0, "Uint", 0)
hServer := DllCall("DdeCreateStringHandle", "UPtr", idInst, "Str", sServer, "int", iCodePage)
hTopic := DllCall("DdeCreateStringHandle", "UPtr", idInst, "Str", "WWW_GetWindowInfo", "int", iCodePage)
hItem := DllCall("DdeCreateStringHandle", "UPtr", idInst, "Str", "0xFFFFFFFF", "int", iCodePage)
hConv := DllCall("DdeConnect", "UPtr", idInst, "UPtr", hServer, "UPtr", hTopic, "Uint", 0)
hData := DllCall("DdeClientTransaction", "Uint", 0, "Uint", 0, "UPtr", hConv, "UPtr", hItem, "UInt", 1, "Uint", 0x20B0, "Uint", 10000, "UPtrP", nResult) ; 0x20B0 = XTYP_REQUEST, 10000 = 10s timeout
sData := DllCall("DdeAccessData", "Uint", hData, "Uint", 0, "Str")
DllCall("DdeFreeStringHandle", "UPtr", idInst, "UPtr", hServer)
DllCall("DdeFreeStringHandle", "UPtr", idInst, "UPtr", hTopic)
DllCall("DdeFreeStringHandle", "UPtr", idInst, "UPtr", hItem)
DllCall("DdeUnaccessData", "UPtr", hData)
DllCall("DdeFreeDataHandle", "UPtr", hData)
DllCall("DdeDisconnect", "UPtr", hConv)
DllCall("DdeUninitialize", "UPtr", idInst)
csvWindowInfo := StrGet(&sData, "CP0")
StringSplit, sWindowInfo, csvWindowInfo, `" ;"; comment to avoid a syntax highlighting issue in autohotkey.com/boards
Return sWindowInfo2
}
GetBrowserURL_ACC(sClass) {
global nWindow, accAddressBar
If (nWindow != WinExist("ahk_class " sClass)) ; reuses accAddressBar if it's the same window
{
nWindow := WinExist("ahk_class " sClass)
accAddressBar := GetAddressBar(Acc_ObjectFromWindow(nWindow))
}
Try sURL := accAddressBar.accValue(0)
If (sURL == "") {
WinGet, nWindows, List, % "ahk_class " sClass ; In case of a nested browser window as in the old CoolNovo (TO DO: check if still needed)
If (nWindows > 1) {
accAddressBar := GetAddressBar(Acc_ObjectFromWindow(nWindows2))
Try sURL := accAddressBar.accValue(0)
}
}
If ((sURL != "") and (SubStr(sURL, 1, 4) != "http")) ; Modern browsers omit "http://"
sURL := "http://" sURL
If (sURL == "")
nWindow := -1 ; Don't remember the window if there is no URL
Return sURL
}
; "GetAddressBar" based in code by uname
; Found at http://autohotkey.com/board/topic/103178-/?p=637687
GetAddressBar(accObj) {
Try If ((accObj.accRole(0) == 42) and IsURL(accObj.accValue(0)))
Return accObj
Try If ((accObj.accRole(0) == 42) and IsURL("http://" accObj.accValue(0))) ; Modern browsers omit "http://"
Return accObj
For nChild, accChild in Acc_Children(accObj)
If IsObject(accAddressBar := GetAddressBar(accChild))
Return accAddressBar
}
IsURL(sURL) {
Return RegExMatch(sURL, "^(?<Protocol>https?|ftp)://(?<Domain>(?:[\w-]+\.)+\w\w+)(?::(?<Port>\d+))?/?(?<Path>(?:[^:/?# ]*/?)+)(?:\?(?<Query>[^#]+)?)?(?:\#(?<Hash>.+)?)?$")
}
; The code below is part of the Acc.ahk Standard Library by Sean (updated by jethrow)
; Found at http://autohotkey.com/board/topic/77303-/?p=491516
Acc_Init()
{
static h
If Not h
h:=DllCall("LoadLibrary","Str","oleacc","Ptr")
}
Acc_ObjectFromWindow(hWnd, idObject = 0)
{
Acc_Init()
If DllCall("oleacc\AccessibleObjectFromWindow", "Ptr", hWnd, "UInt", idObject&=0xFFFFFFFF, "Ptr", -VarSetCapacity(IID,16)+NumPut(idObject==0xFFFFFFF0?0x46000000000000C0:0x719B3800AA000C81,NumPut(idObject==0xFFFFFFF0?0x0000000000020400:0x11CF3C3D618736E0,IID,"Int64"),"Int64"), "Ptr*", pacc)=0
Return ComObjEnwrap(9,pacc,1)
}
Acc_Query(Acc) {
Try Return ComObj(9, ComObjQuery(Acc,"{618736e0-3c3d-11cf-810c-00aa00389b71}"), 1)
}
Acc_Children(Acc) {
If ComObjType(Acc,"Name") != "IAccessible"
ErrorLevel := "Invalid IAccessible Object"
Else {
Acc_Init(), cChildren:=Acc.accChildCount, Children:=[]
If DllCall("oleacc\AccessibleChildren", "Ptr",ComObjValue(Acc), "Int",0, "Int",cChildren, "Ptr",VarSetCapacity(varChildren,cChildren*(8+2*A_PtrSize),0)*0+&varChildren, "Int*",cChildren)=0 {
Loop %cChildren%
i:=(A_Index-1)*(A_PtrSize*2+8)+8, child:=NumGet(varChildren,i), Children.Insert(NumGet(varChildren,i-8)=9?Acc_Query(child):child), NumGet(varChildren,i-8)=9?ObjRelease(child):
Return Children.MaxIndex()?Children:
} Else
ErrorLevel := "AccessibleChildren DllCall Failed"
}
}

End Comment
This script is still in development and will change based on what happens with the Python Dataframe analysis of the information.
Overall the logging process is simpler, it is just gathering the data, but adapting it to the full requirements of a time sheet process will require the processing of the gathered data. That is on-going at present and the next area of focus for the project.