DadOverflow.com

Musings of a dad with too much time on his hands and not enough to do. Wait. Reverse that.

Month: November 2018

Positioning Windows with PowerShell

When it comes to multiple monitor displays connected to my workstations–either at home or at work–I’m a little like Tim “the Toolman” Taylor: I need more!  And just as I carefully separate my peas and carrots, I must carefully separate the sort of tasks I perform on each monitor.  For example, I like to do my folder and file browsing in my right most monitor and in my left most monitor, I like to do my command shell work.

At work, I interface with a lot of Linux servers, so I tend to have a few command shells open at once connected to these systems.  Since I like to have these shells in my left most monitor, I sought out a solution to automatically launch and proportionally position multiple command shells in my left most monitor.  Initially, I did this with an AutoIt script.  That was an OK solution, but I didn’t find it all that flexible.  I work at home occasionally and my multiple monitor setup is different between home and work.  I found it challenging to make my AutoIt script sufficiently adaptive between each environment.  So, I embarked on a PowerShell solution for my problem.  Here’s how I went about crafting my solution:

Step 1: Establish a windows positioning solution

AutoIt already has a nice window positioning library included with it, but PowerShell does not.  Fortunately, someone wrote a great solution called Set-Window.  I downloaded this script and made two small alterations:

  1. I turned the script into a PowerShell module (PSM1) so that more than one PowerShell script I write can take advantage of this tool and
  2. I changed the script’s ProcessName parameter to ProcessId.  That way, I could pass the script the process id of the window I want to position and there would be no ambiguity as to the exact window I want to change.  You can find my modifications here.

Step 2: Figure out where my left most window is

To be honest, I never explored whether or not AutoIt could give me all the properties of the monitors attached to my workstation, but I knew PowerShell could and that was enough for me.  The .NET namespace System.Windows.Forms.Screen contains an AllScreens function that easily gives me all the details I want.  All I need to do is sort by the X coordinate and grab the first returned record to capture the properties of my left most monitor:


1
2
3
4
5
Add-Type -AssemblyName System.Windows.Forms
$left_most_screen = [System.Windows.Forms.Screen]::AllScreens|sort -Property {$_.WorkingArea.X}|select -First 1
$x = $left_most_screen.WorkingArea.X
$screen_width = $left_most_screen.WorkingArea.Width
$screen_height = $left_most_screen.WorkingArea.Height

Step 3: Launch my command shell instances and position them accordingly

The last step is to run a loop for the number of command shells I want to open.  At present, I’m setting a variable to 2 shells, but I can always change that variable and the script will automatically adjust.  Note that for my shells, I’m using the Ubuntu instance of the Windows Subsystem for Linux (WSL).  In each loop, the script performs three tasks:

  1. It launches a new shell instance
  2. It does some simple math to figure out where to place the shell in the left most monitor and
  3. It calls Set-Window to do the positioning

One note: on my work monitor, when I first call Set-Window on a command shell, it mysteriously sets a slightly smaller height and width for the window.  I’ve found that the easiest fix for this problem is to simply call Set-Window a second time:


1
2
3
4
5
6
7
8
9
1..$nbr_of_windows_to_open|%{
    $app = Start-Process $ubuntu_path -PassThru
    Start-Sleep -Seconds 3
    $y = ( ($_ - 1) * ($screen_height / $nbr_of_windows_to_open) )
    $h = ( $screen_height / $nbr_of_windows_to_open )
    Set-Window -ProcessId $app.Id -X $x -Y $y -Width $screen_width -Height $h -Passthru
    # strangely, on some monitors, the first Set-Window doesn't quite take, but setting it again seems to work
    Set-Window -ProcessId $app.Id -X $x -Y $y -Width $screen_width -Height $h -Passthru
}

You can find my full solution here.  To truly functionalize this solution, I can (and do) take two more steps:

  1. Wrap my PowerShell script in a batch script.  In a BAT file, I can write a command like so: powershell -command “& ‘C:\somePath\launch_ubuntu_windows.ps1’ ” -ExecutionPolicy Bypass
  2. Call that BAT file from Slickrun

But wait, there’s more

