launchan
readme
md
# launchan - handy launcher
## このツールは何か
- Windows で使用可能な軽量ランチャです
- ランチャの登録内容は所定ディレクトリの CSV に登録します
- ユーザは画面の入力欄で登録内容を絞り込み、開きたいコマンド/ファイル/フォルダを選択して起動します
- スクリプトファイルと csv で構成されるので、バイナリのダウンロードやインストールが不要です
## 誰のためのツールか
以下のような悩み/制約のある環境での作業者のためのツールです。
- 共有ファイルサーバのいろんなところに情報が散在している
- バイナリのダウンロードやインストールが出来ない環境である
- 権限がなくて拡張子ごとのアプリ紐づけが変更できない
- テスト環境がいくつもあって、URL、DB接続、ターミナル接続などを呼び分けたい
- 指定のブラウザが決まっているページがある
- 作業端末が1か所とは限らないのでブラウザのブックマークに頼りたくない (共有フォルダにファイルとして置きたい)
- ショートカットファイルはリンク切れの管理が面倒なので、テキストベース(csv)で管理したい
## ファイル/フォルダ構成
- launchan.ps1 : スクリプト本体
- launchan.vbs : スクリプトの wrapper (ダブルクリックで呼び出したいためだけに用意したものです)
- launchan-data\
- Sample.csv : ランチャの登録内容サンプルです
- Aaaa.csv : 使用者が自由に作成します
- Bbbb.csv
- ...
- _Xxx.csv : 非表示にしたい場合はファイル名の先頭を `_` にします
## CSV ファイルの仕様
下記4列が必須の、CSV ファイルです。
- 1. Title : 件名(ご自身の分かりやすいように)
- 2. Command : ここにコマンドまたは ファイル/フォルダ のパスを記載
- 3. Params : Command 列のコマンドに引数として渡す内容
- 4. Tags : 検索キーワード用
Tips: Excel でも開きたい場合は BOM つき UTF-8 で保存
## その他 UI の地味ポイント
- 矢印/Home/End/Enter キー操作により、なるべくマウスを触らなくて済むようにしている
- Excel ファイルは右クリックから「読み取り専用で開く」を選択可能
scripts
launchan.ps1
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$Script:DefaultLocale = "en-US"
$Script:Strings = @{
"en-US" = @{
AppTitle = "launchan"
MainFormTitle = "launchan.ps1"
AllCategory = "All"
CsvDirectoryNotFound = "CSV directory not found: {0}"
NoCsvFilesFound = "No csv files found in: {0}"
NoDataInCsvFiles = "No data in csv files"
ExcelMenu = "Excel"
OpenReadOnlyExcel = "Open read-only"
OpenSeparateExcel = "Open in separate session"
OpenSeparateReadOnlyExcel = "Open in separate session, read-only"
OpenParentFolder = "Open parent folder"
CopyToClipboard = "Copy to clipboard"
CopyLaunchCommand = "Copy launch command"
CopyCell = "Copy cell"
OpenSourceCsv = "Open source CSV"
ReloadConfig = "Reload config"
OpenConfigFolder = "Open config folder"
LaunchFailed = "Failed to launch command."
CommandLabel = "Command"
ParamsLabel = "Params"
ReasonLabel = "Reason"
UnexpectedError = "An unexpected error occurred. See the error log for details."
}
"ja-JP" = @{
AppTitle = "launchan"
MainFormTitle = "launchan.ps1"
AllCategory = "すべて"
CsvDirectoryNotFound = "CSV ディレクトリが見つかりません: {0}"
NoCsvFilesFound = "CSV ファイルが見つかりません: {0}"
NoDataInCsvFiles = "CSV ファイルにデータがありません"
ExcelMenu = "Excel"
OpenReadOnlyExcel = "読み取り専用で開く"
OpenSeparateExcel = "別セッションで開く"
OpenSeparateReadOnlyExcel = "別セッションかつ読み取り専用で開く"
OpenParentFolder = "親フォルダを開く"
CopyToClipboard = "クリップボードにコピー"
CopyLaunchCommand = "実行コマンドをコピー"
CopyCell = "セルをコピー"
OpenSourceCsv = "設定ファイル(CSV) を開く"
ReloadConfig = "設定ファイルをリロード"
OpenConfigFolder = "設定ファイルフォルダを開く"
LaunchFailed = "コマンドの起動に失敗しました。"
CommandLabel = "コマンド"
ParamsLabel = "引数"
ReasonLabel = "理由"
UnexpectedError = "予期しないエラーが発生しました。詳細はエラーログを確認してください。"
}
}
function Get-AppLocale {
$cultureName = [System.Globalization.CultureInfo]::CurrentUICulture.Name
if ($Script:Strings.ContainsKey($cultureName)) {
return $cultureName
}
return $Script:DefaultLocale
}
function Get-String {
param(
[Parameter(Mandatory = $true)]
[string]$Key,
[object[]]$Args = @()
)
$table = $Script:Strings[$Script:Locale]
if ($null -eq $table -or -not $table.ContainsKey($Key)) {
$table = $Script:Strings[$Script:DefaultLocale]
}
if ($null -eq $table -or -not $table.ContainsKey($Key)) {
return $Key
}
$text = $table[$Key]
if ($Args.Count -gt 0) {
return [string]::Format($text, $Args)
}
return $text
}
$Script:Locale = Get-AppLocale
function Main {
$settings = [pscustomobject]@{
WindowWidth = 790
WindowHeight = 280
WindowPosition = "BottomLeft"
DataDirectoryName = "launchan-data"
}
$table = Import-LaunchTable -DataDirectoryName $settings.DataDirectoryName
if ($null -eq $table.DefaultView) {
Write-Debug "table.DefaultView is null"
return
}
$layout = Get-Size -Width $settings.WindowWidth -Height $settings.WindowHeight
$app = [pscustomobject]@{
Table = $table
LastHoverRowIndex = -1
Form = $null
TextBox = $null
ComboBox = $null
Grid = $null
Settings = $settings
}
$form = New-MainForm -Layout $layout -WindowPosition $settings.WindowPosition
$textBox = New-SearchTextBox -Layout $layout
$comboBox = New-CategoryComboBox -Table $table -Layout $layout
$grid = New-ResultGrid -Table $table -Layout $layout
$app.Form = $form
$app.TextBox = $textBox
$app.ComboBox = $comboBox
$app.Grid = $grid
$form.Controls.AddRange(@($textBox, $comboBox, $grid))
Register-SearchEvents -App $app
Register-GridEvents -Form $form -Grid $grid -State $app
Register-ContextMenu -Grid $grid -App $app
Register-FormEvents -Form $form -Grid $grid
[void]$form.ShowDialog()
}
function Import-LaunchTable([string]$DataDirectoryName) {
$csvDir = Join-Path $PSScriptRoot $DataDirectoryName
if (-not (Test-Path -LiteralPath $csvDir -PathType Container)) {
[System.Windows.Forms.MessageBox]::Show((Get-String -Key "CsvDirectoryNotFound" -Args @($csvDir)), (Get-String -Key "AppTitle"), 'OK', 'Error')
exit 1
}
$csvFiles = @(
Get-ChildItem -Path $csvDir -Filter *.csv |
Where-Object { -not $_.BaseName.StartsWith("_") }
)
if ($csvFiles.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show((Get-String -Key "NoCsvFilesFound" -Args @($csvDir)), (Get-String -Key "AppTitle"), 'OK', 'Error')
exit 1
}
$table = New-Object System.Data.DataTable
$isColumnsAdded = $false
foreach ($csvFile in $csvFiles) {
$rows = Import-Csv -Path $csvFile.FullName -Encoding UTF8
if ($rows.Count -eq 0) {
continue
}
if (-not $isColumnsAdded) {
$rows[0].PSObject.Properties.Name | ForEach-Object {
[void]$table.Columns.Add($_)
}
[void]$table.Columns.Add("Source")
$isColumnsAdded = $true
}
foreach ($row in $rows) {
$dataRow = $table.NewRow()
foreach ($column in $row.PSObject.Properties) {
$dataRow[$column.Name] = $column.Value
}
$dataRow["Source"] = $csvFile.Name
$table.Rows.Add($dataRow)
}
}
if ($table.Rows.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show((Get-String -Key "NoDataInCsvFiles"), (Get-String -Key "AppTitle"), 'OK', 'Error')
exit 1
}
return ,$table
}
function Get-Size([int]$Width, [int]$Height) {
return @{
FormSize = [System.Drawing.Size]::new($Width, $Height)
TextBoxWidth = $Width - 150
ComboBoxWidth = 100
GridSize = [System.Drawing.Size]::new($Width - 40, $Height - 90)
TextBoxLocation = [System.Drawing.Point]::new(10, 10)
ComboBoxLocation = [System.Drawing.Point]::new($Width - 130, 10)
GridLocation = [System.Drawing.Point]::new(10, 40)
}
}
function New-MainForm($Layout, [string]$WindowPosition) {
$form = New-Object System.Windows.Forms.Form
$form.Text = Get-String -Key "MainFormTitle"
$form.Size = $Layout.FormSize
$screenBounds = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
$form.StartPosition = 'Manual'
$form.Location = Get-FormLocation -ScreenBounds $screenBounds -FormSize $form.Size -WindowPosition $WindowPosition
return $form
}
function Get-FormLocation($ScreenBounds, [System.Drawing.Size]$FormSize, [string]$WindowPosition) {
switch ($WindowPosition) {
"BottomLeft" {
return [System.Drawing.Point]::new(-10, ($ScreenBounds.Height - $FormSize.Height + 10))
}
default {
return [System.Drawing.Point]::new(-10, ($ScreenBounds.Height - $FormSize.Height + 10))
}
}
}
function New-SearchTextBox($Layout) {
$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Location = $Layout.TextBoxLocation
$textBox.Width = $Layout.TextBoxWidth
$textBox.Anchor = "Top, Left, Right"
$textBox.ShortcutsEnabled = $true
return $textBox
}
function New-CategoryComboBox {
param(
[System.Data.DataTable]$Table,
$Layout
)
$comboBox = New-Object System.Windows.Forms.ComboBox
$comboBox.DropDownStyle = 'DropDownList'
$comboBox.Location = $Layout.ComboBoxLocation
$comboBox.Width = $Layout.ComboBoxWidth
$comboBox.Anchor = "Top, Right"
Initialize-CategoryComboBox -ComboBox $comboBox -Table $Table
return $comboBox
}
function Get-CategoryItems {
param(
[System.Data.DataTable]$Table
)
if ($null -eq $Table) {
throw "Get-CategoryItems: Table is null."
}
if (-not $Table.Columns.Contains("Source")) {
return @()
}
return @(
$Table.Rows |
ForEach-Object { $_["Source"] } |
Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } |
Sort-Object -Unique
)
}
function New-ResultGrid {
param(
[System.Data.DataTable]$Table,
$Layout
)
$grid = New-Object System.Windows.Forms.DataGridView
$grid.Location = $Layout.GridLocation
$grid.Size = $Layout.GridSize
$grid.Anchor = "Top, Left, Right, Bottom"
$grid.ReadOnly = $true
$grid.SelectionMode = 'FullRowSelect'
$grid.AutoSizeColumnsMode = 'None'
$grid.DataSource = $Table.DefaultView
$grid.RowHeadersVisible = $false
$grid.AllowUserToAddRows = $false
$grid.AllowUserToResizeRows = $false
$grid.AutoSizeRowsMode = [System.Windows.Forms.DataGridViewAutoSizeRowsMode]::None
$grid.ColumnHeadersHeightSizeMode = [System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode]::DisableResizing
$grid.RowTemplate.Height = 22
$grid.ColumnHeadersHeight = 22
$grid.DefaultCellStyle.WrapMode = [System.Windows.Forms.DataGridViewTriState]::False
$grid.EnableHeadersVisualStyles = $false
return $grid
}
function Initialize-CategoryComboBox($ComboBox, [System.Data.DataTable]$Table, [string]$SelectedCategory = (Get-String -Key "AllCategory")) {
if ($null -eq $ComboBox) {
return
}
$allCategory = Get-String -Key "AllCategory"
$ComboBox.Items.Clear()
$ComboBox.Items.Add($allCategory) | Out-Null
foreach ($category in Get-CategoryItems -Table $Table) {
$ComboBox.Items.Add($category) | Out-Null
}
$selectedIndex = $ComboBox.Items.IndexOf($SelectedCategory)
$ComboBox.SelectedIndex = if ($selectedIndex -ge 0) { $selectedIndex } else { 0 }
}
function Register-SearchEvents($App) {
$TextBox = $App.TextBox
$ComboBox = $App.ComboBox
$Grid = $App.Grid
$TextBox.Add_TextChanged({
Set-LaunchFilter -Table $App.Table -Query $TextBox.Text -Category $ComboBox.SelectedItem
})
$ComboBox.Add_SelectedIndexChanged({
Set-LaunchFilter -Table $App.Table -Query $TextBox.Text -Category $ComboBox.SelectedItem
})
$TextBox.Add_KeyDown({
param($s, $e)
if ($e.Control -and $e.KeyCode -eq [System.Windows.Forms.Keys]::A) {
$TextBox.SelectAll()
$e.Handled = $true
return
}
if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
if ($Grid.SelectedRows.Count -eq 1) {
Invoke-LaunchItem -Row $Grid.SelectedRows[0]
$e.SuppressKeyPress = $true
$e.Handled = $true
}
return
}
if ($e.KeyCode -in @(
[System.Windows.Forms.Keys]::Up,
[System.Windows.Forms.Keys]::Down,
[System.Windows.Forms.Keys]::Home,
[System.Windows.Forms.Keys]::End
)) {
Move-GridSelectionFromSearchBox -Grid $Grid -KeyCode $e.KeyCode
$e.SuppressKeyPress = $true
$e.Handled = $true
return
}
if ($e.KeyCode -in @(
[System.Windows.Forms.Keys]::Left,
[System.Windows.Forms.Keys]::Right
)) {
Clear-TextBoxSelectionToCaret -TextBox $TextBox
}
})
}
function Register-GridEvents($Form, $Grid, $State) {
$invokeLaunchItem = ${function:Invoke-LaunchItem}
$setGridHoverRow = ${function:Set-GridHoverRow}
$clearGridHoverRow = ${function:Clear-GridHoverRow}
$setCellToolTipIfNeeded = ${function:Set-CellToolTipIfNeeded}
$cellClickHandler = {
param($s, $e)
if ($e.RowIndex -lt 0) {
return
}
$row = $Grid.Rows[$e.RowIndex]
if ($row) {
& $invokeLaunchItem -Row $row
}
}.GetNewClosure()
$Grid.Add_CellClick($cellClickHandler)
$keyDownHandler = {
param($s, $e)
if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
if ($Grid.IsCurrentCellInEditMode -or $Grid.EditingControl) {
return
}
$row = $Grid.CurrentRow
if ($row) {
& $invokeLaunchItem -Row $row
$e.SuppressKeyPress = $true
}
}
}.GetNewClosure()
$Grid.Add_KeyDown($keyDownHandler)
$cellMouseEnterHandler = {
param($s, $e)
if ($e.ColumnIndex -ge 0 -and $e.RowIndex -ge 0) {
$Grid.Cursor = [System.Windows.Forms.Cursors]::Hand
}
if ($e.RowIndex -ge 0) {
& $setGridHoverRow -Grid $Grid -State $State -RowIndex $e.RowIndex
}
}.GetNewClosure()
$Grid.Add_CellMouseEnter($cellMouseEnterHandler)
$cellMouseLeaveHandler = {
param($s, $e)
$Grid.Cursor = [System.Windows.Forms.Cursors]::Default
& $clearGridHoverRow -Grid $Grid -State $State
}.GetNewClosure()
$Grid.Add_CellMouseLeave($cellMouseLeaveHandler)
$cellFormattingHandler = {
param($s, $e)
& $setCellToolTipIfNeeded -Grid $Grid -RowIndex $e.RowIndex -ColumnIndex $e.ColumnIndex
}.GetNewClosure()
$Grid.Add_CellFormatting($cellFormattingHandler)
$previewKeyDownHandler = {
param($s, $e)
if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Tab) {
$Form.SelectNextControl($Grid, $true, $true, $true, $true)
$e.IsInputKey = $false
}
}.GetNewClosure()
$Grid.Add_PreviewKeyDown($previewKeyDownHandler)
}
function Register-ContextMenu($Grid, $App) {
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$menu.ShowImageMargin = $false
$showGridContextMenu = ${function:Show-GridContextMenu}
$mouseDownHandler = {
param($s, $e)
if ($e.Button -ne [System.Windows.Forms.MouseButtons]::Right) {
return
}
$hit = $Grid.HitTest($e.X, $e.Y)
if ($hit.RowIndex -lt 0) {
return
}
$Grid.ClearSelection()
$Grid.Rows[$hit.RowIndex].Selected = $true
if ($hit.ColumnIndex -ge 0) {
$Grid.CurrentCell = $Grid.Rows[$hit.RowIndex].Cells[$hit.ColumnIndex]
}
& $showGridContextMenu -Menu $menu -Grid $Grid -App $App -Location $e.Location -RowIndex $hit.RowIndex -ColumnIndex $hit.ColumnIndex
}.GetNewClosure()
$Grid.Add_MouseDown($mouseDownHandler)
}
function Register-FormEvents($Form, $Grid) {
$Form.Add_Shown({
try {
Set-GridColumns -Grid $Grid
} catch {
Write-Debug $_
}
})
}
function Set-LaunchFilter($Table, [string]$Query, [string]$Category) {
if ($null -eq $Table -or $null -eq $Table.DefaultView) {
return
}
$filters = @()
if (-not [string]::IsNullOrWhiteSpace($Query)) {
$keywordFilters = Get-KeywordFilters -Query $Query
if ($keywordFilters.Count -gt 0) {
$filters += ($keywordFilters -join " AND ")
}
}
if ($Category -and $Category -ne (Get-String -Key "AllCategory")) {
$escapedCategory = $Category -replace "'", "''"
$filters += "Source = '$escapedCategory'"
}
$Table.DefaultView.RowFilter = ($filters -join " AND ")
}
function Move-GridSelectionFromSearchBox($Grid, [System.Windows.Forms.Keys]$KeyCode) {
if ($null -eq $Grid -or $Grid.Rows.Count -eq 0) {
return
}
$targetRowIndex = 0
$hasCurrentRow = ($null -ne $Grid.CurrentRow)
if ($hasCurrentRow) {
$targetRowIndex = $Grid.CurrentRow.Index
}
switch ($KeyCode) {
([System.Windows.Forms.Keys]::Up) {
if (-not $hasCurrentRow) {
$targetRowIndex = 0
break
}
$targetRowIndex = [Math]::Max(0, $targetRowIndex - 1)
break
}
([System.Windows.Forms.Keys]::Down) {
if (-not $hasCurrentRow) {
$targetRowIndex = 0
break
}
$targetRowIndex = [Math]::Min($Grid.Rows.Count - 1, $targetRowIndex + 1)
break
}
([System.Windows.Forms.Keys]::Home) {
$targetRowIndex = 0
break
}
([System.Windows.Forms.Keys]::End) {
$targetRowIndex = $Grid.Rows.Count - 1
break
}
}
$Grid.ClearSelection()
$Grid.Rows[$targetRowIndex].Selected = $true
$Grid.CurrentCell = $Grid.Rows[$targetRowIndex].Cells[0]
Set-GridRowVisible -Grid $Grid -TargetRowIndex $targetRowIndex -KeyCode $KeyCode
}
function Set-GridRowVisible($Grid, [int]$TargetRowIndex, [System.Windows.Forms.Keys]$KeyCode) {
if ($null -eq $Grid -or $Grid.Rows.Count -eq 0) {
return
}
if ($TargetRowIndex -lt 0 -or $TargetRowIndex -ge $Grid.Rows.Count) {
return
}
$firstDisplayedRowIndex = $Grid.FirstDisplayedScrollingRowIndex
if ($firstDisplayedRowIndex -lt 0) {
$Grid.FirstDisplayedScrollingRowIndex = $TargetRowIndex
return
}
$visibleRowCount = $Grid.DisplayedRowCount($false)
if ($visibleRowCount -le 0) {
$Grid.FirstDisplayedScrollingRowIndex = $TargetRowIndex
return
}
$lastDisplayedRowIndex = [Math]::Min($Grid.Rows.Count - 1, $firstDisplayedRowIndex + $visibleRowCount - 1)
if ($TargetRowIndex -ge $firstDisplayedRowIndex -and $TargetRowIndex -le $lastDisplayedRowIndex) {
return
}
switch ($KeyCode) {
([System.Windows.Forms.Keys]::Down) {
$Grid.FirstDisplayedScrollingRowIndex = [Math]::Max(0, $TargetRowIndex - $visibleRowCount + 1)
break
}
([System.Windows.Forms.Keys]::End) {
$Grid.FirstDisplayedScrollingRowIndex = [Math]::Max(0, $TargetRowIndex - $visibleRowCount + 1)
break
}
default {
$Grid.FirstDisplayedScrollingRowIndex = $TargetRowIndex
break
}
}
}
function Clear-TextBoxSelectionToCaret($TextBox) {
if ($null -eq $TextBox) {
return
}
$selectionStart = $TextBox.SelectionStart
if ($TextBox.SelectionLength -gt 0 -and $selectionStart -lt $TextBox.TextLength) {
$selectionStart += $TextBox.SelectionLength
}
$TextBox.SelectionStart = $selectionStart
$TextBox.SelectionLength = 0
}
function Get-KeywordFilters([string]$Query) {
$terms = $Query -split '\s+' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
if ($terms.Count -eq 0) {
return @()
}
$columns = @("Title", "Command", "Params", "Tags")
$filters = @()
foreach ($term in $terms) {
$filters += "(" + ((Get-ColumnFiltersForTerm -Term $term -Columns $columns) -join " OR ") + ")"
}
return $filters
}
function Get-ColumnFiltersForTerm([string]$Term, [string[]]$Columns) {
$normalizedTerm = $Term.Trim()
if ([string]::IsNullOrWhiteSpace($normalizedTerm)) {
return @("1 = 1")
}
$escapedTerm = ConvertTo-RowFilterLikeValue -Value $normalizedTerm
$pattern = "%$escapedTerm%"
return @(
$Columns | ForEach-Object { "CONVERT([{0}], 'System.String') LIKE '{1}'" -f $_, $pattern }
)
}
function ConvertTo-RowFilterLikeValue([string]$Value) {
$escapedValue = $Value -replace "'", "''"
$escapedValue = $escapedValue -replace '\[', '[[]'
$escapedValue = $escapedValue -replace '\]', '[]]'
$escapedValue = $escapedValue -replace '%', '[%]'
$escapedValue = $escapedValue -replace '\*', '[*]'
return $escapedValue
}
function Set-GridHoverRow($Grid, $State, [int]$RowIndex) {
$lastRowIndex = $State.LastHoverRowIndex
if ($lastRowIndex -ge 0 -and $lastRowIndex -ne $RowIndex) {
$Grid.Rows[$lastRowIndex].DefaultCellStyle.BackColor = [System.Drawing.Color]::White
}
$Grid.Rows[$RowIndex].DefaultCellStyle.BackColor = [System.Drawing.Color]::AliceBlue
$State.LastHoverRowIndex = $RowIndex
}
function Clear-GridHoverRow($Grid, $State) {
$lastRowIndex = $State.LastHoverRowIndex
if ($lastRowIndex -ge 0) {
$Grid.Rows[$lastRowIndex].DefaultCellStyle.BackColor = [System.Drawing.Color]::White
$State.LastHoverRowIndex = -1
}
}
function Reload-LaunchData($App) {
if ($null -eq $App) {
return
}
$table = Import-LaunchTable -DataDirectoryName $App.Settings.DataDirectoryName
if ($null -eq $table -or $null -eq $table.DefaultView) {
return
}
$currentQuery = [string]$App.TextBox.Text
$currentCategory = [string]$App.ComboBox.SelectedItem
$App.Table = $table
$App.LastHoverRowIndex = -1
$App.Grid.DataSource = $table.DefaultView
Initialize-CategoryComboBox -ComboBox $App.ComboBox -Table $table -SelectedCategory $currentCategory
Set-LaunchFilter -Table $table -Query $currentQuery -Category $App.ComboBox.SelectedItem
Set-GridColumns -Grid $App.Grid
}
function Set-CellToolTipIfNeeded($Grid, [int]$RowIndex, [int]$ColumnIndex) {
if ($RowIndex -lt 0 -or $ColumnIndex -lt 0) {
return
}
$cell = $Grid.Rows[$RowIndex].Cells[$ColumnIndex]
$text = $cell.Value
if ($null -eq $text -or $text -isnot [string]) {
return
}
$cellRectangle = $Grid.GetCellDisplayRectangle($ColumnIndex, $RowIndex, $false)
$textSize = [System.Windows.Forms.TextRenderer]::MeasureText($text, $Grid.Font)
$cell.ToolTipText = if ($textSize.Width -gt $cellRectangle.Width) { $text } else { "" }
}
function Show-GridContextMenu($Menu, $Grid, $App, $Location, [int]$RowIndex, [int]$ColumnIndex) {
if ($null -eq $Menu -or $null -eq $Grid) {
return
}
$reloadLaunchData = ${function:Reload-LaunchData}
$testIsExcelFilePath = ${function:Test-IsExcelFilePath}
$getExcelExecutablePath = ${function:Get-ExcelExecutablePath}
$row = $Grid.Rows[$RowIndex]
$commandPath = Get-ExpandedRowValue -Row $row -ColumnName "Command"
$sourceFilePath = Get-SourceFilePath -Row $row -DataDirectoryName $App.Settings.DataDirectoryName
$configFolderPath = Get-ConfigFolderPath -DataDirectoryName $App.Settings.DataDirectoryName
$cellText = Get-GridCellText -Grid $Grid -RowIndex $RowIndex -ColumnIndex $ColumnIndex
$launchCommandText = Get-LaunchCommandText -Row $row
$parentFolderPath = Get-ParentFolderPath -Path $commandPath
$excelExecutablePath = & $getExcelExecutablePath
$canOpenWithExcel = (& $testIsExcelFilePath -Path $commandPath) -and -not [string]::IsNullOrWhiteSpace($excelExecutablePath)
$Menu.Items.Clear()
$openParentFolderItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openParentFolderItem.Text = Get-String -Key "OpenParentFolder"
$openParentFolderItem.Enabled = -not [string]::IsNullOrWhiteSpace($parentFolderPath)
$openParentFolderItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($parentFolderPath)) {
Start-Process -FilePath "explorer.exe" -ArgumentList @($parentFolderPath)
}
}.GetNewClosure())
$Menu.Items.Add($openParentFolderItem) | Out-Null
$copyMenuItem = New-Object System.Windows.Forms.ToolStripMenuItem
$copyMenuItem.Text = Get-String -Key "CopyToClipboard"
$copyLaunchCommandItem = New-Object System.Windows.Forms.ToolStripMenuItem
$copyLaunchCommandItem.Text = Get-String -Key "CopyLaunchCommand"
$copyLaunchCommandItem.Enabled = -not [string]::IsNullOrWhiteSpace($launchCommandText)
$copyLaunchCommandItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($launchCommandText)) {
[System.Windows.Forms.Clipboard]::SetText($launchCommandText)
}
}.GetNewClosure())
$copyMenuItem.DropDownItems.Add($copyLaunchCommandItem) | Out-Null
$copyCellItem = New-Object System.Windows.Forms.ToolStripMenuItem
$copyCellItem.Text = Get-String -Key "CopyCell"
$copyCellItem.Enabled = -not [string]::IsNullOrWhiteSpace($cellText)
$copyCellItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($cellText)) {
[System.Windows.Forms.Clipboard]::SetText($cellText)
}
}.GetNewClosure())
$copyMenuItem.DropDownItems.Add($copyCellItem) | Out-Null
$Menu.Items.Add($copyMenuItem) | Out-Null
$excelMenuItem = New-Object System.Windows.Forms.ToolStripMenuItem
$excelMenuItem.Text = Get-String -Key "ExcelMenu"
$openReadOnlyItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openReadOnlyItem.Text = Get-String -Key "OpenReadOnlyExcel"
$openReadOnlyItem.Enabled = $canOpenWithExcel
$openReadOnlyItem.Add_Click({
if ($canOpenWithExcel) {
Start-Process -FilePath $excelExecutablePath -ArgumentList @("/r", $commandPath)
}
}.GetNewClosure())
$excelMenuItem.DropDownItems.Add($openReadOnlyItem) | Out-Null
$openSeparateItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openSeparateItem.Text = Get-String -Key "OpenSeparateExcel"
$openSeparateItem.Enabled = $canOpenWithExcel
$openSeparateItem.Add_Click({
if ($canOpenWithExcel) {
Start-Process -FilePath $excelExecutablePath -ArgumentList @("/x", $commandPath)
}
}.GetNewClosure())
$excelMenuItem.DropDownItems.Add($openSeparateItem) | Out-Null
$openSeparateReadOnlyItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openSeparateReadOnlyItem.Text = Get-String -Key "OpenSeparateReadOnlyExcel"
$openSeparateReadOnlyItem.Enabled = $canOpenWithExcel
$openSeparateReadOnlyItem.Add_Click({
if ($canOpenWithExcel) {
Start-Process -FilePath $excelExecutablePath -ArgumentList @("/x", "/r", $commandPath)
}
}.GetNewClosure())
$excelMenuItem.DropDownItems.Add($openSeparateReadOnlyItem) | Out-Null
$Menu.Items.Add($excelMenuItem) | Out-Null
$openSourceItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openSourceItem.Text = Get-String -Key "OpenSourceCsv"
$openSourceItem.Enabled = -not [string]::IsNullOrWhiteSpace($sourceFilePath)
$openSourceItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($sourceFilePath)) {
Start-Process -FilePath $sourceFilePath
}
}.GetNewClosure())
$Menu.Items.Add($openSourceItem) | Out-Null
$Menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
$reloadConfigItem = New-Object System.Windows.Forms.ToolStripMenuItem
$reloadConfigItem.Text = Get-String -Key "ReloadConfig"
$reloadConfigItem.Add_Click({
& $reloadLaunchData -App $App
}.GetNewClosure())
$Menu.Items.Add($reloadConfigItem) | Out-Null
$openConfigFolderItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openConfigFolderItem.Text = Get-String -Key "OpenConfigFolder"
$openConfigFolderItem.Enabled = -not [string]::IsNullOrWhiteSpace($configFolderPath)
$openConfigFolderItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($configFolderPath)) {
Start-Process -FilePath "explorer.exe" -ArgumentList @($configFolderPath)
}
}.GetNewClosure())
$Menu.Items.Add($openConfigFolderItem) | Out-Null
$Menu.Show($Grid, $Location)
}
function Get-ExpandedRowValue($Row, [string]$ColumnName) {
if ($null -eq $Row -or [string]::IsNullOrWhiteSpace($ColumnName)) {
return ""
}
$rawValue = [string]$Row.Cells[$ColumnName].Value
return [Environment]::ExpandEnvironmentVariables($rawValue)
}
function Format-CommandForClipboard([string]$Command) {
if ([string]::IsNullOrWhiteSpace($Command)) {
return ""
}
if ($Command.StartsWith('"') -and $Command.EndsWith('"')) {
return $Command
}
if ($Command -match '\s') {
$escapedCommand = $Command -replace '"', '\"'
return """$escapedCommand"""
}
return $Command
}
function Get-LaunchCommandText($Row) {
if ($null -eq $Row) {
return ""
}
$command = Get-ExpandedRowValue -Row $Row -ColumnName "Command"
$params = Get-ExpandedRowValue -Row $Row -ColumnName "Params"
if ([string]::IsNullOrWhiteSpace($command)) {
return ""
}
$commandText = Format-CommandForClipboard -Command $command
if ([string]::IsNullOrWhiteSpace($params)) {
return $commandText
}
return "$commandText $params"
}
function Get-GridCellText($Grid, [int]$RowIndex, [int]$ColumnIndex) {
if ($null -eq $Grid -or $RowIndex -lt 0 -or $ColumnIndex -lt 0) {
return ""
}
$value = $Grid.Rows[$RowIndex].Cells[$ColumnIndex].Value
if ($null -eq $value) {
return ""
}
return [string]$value
}
function Get-SourceFilePath($Row, [string]$DataDirectoryName) {
if ($null -eq $Row) {
return ""
}
$sourceName = [string]$Row.Cells["Source"].Value
if ([string]::IsNullOrWhiteSpace($sourceName)) {
return ""
}
$sourceFilePath = Join-Path (Join-Path $PSScriptRoot $DataDirectoryName) $sourceName
if (-not (Test-Path -LiteralPath $sourceFilePath -PathType Leaf)) {
return ""
}
return $sourceFilePath
}
function Get-ConfigFolderPath([string]$DataDirectoryName) {
$configFolderPath = Join-Path $PSScriptRoot $DataDirectoryName
if (-not (Test-Path -LiteralPath $configFolderPath -PathType Container)) {
return ""
}
return $configFolderPath
}
function Test-IsExcelFilePath([string]$Path) {
if ([string]::IsNullOrWhiteSpace($Path)) {
return $false
}
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
return $false
}
return ($Path -match '\.(xlsx?|xlsm|xlsb)$')
}
function Get-ExcelExecutablePath {
$appPathRegistryKeys = @(
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\excel.exe",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\excel.exe",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\excel.exe"
)
foreach ($registryKey in $appPathRegistryKeys) {
try {
if (-not (Test-Path -LiteralPath $registryKey)) {
continue
}
$item = Get-Item -LiteralPath $registryKey
$candidate = [string]$item.GetValue("")
if ([string]::IsNullOrWhiteSpace($candidate)) {
continue
}
$candidate = $candidate.Trim('"')
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
return $candidate
}
} catch {
Write-Debug $_
}
}
$command = Get-Command "excel.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($null -ne $command -and -not [string]::IsNullOrWhiteSpace($command.Source)) {
return $command.Source
}
return ""
}
function Get-ParentFolderPath([string]$Path) {
if ([string]::IsNullOrWhiteSpace($Path)) {
return ""
}
if (-not (Test-Path -LiteralPath $Path)) {
return ""
}
$parent = Split-Path -Path $Path -Parent
if ([string]::IsNullOrWhiteSpace($parent)) {
if (Test-Path -LiteralPath $Path -PathType Container) {
return $Path
}
return ""
}
return $parent
}
function Set-GridColumns($Grid) {
$Grid.Columns["Title"].DisplayIndex = 0
$Grid.Columns["Command"].DisplayIndex = 1
$Grid.Columns["Params"].DisplayIndex = 2
$Grid.Columns["Tags"].DisplayIndex = 3
$Grid.Columns["Source"].DisplayIndex = 4
$Grid.AutoSizeColumnsMode = 'None'
$Grid.ScrollBars = 'Both'
$Grid.Columns["Title"].Width = 200
$Grid.Columns["Command"].Width = 150
$Grid.Columns["Params"].Width = 100
$Grid.Columns["Tags"].Width = 100
$Grid.Columns["Source"].Width = 80
foreach ($column in $Grid.Columns) {
$column.Resizable = [System.Windows.Forms.DataGridViewTriState]::True
}
}
function Show-LaunchFailureDialog([string]$Command, [string]$Params, [string]$Reason) {
$messageLines = @(
(Get-String -Key "LaunchFailed"),
"",
("{0}: {1}" -f (Get-String -Key "CommandLabel"), $Command)
)
if (-not [string]::IsNullOrWhiteSpace($Params)) {
$messageLines += ("{0}: {1}" -f (Get-String -Key "ParamsLabel"), $Params)
}
if (-not [string]::IsNullOrWhiteSpace($Reason)) {
$messageLines += ("{0}: {1}" -f (Get-String -Key "ReasonLabel"), $Reason)
}
[System.Windows.Forms.MessageBox]::Show(
($messageLines -join [Environment]::NewLine),
(Get-String -Key "AppTitle"),
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Error
) | Out-Null
}
function Get-LogFilePath {
# $logDir = Join-Path $env:LOCALAPPDATA "launchan"
$logDir = $PSScriptRoot
if (-not (Test-Path -LiteralPath $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
}
return (Join-Path $logDir "launchan-error.log")
}
function Write-FatalErrorLog($ErrorRecord) {
try {
$logFilePath = Get-LogFilePath
$positionMessage = ""
if ($null -ne $ErrorRecord.InvocationInfo) {
$positionMessage = $ErrorRecord.InvocationInfo.PositionMessage
}
$logLines = @(
("[{0}]" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss")),
("Message: {0}" -f $ErrorRecord.Exception.Message),
("Script: {0}" -f $ErrorRecord.InvocationInfo.ScriptName),
("Line: {0}" -f $ErrorRecord.InvocationInfo.ScriptLineNumber),
("Position: {0}" -f $positionMessage),
""
)
Add-Content -Path $logFilePath -Value $logLines -Encoding UTF8
} catch {
Write-Debug $_
}
}
function Show-FatalErrorDialog {
[System.Windows.Forms.MessageBox]::Show(
(Get-String -Key "UnexpectedError"),
(Get-String -Key "AppTitle"),
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Error
) | Out-Null
}
function Invoke-LaunchItem($Row) {
$rawCommand = $Row.Cells["Command"].Value
$rawParams = $Row.Cells["Params"].Value
$command = [Environment]::ExpandEnvironmentVariables($rawCommand)
$params = [Environment]::ExpandEnvironmentVariables($rawParams)
Write-Debug "Command: $command"
Write-Debug "Params: $params"
try {
if ([string]::IsNullOrEmpty($params)) {
Start-Process -FilePath $command -ErrorAction Stop
return
}
Start-Process -FilePath $command -ArgumentList $params -ErrorAction Stop
} catch {
Show-LaunchFailureDialog -Command $command -Params $params -Reason $_.Exception.Message
}
}
try {
Main
} catch {
Write-FatalErrorLog -ErrorRecord $_
Show-FatalErrorDialog
}
launchan.vbs
Set fso = CreateObject("Scripting.FileSystemObject")
Set shell = CreateObject("WScript.Shell")
ps1Path = fso.GetParentFolderName(WScript.ScriptFullName) & "\launchan.ps1"
shell.Run "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & ps1Path & """", 0, False