├── .gitattributes ├── .gitignore ├── Categories.xml ├── License.txt ├── MyMonitor.format.ps1xml ├── MyMonitor.psd1 ├── MyMonitor.psm1 ├── README.md ├── about_mymonitor.help.txt └── changelog.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #my temp work files 2 | scratch* 3 | *.tmp 4 | 5 | # Windows image file caches 6 | Thumbs.db 7 | ehthumbs.db 8 | 9 | # Folder config file 10 | Desktop.ini 11 | 12 | # Recycle Bin used on file shares 13 | $RECYCLE.BIN/ 14 | 15 | # Windows Installer files 16 | *.cab 17 | *.msi 18 | *.msm 19 | *.msp 20 | 21 | # Windows shortcuts 22 | *.lnk 23 | 24 | # ========================= 25 | # Operating System Files 26 | # ========================= 27 | 28 | # OSX 29 | # ========================= 30 | 31 | .DS_Store 32 | .AppleDouble 33 | .LSOverride 34 | 35 | # Thumbnails 36 | ._* 37 | 38 | # Files that might appear on external disk 39 | .Spotlight-V100 40 | .Trashes 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /Categories.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdhitsolutions/MyMonitor/77684b894f15f084ffaf277a48bc857ee49fefcf/Categories.xml -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdhitsolutions/MyMonitor/77684b894f15f084ffaf277a48bc857ee49fefcf/License.txt -------------------------------------------------------------------------------- /MyMonitor.format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | default 14 | 15 | My.Monitored.Window 16 | 17 | 18 | 19 | 20 | 21 | 22 | Time 23 | 24 | 25 | Application 26 | 27 | 28 | WindowTitle 29 | 30 | 31 | Product 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | default 40 | 41 | My.Monitored.Window 42 | 43 | 44 | 45 | 46 | 47 | 17 48 | Left 49 | 50 | 51 | 52 | 45 53 | Left 54 | 55 | 56 | 57 | 30 58 | Left 59 | 60 | 61 | 62 | 63 | 64 | 65 | Time 66 | 67 | 68 | WindowTitle 69 | 70 | 71 | Application 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Application 80 | 81 | My.Monitored.Window 82 | 83 | 84 | Application 85 | 86 | 87 | 88 | 89 | 90 | 17 91 | Left 92 | 93 | 94 | 95 | 45 96 | Left 97 | 98 | 99 | 100 | 37 101 | Left 102 | 103 | 104 | 105 | 106 | 107 | 108 | Time 109 | 110 | 111 | WindowTitle 112 | 113 | 114 | Product 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | Product 123 | 124 | My.Monitored.Window 125 | 126 | 127 | Product 128 | 129 | 130 | 131 | 132 | 133 | 134 | 17 135 | Left 136 | 137 | 138 | 139 | 45 140 | Left 141 | 142 | 143 | 144 | 30 145 | Left 146 | 147 | 148 | 149 | 150 | 151 | 152 | Time 153 | 154 | 155 | WindowTitle 156 | 157 | 158 | Application 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /MyMonitor.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdhitsolutions/MyMonitor/77684b894f15f084ffaf277a48bc857ee49fefcf/MyMonitor.psd1 -------------------------------------------------------------------------------- /MyMonitor.psm1: -------------------------------------------------------------------------------- 1 | #requires -version 4.0 2 | 3 | #region Commands 4 | 5 | Function Get-ForegroundWindowProcess { 6 | 7 | <# 8 | .Synopsis 9 | Get process for foreground window process 10 | .Description 11 | This command will retrieve the process for the active foreground window, ignoring any process with a main window handle of 0. 12 | 13 | It will also ignore Task Switching done with Explorer. 14 | .Example 15 | PS C:\> get-foregroundwindowprocess 16 | 17 | Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName 18 | ------- ------ ----- ----- ----- ------ -- ----------- 19 | 538 57 124392 151484 885 34.22 4160 powershell_ise 20 | 21 | .Notes 22 | 23 | Learn more about PowerShell: 24 | http://jdhitsolutions.com/blog/essential-powershell-resources/ 25 | 26 | 27 | **************************************************************** 28 | * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * 29 | * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * 30 | * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * 31 | * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * 32 | **************************************************************** 33 | .Link 34 | Get-Process 35 | #> 36 | 37 | [cmdletbinding()] 38 | Param() 39 | 40 | Try { 41 | #test if the custom type has already been added 42 | [user32] -is [Type] | Out-Null 43 | } 44 | catch { 45 | #type not found so add it 46 | 47 | Add-Type -typeDefinition @" 48 | using System; 49 | using System.Runtime.InteropServices; 50 | 51 | public class User32 52 | { 53 | [DllImport("user32.dll")] 54 | public static extern IntPtr GetForegroundWindow(); 55 | } 56 | "@ 57 | #must left justify here-string closing @ 58 | } #catch 59 | 60 | <# 61 | get the process for the currently active foreground window as long as it has a value 62 | greater than 0. A value of 0 typically means a non-interactive window. Also ignore 63 | any Task Switch windows 64 | #> 65 | 66 | (Get-Process).where({$_.MainWindowHandle -eq ([user32]::GetForegroundWindow()) -AND $_.MainWindowHandle -ne 0 -AND $_.Name -ne 'Explorer' -AND $_.Title -notmatch "Task Switching"}) 67 | 68 | 69 | } #end Get-ForegroundWindowProcess 70 | 71 | Function Get-WindowTime { 72 | 73 | <# 74 | .Synopsis 75 | Monitor time by active window 76 | .Description 77 | This script will monitor how much time you spend based on how long a given window is active. Monitoring will continue until one of the specified triggers is detected. 78 | 79 | By default monitoring will continue for 1 minute. Use -Minutes to specify a different value. You can also specify a trigger by a specific date and time or by detection of a specific process. 80 | .Parameter Time 81 | Monitoring will continue until this datetime value is met or exceeded. 82 | .Parameter Minutes 83 | The numer of minutes to monitor. This is the default behavior. 84 | .Parameter ProcessName 85 | The name of a process that you would see with Get-Process, e.g. Notepad or Calc. Monitoring will stop when this process is detected. 86 | Parameter AsJob 87 | Run the monitoring in a background job. Note that if you stop the job you will NOT have any results. 88 | .Example 89 | PS C:\> $data = Get-WindowTime -minutes 60 90 | 91 | Monitor window activity for the next 60 minutes. Be aware that you won't get your prompt back until this command completes. 92 | .Example 93 | PS C:\> Get-WindowTime -processname calc -asjob 94 | 95 | Start monitoring windows in the background until the Calculator process is detected. 96 | .Notes 97 | Last Updated: October 7, 2015 98 | Version : 2.0 99 | 100 | Learn more about PowerShell: 101 | http://jdhitsolutions.com/blog/essential-powershell-resources/ 102 | 103 | **************************************************************** 104 | * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * 105 | * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * 106 | * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * 107 | * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * 108 | **************************************************************** 109 | 110 | .Link 111 | Get-Process 112 | 113 | #> 114 | 115 | [cmdletbinding(DefaultParameterSetName= "Minutes")] 116 | Param( 117 | [Parameter(ParameterSetName="Time")] 118 | [ValidateNotNullorEmpty()] 119 | [DateTime]$Time, 120 | 121 | [Parameter(ParameterSetName="Minutes")] 122 | [ValidateScript({ $_ -ge 1})] 123 | [Int]$Minutes = 1, 124 | 125 | [Parameter(ParameterSetName="Process")] 126 | [ValidateNotNullorEmpty()] 127 | [string]$ProcessName, 128 | 129 | [switch]$AsJob 130 | 131 | ) 132 | 133 | Write-Verbose "[$(Get-Date)] Starting $($MyInvocation.Mycommand)" 134 | 135 | #define a scriptblock to use in the While loop 136 | Switch ($PSCmdlet.ParameterSetName) { 137 | 138 | "Time" { 139 | Write-Verbose "[$(Get-Date)] Stop monitoring at $Time" 140 | [scriptblock]$Trigger = [scriptblock]::Create("(get-date) -ge ""$time""") 141 | Break 142 | } 143 | "Minutes" { 144 | $Quit = (Get-Date).AddMinutes($Minutes) 145 | Write-Verbose "[$(Get-Date)] Stop monitoring in $minutes minute(s) at $Quit" 146 | [scriptblock]$Trigger = [scriptblock]::Create("(get-date) -ge ""$Quit""") 147 | Break 148 | } 149 | "Process" { 150 | If (Get-Process -name $processname -ErrorAction SilentlyContinue) { 151 | Write-Warning "The $ProcessName process is already running. Close it first then try again." 152 | #bail out 153 | Return 154 | } 155 | Write-Verbose "[$(Get-Date)] Stop monitoring after trigger $Processname" 156 | [scriptblock]$Trigger = [scriptblock]::Create("Get-Process -Name $ProcessName -ErrorAction SilentlyContinue") 157 | Break 158 | } 159 | 160 | } #switch 161 | 162 | #define the entire command as a scriptblock so it can be run as a job if necessary 163 | $main = { 164 | Param($sb) 165 | 166 | if (-Not ($sb -is [scriptblock])) { 167 | #convert $sb to a scriptblock 168 | Write-Verbose "Creating sb from $sb" 169 | $sb = [scriptblock]::Create("$sb") 170 | } 171 | 172 | #create a hashtable 173 | $hash=@{} 174 | 175 | #create a collection of objects 176 | $objs = @() 177 | 178 | New-Variable -Name LastApp -Value $Null 179 | 180 | while( -Not (&$sb) ) { 181 | 182 | $Process = Get-ForegroundWindowProcess 183 | 184 | [string]$app = $process.MainWindowTitle 185 | 186 | if ( (-Not $app) -AND $process.MainModule.Description ) { 187 | #if no title but there is a description, use that 188 | $app = $process.MainModule.Description 189 | } 190 | elseif (-Not $app) { 191 | #otherwise use the module name 192 | $app = $process.mainmodule.modulename 193 | } 194 | 195 | if ($process -AND (($Process.MainWindowHandle -ne $LastProcess.MainWindowHandle) -OR ($app -ne $lastApp )) ) { 196 | Write-Verbose "[$(Get-Date)] NEW App changed to $app" 197 | 198 | #record $last 199 | if ($LastApp) { 200 | if ($objs.WindowTitle -contains $LastApp) { 201 | #update same app that was previously found 202 | Write-Verbose "[$(Get-Date)] updating existing object $LastApp" 203 | 204 | $existing = $objs | where { $_.WindowTitle -eq $LastApp } 205 | 206 | Write-Verbose "[$(Get-Date)] SW = $($sw.elapsed)" 207 | 208 | $existing.Time+= $sw.Elapsed 209 | 210 | #include a detail property object 211 | 212 | $existing.Detail += [pscustomObject]@{ 213 | StartTime = $start 214 | EndTime = Get-Date 215 | ProcessID = $lastProcess.ID 216 | Process = if ($LastProcess) {$LastProcess} else {$process} 217 | } 218 | Write-Verbose "[$(Get-Date)] Total time = $($existing.time)" 219 | } 220 | else { 221 | #create new object 222 | 223 | #include a detail property object 224 | [pscustomObject]$detail=@{ 225 | StartTime = $start 226 | EndTime = Get-Date 227 | ProcessID = $lastProcess.ID 228 | Process = if ($LastProcess) {$LastProcess} else {$process} 229 | } 230 | Write-Verbose "[$(Get-Date)] Creating new object for $LastApp" 231 | Write-Verbose "[$(Get-Date)] Time = $($sw.elapsed)" 232 | 233 | #get categories 234 | $appCategory = (Select-XML -xml $MonitorCategories -xpath "//app[@name='$($LastMainModule.Description.Trim())']").node.parentnode.name 235 | 236 | if (!$appcategory) { 237 | $appCategory = "None" 238 | } 239 | 240 | #if there is no process description then use the product name 241 | #for the application 242 | if ($LastMainModule.Description -match "\w+") { 243 | $theApp = $LastMainModule.Description 244 | } 245 | else { 246 | $theApp = $LastMainModule.Product 247 | } 248 | 249 | $obj = New-Object -TypeName PSobject -Property @{ 250 | WindowTitle = $LastApp 251 | Application = $theApp #$LastProcess.MainModule.Description 252 | Product = $LastMainModule.Product #$LastProcess.MainModule.Product 253 | Time = $sw.Elapsed 254 | Detail = ,([pscustomObject]@{ 255 | StartTime = $start 256 | EndTime = Get-Date 257 | ProcessID = $lastProcess.ID 258 | Process = if ($LastProcess) {$LastProcess} else {$process} 259 | } ) 260 | Category = $appCategory 261 | Computername = $env:COMPUTERNAME 262 | 263 | } 264 | 265 | $obj.psobject.TypeNames.Insert(0,"My.Monitored.Window") 266 | #add a custom type name 267 | 268 | #add the object to the collection 269 | $objs += $obj 270 | } #else create new object 271 | } #if $lastApp was defined 272 | else { 273 | Write-Verbose "You should only see this once" 274 | } 275 | 276 | #new Process with a window 277 | Write-Verbose "[$(Get-Date)] Start a timer" 278 | $SW = [System.Diagnostics.Stopwatch]::StartNew() 279 | $start = Get-Date 280 | 281 | #set the last app 282 | $LastApp = $app 283 | #preserve process information 284 | $LastProcess = $Process 285 | $LastMainModule = $process.mainmodule 286 | 287 | #clear app just in case 288 | Remove-Variable app 289 | } 290 | Start-Sleep -Milliseconds 100 291 | 292 | } #while 293 | 294 | #update last app 295 | if ($objs.WindowTitle -contains $LastApp) { 296 | #update same app that was previously found 297 | Write-Verbose "[$(Get-Date)] processing last object" 298 | Write-Verbose "[$(Get-Date)] updating existing object for $LastApp" 299 | Write-Verbose "[$(Get-Date)] SW = $($sw.elapsed)" 300 | $existing = $objs | where { $_.WindowTitle -eq $LastApp } 301 | 302 | $existing.Time+= $sw.Elapsed 303 | 304 | Write-Verbose "[$(Get-Date)] Total time = $($existing.time)" 305 | 306 | #include a detail property object 307 | 308 | $existing.Detail += [pscustomObject]@{ 309 | StartTime = $start 310 | EndTime = Get-Date 311 | ProcessID = $lastProcess.ID 312 | Process = if ($LastProcess) {$LastProcess} else {$process} 313 | } 314 | } 315 | else { 316 | #create new object 317 | 318 | Write-Verbose "[$(Get-Date)] Creating new object" 319 | Write-Verbose "[$(Get-Date)] Time = $($sw.elapsed)" 320 | 321 | #get categories 322 | $appCategory = (Select-XML -xml $MonitorCategories -xpath "//app[@name='$($LastMainModule.Description.Trim())']").node.parentnode.name 323 | 324 | if (!$appcategory) { 325 | $appCategory = "None" 326 | } 327 | 328 | if ($LastMainModule.Description -match "\w+") { 329 | $theApp = $LastMainModule.Description 330 | } 331 | else { 332 | $theApp = $LastMainModule.Product 333 | } 334 | $obj = New-Object -TypeName PSobject -Property @{ 335 | WindowTitle = $LastApp 336 | Application = $theApp #$LastProcess.MainModule.Description 337 | Product = $LastMainModule.Product #$LastProcess.MainModule.Product 338 | Time = $sw.Elapsed 339 | Detail = ,([pscustomObject]@{ 340 | StartTime = $start 341 | EndTime = Get-Date 342 | ProcessID = $lastProcess.ID 343 | Process = if ($LastProcess) {$LastProcess} else {$process} 344 | }) 345 | Category = $appCategory 346 | Computername = $env:COMPUTERNAME 347 | } 348 | 349 | $obj.psobject.TypeNames.Insert(0,"My.Monitored.Window") 350 | #add a custom type name 351 | 352 | #add the object to the collection 353 | $objs += $obj 354 | } #else create new object 355 | 356 | $objs 357 | 358 | Write-Verbose "[$(Get-Date)] Ending $($MyInvocation.Mycommand)" 359 | } #main 360 | 361 | 362 | if ($asJob) { 363 | Write-Verbose "Running as background job" 364 | Start-Job -ScriptBlock $main -ArgumentList @($Trigger) -InitializationScript {Import-Module MyMonitor} 365 | 366 | } 367 | else { 368 | #run it 369 | Invoke-Command -ScriptBlock $main -ArgumentList @($Trigger) 370 | } 371 | 372 | } #end Get-WindowTime 373 | 374 | Function Measure-WindowTotal { 375 | 376 | <# 377 | .Synopsis 378 | Measure Window usage results. 379 | .Description 380 | This command is designed to take output from Get-WindowTime and measure total time either by Application, the default, by Product or Window title. Or you can elect to get the total time for all piped in measured window objects. 381 | 382 | You can also filter based on keywords found in the window title. See examples. 383 | .Parameter Filter 384 | A string to be used for filtering based on the window title. The string can be a regular expression pattern. 385 | .Example 386 | PS C:\> $data = Get-WindowTime -ProcessName calc 387 | 388 | Start monitoring windows until calc is detected as a running process. 389 | 390 | PS C:\> $data | Measure-WindowTotal 391 | 392 | Application TotalTime 393 | ----------- --------- 394 | Desktop Window Manager 00:00:05.5737147 395 | Dropbox 00:00:05.9456759 396 | Skype 00:00:07.3145639 397 | Microsoft Management Console 00:00:08.4824010 398 | COM Surrogate 00:00:08.6499505 399 | SugarSync 00:00:11.3705429 400 | Wireshark 00:00:21.0674948 401 | Windows PowerShell 00:00:33.6266261 402 | Virtual Machine Connection 00:02:07.3252447 403 | Spotify 00:04:25.0623684 404 | Thunderbird 00:17:38.3666410 405 | Windows PowerShell ISE 00:20:28.3669673 406 | Microsoft Word 00:22:18.5841682 407 | Waterfox 01:20:46.0559866 408 | 409 | 410 | PS C:\> $data | Measure-WindowTotal -Product 411 | 412 | Product TotalTime 413 | ------- --------- 414 | Dropbox 00:00:05.9456759 415 | Skype 00:00:07.3145639 416 | SugarSync 00:00:11.3705429 417 | Wireshark 00:00:21.0674948 418 | Spotify 00:04:25.0623684 419 | Thunderbird 00:17:38.3666410 420 | Microsoft Office 2013 00:22:18.5841682 421 | Microsoft® Windows® Operating System 00:23:32.0249043 422 | Waterfox 01:20:46.0559866 423 | 424 | The first command gets data from active window usage. The second command measures the results by Application. The last command measures the same data but by the product property. 425 | .Example 426 | PS C:\> $data | Measure-WindowTotal -filter "facebook" -TimeOnly 427 | 428 | Days : 0 429 | Hours : 0 430 | Minutes : 11 431 | Seconds : 2 432 | Milliseconds : 781 433 | Ticks : 6627813559 434 | TotalDays : 0.00767108050810185 435 | TotalHours : 0.184105932194444 436 | TotalMinutes : 11.0463559316667 437 | TotalSeconds : 662.7813559 438 | TotalMilliseconds : 662781.3559 439 | 440 | Get just the time that was spent on any window that had Facebook in the title. 441 | .Example 442 | PS C:\> $data | Measure-WindowTotal -filter "facebook|twitter" 443 | 444 | Application TotalTime 445 | ----------- --------- 446 | Desktop Window Manager 00:00:05.5737147 447 | Waterfox 00:10:57.2076412 448 | 449 | Display how much time was spent on Facebook or Twitter. 450 | 451 | .Example 452 | PS C:\> Measure-WindowTotal $data -Category | Sort Time -descending | format-table -AutoSize 453 | 454 | Category Count Time 455 | -------- ----- ---- 456 | Internet 68 01:25:18.4329189 457 | Office 5 00:22:18.5841682 458 | PowerShell 4 00:21:01.9935934 459 | Test 1 00:20:28.3669673 460 | Development 1 00:20:28.3669673 461 | Mail 29 00:17:38.3666410 462 | None 5 00:02:27.4945858 463 | Utilities 2 00:00:29.5498958 464 | Cloud 1 00:00:11.3705429 465 | Communication 1 00:00:07.3145639 466 | 467 | Measure window totals by category then sort the results by time in descending order. The result is formatted as a table. 468 | 469 | .Notes 470 | Last Updated: October 7, 2015 471 | Version : 2.0 472 | 473 | Learn more about PowerShell: 474 | http://jdhitsolutions.com/blog/essential-powershell-resources/ 475 | 476 | **************************************************************** 477 | * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * 478 | * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * 479 | * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * 480 | * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * 481 | **************************************************************** 482 | 483 | .Link 484 | Get-WindowTime 485 | #> 486 | 487 | [cmdletbinding(DefaultParameterSetName="Product")] 488 | Param( 489 | [Parameter(Position=0,ValueFromPipeline)] 490 | [Parameter(ParameterSetName="Product")] 491 | [Parameter(ParameterSetName="Title")] 492 | [Parameter(ParameterSetName="Category")] 493 | [ValidateNotNullorEmpty()] 494 | $InputObject, 495 | [Parameter(ParameterSetName="Product")] 496 | [Switch]$Product, 497 | [Parameter(ParameterSetName="Title")] 498 | [Switch]$Title, 499 | [Parameter(ParameterSetName="Category")] 500 | [Switch]$Category, 501 | [Parameter(ParameterSetName="Product")] 502 | [Parameter(ParameterSetName="Title")] 503 | [ValidateNotNullorEmpty()] 504 | [String]$Filter=".*", 505 | [Parameter(ParameterSetName="Product")] 506 | [Parameter(ParameterSetName="Title")] 507 | [Switch]$TimeOnly 508 | ) 509 | 510 | Begin { 511 | Write-Verbose "Starting $($MyInvocation.Mycommand)" 512 | 513 | #initialize 514 | $hash=@{} 515 | 516 | if ($Product) { 517 | $objFilter = "Product" 518 | } 519 | elseif($Title) { 520 | $objFilter = "WindowTitle" 521 | } 522 | elseif($Category) { 523 | $objFilter = "Category" 524 | #initialize an array to hold incoming items 525 | $data=@() 526 | } 527 | else { 528 | $objFilter = "Application" 529 | } 530 | 531 | Write-Verbose "Calculating totals by $objFilter" 532 | } #begin 533 | 534 | Process { 535 | 536 | foreach ($item in $InputObject) { 537 | #only process objects where the window title matches the filter which 538 | #by default is everything and there is only one object in the product 539 | #which should eliminate task switching data 540 | if ($item.WindowTitle -match $filter -AND $item.Product.count -eq 1) { 541 | 542 | If ($Category) { 543 | $data+=$item 544 | } 545 | else { 546 | if ($hash.ContainsKey($item.$objFilter)) { 547 | #update an existing entry in the hash table 548 | $hash.Item($item.$objFilter) += $item.Time 549 | } 550 | else { 551 | #Add an entry to the hashtable 552 | Write-Verbose "Adding $($item.$objFilter)" 553 | $hash.Add($item.$objFilter,$item.time) 554 | } 555 | } #else not -Category 556 | } 557 | } #foreach 558 | } #process 559 | 560 | End { 561 | Write-Verbose "Processing data" 562 | 563 | if ($Category) { 564 | Write-Verbose "Getting category breakdown" 565 | $output = $data.category | select -Unique | foreach { 566 | $thecategory = $_ 567 | $hash = [ordered]@{Category=$theCategory} 568 | $items = $($data).Where({$_.category -contains $thecategory}) 569 | $totaltime = $items | foreach -begin {$time = new-timespan} -process {$time+=$_.time} -end {$time} 570 | $hash.Add("Count",$items.count) 571 | $hash.Add("Time",$TotalTime) 572 | [pscustomobject]$hash 573 | } 574 | } 575 | else { 576 | #turn hash table into a custom object and sort on time by default 577 | $output = ($hash.GetEnumerator()).foreach({ 578 | [pscustomobject]@{$objfilter=$_.Name;"TotalTime"=$_.Value} 579 | 580 | }) | Sort TotalTime 581 | } 582 | 583 | if ($TimeOnly) { 584 | $output | foreach -begin {$total = New-TimeSpan} -process {$total+=$_.Totaltime} -end {$total} 585 | } 586 | else { 587 | $output # | Select $objFilter,TotalTime 588 | } 589 | 590 | Write-Verbose "Ending $($MyInvocation.Mycommand)" 591 | } #end 592 | 593 | } #end Measure-WindowTotal 594 | 595 | Function Get-WindowTimeSummary { 596 | <# 597 | .Synopsis 598 | Get a summary of window usage time 599 | .Description 600 | This command will take an array of window usage data and present a summary based on application. The output will include the total time as well as the first and last times for that particular application. 601 | 602 | As an alternative you can get a summary by Product or you can filter using a regular expression pattern on the window title. 603 | .Example 604 | PS C:> 605 | 606 | PS C:\> Get-WindowTimeSummary $data 607 | 608 | Name Total Start End 609 | ---- ----- ----- --- 610 | Windows PowerShell ISE 00:20:28.3669673 10/7/2015 8:07:09 AM 10/7/2015 10:36:36 AM 611 | SugarSync 00:00:11.3705429 10/7/2015 8:07:20 AM 10/7/2015 9:01:31 AM 612 | Spotify 00:04:25.0623684 10/7/2015 8:07:34 AM 10/7/2015 8:39:27 AM 613 | Thunderbird 00:17:38.3666410 10/7/2015 10:24:47 AM 10/7/2015 10:30:52 AM 614 | COM Surrogate 00:00:08.6499505 10/7/2015 8:09:51 AM 10/7/2015 8:10:00 AM 615 | Waterfox 01:20:46.0559866 10/7/2015 9:19:33 AM 10/7/2015 10:30:54 AM 616 | Wireshark 00:00:21.0674948 10/7/2015 8:13:23 AM 10/7/2015 8:29:48 AM 617 | Virtual Machine Connection 00:02:07.3252447 10/7/2015 10:12:46 AM 10/7/2015 10:12:43 AM 618 | Skype 00:00:07.3145639 10/7/2015 8:29:27 AM 10/7/2015 8:39:12 AM 619 | Windows PowerShell 00:00:33.6266261 10/7/2015 8:30:59 AM 10/7/2015 8:40:09 AM 620 | Dropbox 00:00:05.9456759 10/7/2015 8:32:26 AM 10/7/2015 8:32:32 AM 621 | Microsoft Management Console 00:00:08.4824010 10/7/2015 8:39:45 AM 10/7/2015 10:12:46 AM 622 | Microsoft Word 00:22:18.5841682 10/7/2015 9:04:00 AM 10/7/2015 10:04:05 AM 623 | Desktop Window Manager 00:00:05.5737147 10/7/2015 9:19:57 AM 10/7/2015 9:20:02 AM 624 | 625 | Get time summary using the default application type. 626 | 627 | .Example 628 | PS C:\> Get-WindowTimeSummary $data -Type Product 629 | 630 | Name Total Start End 631 | ---- ----- ----- --- 632 | Microsoft® Windows® Operating ... 00:23:32.0249043 10/7/2015 8:09:51 AM 10/7/2015 10:12:43 AM 633 | SugarSync 00:00:11.3705429 10/7/2015 8:07:20 AM 10/7/2015 9:01:31 AM 634 | Spotify 00:04:25.0623684 10/7/2015 8:07:34 AM 10/7/2015 8:39:27 AM 635 | Thunderbird 00:17:38.3666410 10/7/2015 10:24:47 AM 10/7/2015 10:30:52 AM 636 | Waterfox 01:20:46.0559866 10/7/2015 9:19:33 AM 10/7/2015 10:30:54 AM 637 | Wireshark 00:00:21.0674948 10/7/2015 8:13:23 AM 10/7/2015 8:29:48 AM 638 | Skype 00:00:07.3145639 10/7/2015 8:29:27 AM 10/7/2015 8:39:12 AM 639 | Dropbox 00:00:05.9456759 10/7/2015 8:32:26 AM 10/7/2015 8:32:32 AM 640 | Microsoft Office 2013 00:22:18.5841682 10/7/2015 9:04:00 AM 10/7/2015 10:04:05 AM 641 | 642 | Get time summary by product. 643 | .Example 644 | PS C:\> Get-WindowTimeSummary $data -filter "facebook|hootsuite" 645 | 646 | Name Total Start End 647 | ---- ----- ----- --- 648 | facebook|hootsuite 00:28:22.3692839 10/7/2015 8:33:47 AM 10/7/2015 10:30:54 AM 649 | 650 | Filter window titles with a regular expression. 651 | .Notes 652 | Last Updated: October 7, 2015 653 | Version : 2.0 654 | 655 | Learn more about PowerShell: 656 | http://jdhitsolutions.com/blog/essential-powershell-resources/ 657 | 658 | **************************************************************** 659 | * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * 660 | * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * 661 | * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * 662 | * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * 663 | **************************************************************** 664 | 665 | .Link 666 | Get-WindowTime 667 | Measure-WindowTotal 668 | #> 669 | 670 | [cmdletbinding(DefaultParameterSetName="Type")] 671 | Param( 672 | [Parameter( 673 | Position=0,Mandatory, 674 | HelpMessage="Enter a variable with your Window usage data")] 675 | [ValidateNotNullorEmpty()] 676 | $Data, 677 | [Parameter(ParameterSetName="Type")] 678 | [ValidateSet("Product","Application")] 679 | [string]$Type="Application", 680 | 681 | [Parameter(ParameterSetName="Filter")] 682 | [ValidateNotNullorEmpty()] 683 | [string]$Filter 684 | 685 | ) 686 | 687 | Write-Verbose -Message "Starting $($MyInvocation.Mycommand)" 688 | 689 | 690 | if ($PSCmdlet.ParameterSetName -eq 'Type') { 691 | Write-Verbose "Processing on $Type" 692 | #filter out blanks and objects with multiple products from ALT-Tabbing 693 | $grouped = ($data).Where({$_.$Type -AND $_.$Type.Count -eq 1}) | Group-Object -Property $Type 694 | } 695 | else { 696 | #use filter 697 | Write-Verbose "Processing on filter: $Filter" 698 | $grouped = ($data).where({$_.WindowTitle -match $Filter -AND $_.Product.Count -eq 1}) | 699 | Group-Object -Property {$Filter} 700 | } 701 | 702 | if ($Grouped) { 703 | $grouped| Select Name, 704 | @{Name="Total";Expression={ 705 | $_.Group | foreach -begin {$total = New-TimeSpan} -process {$total+=$_.time} -end {$total} 706 | }}, 707 | @{Name="Start";Expression={ 708 | ($_.group | sort Detail).Detail[0].StartTime 709 | }}, 710 | @{Name="End";Expression={ 711 | ($_.group | sort Detail).Detail[-1].EndTime 712 | }} 713 | } 714 | else { 715 | Write-Warning "No items found" 716 | } 717 | 718 | Write-Verbose -Message "Ending $($MyInvocation.Mycommand)" 719 | 720 | } #end Get-WindowsTimeSummary 721 | 722 | Function Add-TimeSpan { 723 | <# 724 | .Synopsis 725 | Add timespan values 726 | 727 | .Description 728 | This command can be used to add timespan values. Measure-Object doesn't appear to be able to calculate a sum of timespans. The default output is a timespan object but you can also specify it as a string. 729 | 730 | .Notes 731 | Last Updated: May 31, 2016 732 | Version : 1.0 733 | 734 | Learn more about PowerShell: 735 | http://jdhitsolutions.com/blog/essential-powershell-resources/ 736 | 737 | **************************************************************** 738 | * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * 739 | * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * 740 | * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * 741 | * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * 742 | **************************************************************** 743 | 744 | .Link 745 | Measure-WindowTotal 746 | 747 | .Example 748 | PS C:\> $d | measure-windowtotal -title -Filter facebook | Add-Timespan -verbose 749 | VERBOSE: [BEGIN ] Starting: Add-Timespan 750 | VERBOSE: [PROCESS] Adding 00:00:07.0100396 751 | VERBOSE: [PROCESS] Adding 00:00:22.4516731 752 | VERBOSE: [PROCESS] Adding 00:03:02.5448095 753 | 754 | 755 | Days : 0 756 | Hours : 0 757 | Minutes : 3 758 | Seconds : 32 759 | Milliseconds : 6 760 | Ticks : 2120065222 761 | TotalDays : 0.00245377919212963 762 | TotalHours : 0.0588907006111111 763 | TotalMinutes : 3.53344203666667 764 | TotalSeconds : 212.0065222 765 | TotalMilliseconds : 212006.5222 766 | 767 | VERBOSE: [END ] Ending: Add-Timespan 768 | 769 | PS C:\> $d | measure-windowtotal -title -Filter facebook | Add-Timespan -AsString 770 | 00:03:32.0065222 771 | 772 | #> 773 | 774 | [cmdletbinding()] 775 | Param( 776 | [Parameter( 777 | Position=0, 778 | Mandatory, 779 | ValueFromPipeline, 780 | ValueFromPipelineByPropertyName 781 | )] 782 | [ValidateNotNullorEmpty()] 783 | [Alias('totaltime','time')] 784 | [timespan]$Timespan, 785 | [switch]$AsString 786 | ) 787 | 788 | Begin { 789 | Write-Verbose "[BEGIN ] Starting: $($MyInvocation.Mycommand)" 790 | 791 | $Total=0 792 | } #begin 793 | 794 | Process { 795 | Write-Verbose "[PROCESS] Adding $Timespan" 796 | $total+=$Timespan 797 | } #process 798 | 799 | 800 | End { 801 | if ($AsString) { 802 | Write-Verbose "[END ] Converting result to a string" 803 | $Total.ToString() 804 | } 805 | else { 806 | #write the full result 807 | $Total 808 | } 809 | Write-Verbose "[END ] Ending: $($MyInvocation.Mycommand)" 810 | } #end 811 | 812 | } 813 | 814 | 815 | 816 | #endregion 817 | 818 | #region TypeData 819 | 820 | #set default display property set 821 | Update-TypeData -TypeName "my.monitored.window" -DefaultDisplayPropertySet "Time","Application","WindowTitle","Product" -DefaultDisplayProperty WindowTitle -Force 822 | Update-TypeData -TypeName "deserialized.my.monitored.window" -DefaultDisplayPropertySet "Time","Application","WindowTitle","Product" -DefaultDisplayProperty WindowTitle -Force 823 | 824 | #add an alias for the WindowTitle property 825 | Update-TypeData -TypeName "My.Monitored.Window" -MemberType AliasProperty -MemberName Title -Value WindowTitle -force 826 | Update-TypeData -TypeName "deserialized.My.Monitored.Window" -MemberType AliasProperty -MemberName Title -Value WindowTitle -force 827 | 828 | #endregion 829 | 830 | #region Aliases 831 | 832 | Set-Alias -name mwt -Value Measure-WindowTotal 833 | Set-Alias -name gfwp -Value Get-ForegroundWindowProcess 834 | Set-Alias -Name gwt -Value Get-WindowTime 835 | Set-Alias -name gwts -Value Get-WindowTimeSummary 836 | 837 | #endregion 838 | 839 | #region Variables 840 | 841 | <# 842 | Look for a copy of Categories.xml in the user's PowerShell directory and use that if found. 843 | Otherwise use the one included with the module. This is to prevent overwriting the xml 844 | file in future module updates. 845 | #> 846 | 847 | $localXML = Join-Path -Path $env:USERPROFILE -ChildPath "Documents\WindowsPowerShell\Categories.xml" 848 | $modXML = Join-Path -path $PSScriptroot -ChildPath categories.xml 849 | 850 | if (Test-Path -path $localXML) { 851 | [xml]$MonitorCategories = Get-Content -Path $localXML 852 | } 853 | else { 854 | [xml]$MonitorCategories = Get-Content -Path $modXML 855 | } 856 | 857 | 858 | #endregion 859 | 860 | Export-ModuleMember -Function * -Alias * -Variable MonitorCategories 861 | 862 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdhitsolutions/MyMonitor/77684b894f15f084ffaf277a48bc857ee49fefcf/README.md -------------------------------------------------------------------------------- /about_mymonitor.help.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdhitsolutions/MyMonitor/77684b894f15f084ffaf277a48bc857ee49fefcf/about_mymonitor.help.txt -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdhitsolutions/MyMonitor/77684b894f15f084ffaf277a48bc857ee49fefcf/changelog.txt --------------------------------------------------------------------------------