I recently replaced my work laptop.  More memory, more power…a true Tim “The Toolman” Taylor moment.  I’m now running the script I described above to launch and neatly place a couple of command shells in my new-and-improved system.  But a frustrating situation occurs to me, not all the time, but enough to be annoying.  I will launch my shells, do a few hours of work, then get up to stretch my legs, get some coffee or otherwise move away from my workstation.  Before moving away, I lock my workstation like any decently security-minded person would.  When I get back, quite often, all my applications, including my command shells, have shifted off my extended monitors and on to my main monitor, negating all the work I did to line them up just right on my extended displays.  I don’t want to re-run my “launch” script again because I don’t need to open more shells–just reposition the ones I already have open.  What I now need is a repositioning script!

My repositioning script works exactly like my “launch” script, only, instead of starting brand new command shell processes, it uses PowerShell’s Get-Process cmdlet to find the already open command shells, get their PIDs, and reposition them the same way as I did before.  Here’s a snippet:


1
2
3
4
5
6
7
8
9
10
$running_ubuntu_pids = Get-Process|?{$_.Name -eq "Ubuntu"}|select Id
$count = 1
foreach($uPid in $running_ubuntu_pids){
    $y = ( ($count - 1) * ($screen_height / $running_ubuntu_pids.Count) )
    $h = ( $screen_height / $running_ubuntu_pids.Count )
    Set-Window -ProcessId $app.Id -X $x -Y $y -Width $screen_width -Height $h -Passthru
    # strangely, on some monitors, the first Set-Window doesn't quite take, but setting it again seems to work
    Set-Window -ProcessId $app.Id -X $x -Y $y -Width $screen_width -Height $h -Passthru
    $count++
}

What next?

So far, I’ve only played around with launching and position 2-3 command shells.  If I ever get the urge to launch four or more, I should probably work out the math to position all those shells in a grid-like pattern.  Also, it’d be nice to have a script that launches, positions, and repositions all types of applications, not just my command shells.  Maybe even a script that will memorize the current positions of my applications and use that to reposition them as needed.

Grouping and counting by date

Subtitle: In one line

The other day at work, I had a file of a bunch of activities and the dates on which they occurred.  Something like this:

What I wanted to do was to get a count of the number of activities occurring on each day, so I fired up a PowerShell command shell and ran the following one-liner:


1
ipcsv "C:\my_path\test_data.csv"|group event_date|select Name, Count

That line produced these results:


1
2
3
4
5
6
7
8
Name       Count
----       -----
8/23/2018     10
9/19/2018      8
7/3/2018      18
10/13/2018    15
11/2/2018      9
10/1/2018     15

Nice.  I have my counts, but it’d be even better if my dates were in order.  Let’s try to sort those suckers:


1
ipcsv "C:\my_path\test_data.csv"|sort event_date|group event_date|select Name, Count

That produced:


1
2
3
4
5
6
7
8
Name       Count
----       -----
10/1/2018     15
10/13/2018    15
11/2/2018      9
7/3/2018      18
8/23/2018     10
9/19/2018      8

Not the kind of sorting I was looking for.  It sorted the dates like they were strings.  Wait a minute…


1
2
3
4
5
PS C:\WINDOWS\system32> (ipcsv "C:\my_path\test_data.csv")[0].event_date.GetType()

IsPublic IsSerial Name                                     BaseType                                                                                                                          
-------- -------- ----                                     --------                                                                                                                          
True     True     String                                   System.Object

Just as I suspected: PowerShell is importing those dates as strings!  If I want to sort my results by date, I’ll have to cast that date to a proper datetime data type first.  Here’s what I came up with:


1
ipcsv "C:\my_path\test_data.csv"|select -Property @{Name="event_dt"; Expression={[datetime]$_.event_date}}|sort event_dt|group event_dt|select Name, Count

And the results:


1
2
3
4
5
6
7
8
Name                   Count
----                   -----
7/3/2018 12:00:00 AM      18
8/23/2018 12:00:00 AM     10
9/19/2018 12:00:00 AM      8
10/1/2018 12:00:00 AM     15
10/13/2018 12:00:00 AM    15
11/2/2018 12:00:00 AM      9

Nice!  I generated my counts and sorted by date all in one line of code.  Sweet!

© 2019 DadOverflow.com

Theme by Anders NorenUp ↑