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
# Copyright (c) 2026 takaaki024
# Licensed under the MIT License
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
if (-not ("LaunchanNativeMethods" -as [type]) -or -not ("LaunchanToolStripRenderer" -as [type])) {
Add-Type -ReferencedAssemblies @("System.Drawing.dll", "System.Windows.Forms.dll") -TypeDefinition @"
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public static class LaunchanNativeMethods
{
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int attributeValue, int attributeSize);
public static bool SetImmersiveDarkMode(IntPtr hwnd, bool enabled)
{
if (hwnd == IntPtr.Zero)
{
return false;
}
int value = enabled ? 1 : 0;
int size = sizeof(int);
int result = DwmSetWindowAttribute(hwnd, 20, ref value, size);
if (result != 0)
{
result = DwmSetWindowAttribute(hwnd, 19, ref value, size);
}
return result == 0;
}
}
public sealed class LaunchanToolStripColorTable : ProfessionalColorTable
{
private readonly Color backColor;
private readonly Color borderColor;
private readonly Color selectedBackColor;
public LaunchanToolStripColorTable(Color backColor, Color borderColor, Color selectedBackColor)
{
this.backColor = backColor;
this.borderColor = borderColor;
this.selectedBackColor = selectedBackColor;
}
public override Color ToolStripDropDownBackground { get { return backColor; } }
public override Color ImageMarginGradientBegin { get { return backColor; } }
public override Color ImageMarginGradientMiddle { get { return backColor; } }
public override Color ImageMarginGradientEnd { get { return backColor; } }
public override Color MenuBorder { get { return borderColor; } }
public override Color MenuItemBorder { get { return borderColor; } }
public override Color MenuItemSelected { get { return selectedBackColor; } }
public override Color MenuItemSelectedGradientBegin { get { return selectedBackColor; } }
public override Color MenuItemSelectedGradientEnd { get { return selectedBackColor; } }
public override Color CheckBackground { get { return selectedBackColor; } }
public override Color CheckSelectedBackground { get { return selectedBackColor; } }
public override Color CheckPressedBackground { get { return selectedBackColor; } }
public override Color SeparatorDark { get { return borderColor; } }
public override Color SeparatorLight { get { return borderColor; } }
}
public sealed class LaunchanToolStripRenderer : ToolStripProfessionalRenderer
{
private readonly Color borderColor;
private readonly Color selectedBackColor;
public LaunchanToolStripRenderer(Color backColor, Color borderColor, Color selectedBackColor)
: base(new LaunchanToolStripColorTable(backColor, borderColor, selectedBackColor))
{
this.borderColor = borderColor;
this.selectedBackColor = selectedBackColor;
}
protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e)
{
if (e.Item.Selected)
{
using (SolidBrush brush = new SolidBrush(selectedBackColor))
{
e.Graphics.FillRectangle(brush, new Rectangle(Point.Empty, e.Item.Size));
}
using (Pen pen = new Pen(borderColor))
{
e.Graphics.DrawRectangle(pen, 0, 0, e.Item.Width - 1, e.Item.Height - 1);
}
return;
}
base.OnRenderMenuItemBackground(e);
}
}
"@
}
$Script:DefaultLocale = "en-US"
$Script:Strings = @{
"en-US" = @{
AppTitle = "launchan"
MainFormTitle = "launchan.ps1"
AllCategory = "All"
DataDirectoryNotFound = "Data folder not found: {0}"
NoCsvFilesFound = "No csv files found in: {0}"
NoDataInCsvFiles = "No data in csv files"
ExcelDataFilesSkipped = "Some Excel files were skipped."
NoReadableExcelSheets = "No readable sheets"
ExcelMenu = "Open in Excel"
OpenReadOnlyExcel = "Read-Only"
OpenSeparateExcel = "New Session"
OpenSeparateReadOnlyExcel = "New Session + Read-Only"
OpenParentFolder = "Open Parent Folder"
CopyToClipboard = "Copy to clipboard"
CopyLaunchCommand = "Copy Command to Execute"
CopyCell = "Copy Cell Value"
CopyRow = "Copy Row"
OpenSourceCsv = "Open Data File ({0})"
ReloadConfig = "Reload Data Files"
OpenConfigFolder = "Open Data Folder"
OpenConfigLogFolder = "Open Config/Log Folder"
SelectAllTextOnFocus = "Select All Text on Focus"
ShowBuiltIn = "Show Built-in"
ThemeMenu = "Theme"
ThemeSystem = "System"
ThemeLight = "Light"
ThemeDark = "Dark"
LaunchAtLogin = "Launch at Startup"
ChangeDataDirectory = "Change Data Folder ..."
ChooseDataDirectoryDescription = "Select the folder that contains launch data (CSV/Excel) files."
DataDirectoryRecoveryPrompt = "{0}`n`n[Yes] Choose another Data Folder`n[No] Reset to default launchan-data`n[Cancel] Exit"
OpenMenu = "Open"
OpenAssociatedApp = "Open File with Associated App"
OpenDirectoryExplorer = "Open Folder in File Explorer"
OpenDirectoryCommandPrompt = "Open in Command Prompt"
OpenDirectoryPowerShellPrompt = "Open in PowerShell"
OpenFileWithApp = "Open File with {0}"
OpenDirectoryWithApp = "Open Folder with {0}"
CopyDirectory = "Copy Folder Path"
CopyFilename = "Copy File Name"
ConfirmRegisterLaunchAtLogin = "Create a startup shortcut here?`n`n{0}`n`nTarget:`n{1}"
ConfirmUnregisterLaunchAtLogin = "Remove this startup shortcut?`n`n{0}"
LaunchAtLoginAlreadyRegistered = "Startup shortcut is already registered.`n`n{0}"
LaunchAtLoginAlreadyUnregistered = "Startup shortcut is already unregistered.`n`n{0}"
LaunchAtLoginTargetNotFound = "Launch target was not found.`n`n{0}"
LaunchAtLoginRegisterFailed = "Failed to register startup shortcut."
LaunchAtLoginUnregisterFailed = "Failed to remove startup shortcut."
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 = "すべて"
DataDirectoryNotFound = "Data フォルダが見つかりません: {0}"
NoCsvFilesFound = "CSV ファイルが見つかりません: {0}"
NoDataInCsvFiles = "CSV ファイルにデータがありません"
ExcelDataFilesSkipped = "一部の Excel ファイルをスキップしました。"
NoReadableExcelSheets = "読み込み対象のシートがありません"
ExcelMenu = "Excel で開く"
OpenReadOnlyExcel = "読み取り専用"
OpenSeparateExcel = "新しいセッション"
OpenSeparateReadOnlyExcel = "新しいセッション + 読み取り専用"
OpenParentFolder = "親フォルダを開く"
CopyToClipboard = "クリップボードにコピー"
CopyLaunchCommand = "実行コマンドをコピー"
CopyCell = "セルの値をコピー"
CopyRow = "行をコピー"
OpenSourceCsv = "Data ファイルを開く ({0})"
ReloadConfig = "Data ファイルをリロード"
OpenConfigFolder = "Data フォルダを開く"
OpenConfigLogFolder = "Config/Log フォルダを開く"
SelectAllTextOnFocus = "Select All Text on Focus"
ShowBuiltIn = "Built-in を表示"
ThemeMenu = "テーマ"
ThemeSystem = "システム"
ThemeLight = "ライト"
ThemeDark = "ダーク"
LaunchAtLogin = "スタートアップに登録"
ChangeDataDirectory = "Data フォルダを変更 ..."
ChooseDataDirectoryDescription = "Data (CSV/Excel) ファイルを含むフォルダを選択してください。"
DataDirectoryRecoveryPrompt = "{0}`n`n[はい] 別の Data フォルダを選択`n[いいえ] デフォルトの launchan-data に戻す`n[キャンセル] 終了"
OpenMenu = "開く"
OpenAssociatedApp = "関連付けアプリでファイルを開く"
OpenDirectoryExplorer = "エクスプローラーでフォルダを開く"
OpenDirectoryCommandPrompt = "コマンドプロンプトで開く"
OpenDirectoryPowerShellPrompt = "PowerShell で開く"
OpenFileWithApp = "{0} でファイルを開く"
OpenDirectoryWithApp = "{0} でフォルダを開く"
CopyDirectory = "フォルダのパスをコピー"
CopyFilename = "ファイル名をコピー"
ConfirmRegisterLaunchAtLogin = "この場所にスタートアップ用ショートカットを作成しますか?`n`n{0}`n`nリンク先:`n{1}"
ConfirmUnregisterLaunchAtLogin = "このスタートアップ用ショートカットを削除しますか?`n`n{0}"
LaunchAtLoginAlreadyRegistered = "スタートアップには既に登録済みです。`n`n{0}"
LaunchAtLoginAlreadyUnregistered = "スタートアップには既に存在しません。`n`n{0}"
LaunchAtLoginTargetNotFound = "起動対象が見つかりません。`n`n{0}"
LaunchAtLoginRegisterFailed = "スタートアップの登録に失敗しました。"
LaunchAtLoginUnregisterFailed = "スタートアップの登録解除に失敗しました。"
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 Get-LaunchConfigDirectoryPath {
return (Join-Path $PSScriptRoot "launchan-config")
}
function Ensure-LaunchConfigDirectory {
$configDir = Get-LaunchConfigDirectoryPath
if (-not (Test-Path -LiteralPath $configDir -PathType Container)) {
New-Item -ItemType Directory -Path $configDir -Force | Out-Null
}
return $configDir
}
function Read-JsonFile($Path) {
if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) {
return $null
}
try {
$content = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
if ([string]::IsNullOrWhiteSpace($content)) {
return $null
}
return ($content | ConvertFrom-Json)
}
catch {
Write-Debug $_
return $null
}
}
function Write-JsonFile($Path, $Value) {
$json = Format-JsonString -Json ($Value | ConvertTo-Json -Depth 8 -Compress)
Set-Content -LiteralPath $Path -Value $json -Encoding UTF8
}
function Format-JsonString([string]$Json) {
$builder = New-Object System.Text.StringBuilder
$indent = 0
$inString = $false
$escaped = $false
$indentText = " "
foreach ($char in $Json.ToCharArray()) {
if ($escaped) {
[void]$builder.Append($char)
$escaped = $false
continue
}
if ($char -eq '\') {
[void]$builder.Append($char)
$escaped = $inString
continue
}
if ($char -eq '"') {
[void]$builder.Append($char)
$inString = -not $inString
continue
}
if ($inString) {
[void]$builder.Append($char)
continue
}
switch ($char) {
{ $_ -eq '{' -or $_ -eq '[' } {
[void]$builder.Append($char)
[void]$builder.AppendLine()
$indent += 1
[void]$builder.Append($indentText * $indent)
break
}
{ $_ -eq '}' -or $_ -eq ']' } {
[void]$builder.AppendLine()
$indent = [Math]::Max(0, $indent - 1)
[void]$builder.Append($indentText * $indent)
[void]$builder.Append($char)
break
}
{ $_ -eq ',' } {
[void]$builder.Append($char)
[void]$builder.AppendLine()
[void]$builder.Append($indentText * $indent)
break
}
{ $_ -eq ':' } {
[void]$builder.Append(": ")
break
}
default {
[void]$builder.Append($char)
}
}
}
return $builder.ToString()
}
function Get-JsonIntValue($Object, [string]$Name, [int]$DefaultValue) {
if ($null -eq $Object -or $null -eq $Object.$Name) {
return $DefaultValue
}
try {
return [int]$Object.$Name
}
catch {
return $DefaultValue
}
}
function Get-JsonBoolValue($Object, [string]$Name, [bool]$DefaultValue) {
if ($null -eq $Object -or $null -eq $Object.$Name) {
return $DefaultValue
}
try {
return [bool]$Object.$Name
}
catch {
return $DefaultValue
}
}
function Resolve-LaunchDataDirectoryPath([string]$DataDirectory) {
$expandedPath = [Environment]::ExpandEnvironmentVariables($DataDirectory)
if ([System.IO.Path]::IsPathRooted($expandedPath)) {
return $expandedPath
}
return (Join-Path $PSScriptRoot $expandedPath)
}
function ConvertTo-LaunchDataDirectorySetting([string]$DataDirectoryPath) {
$defaultDataDirectoryPath = Resolve-LaunchDataDirectoryPath -DataDirectory "launchan-data"
try {
$resolvedPath = [System.IO.Path]::GetFullPath($DataDirectoryPath).TrimEnd('\')
$resolvedDefaultPath = [System.IO.Path]::GetFullPath($defaultDataDirectoryPath).TrimEnd('\')
if ([string]::Equals($resolvedPath, $resolvedDefaultPath, [System.StringComparison]::OrdinalIgnoreCase)) {
return "launchan-data"
}
}
catch {
Write-Debug $_
}
return $DataDirectoryPath
}
function Ensure-LaunchDataDirectory($Settings) {
if ($null -eq $Settings -or [string]::IsNullOrWhiteSpace($Settings.DataDirectoryPath)) {
return
}
if (-not (Test-Path -LiteralPath $Settings.DataDirectoryPath -PathType Container)) {
New-Item -ItemType Directory -Path $Settings.DataDirectoryPath -Force | Out-Null
}
}
function Test-IsDefaultLaunchDataDirectory($Settings) {
if ($null -eq $Settings -or [string]::IsNullOrWhiteSpace($Settings.DataDirectoryPath)) {
return $false
}
$defaultDataDirectoryPath = Resolve-LaunchDataDirectoryPath -DataDirectory "launchan-data"
try {
$currentPath = [System.IO.Path]::GetFullPath($Settings.DataDirectoryPath).TrimEnd('\')
$defaultPath = [System.IO.Path]::GetFullPath($defaultDataDirectoryPath).TrimEnd('\')
return [string]::Equals($currentPath, $defaultPath, [System.StringComparison]::OrdinalIgnoreCase)
}
catch {
Write-Debug $_
return $false
}
}
function Test-HasLaunchDataFiles([string]$DataDirectoryPath) {
if ([string]::IsNullOrWhiteSpace($DataDirectoryPath) -or -not (Test-Path -LiteralPath $DataDirectoryPath -PathType Container)) {
return $false
}
$dataFiles = @(
Get-ChildItem -LiteralPath $DataDirectoryPath -File |
Where-Object {
-not $_.BaseName.StartsWith("_") -and
($_.Extension -match '^\.(csv|xlsx|xlsm|xlsb)$')
} |
Select-Object -First 1
)
return ($dataFiles.Count -gt 0)
}
function Get-DefaultSampleCsvContent {
return @"
Title,Command,Params,Keywords
User Profile,%USERPROFILE%,,folder home
Desktop,%USERPROFILE%\Desktop,,folder desktop
Notepad,notepad.exe,,app text
Microsoft,https://www.microsoft.com,,browser web
"@
}
function Ensure-DefaultSampleCsv($Settings) {
if (-not (Test-IsDefaultLaunchDataDirectory -Settings $Settings)) {
return
}
Ensure-LaunchDataDirectory -Settings $Settings
if (Test-HasLaunchDataFiles -DataDirectoryPath $Settings.DataDirectoryPath) {
return
}
$sampleFilePath = Join-Path $Settings.DataDirectoryPath "Sample.csv"
if (Test-Path -LiteralPath $sampleFilePath -PathType Leaf) {
return
}
Set-Content -LiteralPath $sampleFilePath -Value (Get-DefaultSampleCsvContent) -Encoding UTF8
}
function Set-LaunchDataDirectory($Settings, [string]$DataDirectoryPath) {
if ($null -eq $Settings -or [string]::IsNullOrWhiteSpace($DataDirectoryPath)) {
return
}
$selectedPath = [System.IO.Path]::GetFullPath($DataDirectoryPath)
if (-not (Test-Path -LiteralPath $selectedPath -PathType Container)) {
New-Item -ItemType Directory -Path $selectedPath -Force | Out-Null
}
$Settings.DataDirectory = ConvertTo-LaunchDataDirectorySetting -DataDirectoryPath $selectedPath
$Settings.DataDirectoryPath = Resolve-LaunchDataDirectoryPath -DataDirectory $Settings.DataDirectory
Save-LaunchSettings -Settings $Settings
}
function Reset-LaunchDataDirectoryToDefault($Settings) {
Set-LaunchDataDirectory -Settings $Settings -DataDirectoryPath (Resolve-LaunchDataDirectoryPath -DataDirectory "launchan-data")
}
function Get-DefaultSearchPathActions {
return @(
[pscustomobject]@{
appName = "VS Code"
binaryPath = "code"
targetType = "Directory"
}
[pscustomobject]@{
appName = "VS Code"
binaryPath = "code"
targetType = "File"
}
)
}
function Get-SearchPathActionsFromJson($SettingsJson) {
if ($null -eq $SettingsJson -or $null -eq $SettingsJson.searchPathActions) {
return @(Get-DefaultSearchPathActions)
}
return @(
$SettingsJson.searchPathActions |
Where-Object {
-not [string]::IsNullOrWhiteSpace([string]$_.appName) -and
-not [string]::IsNullOrWhiteSpace([string]$_.binaryPath) -and
([string]$_.targetType -in @("Directory", "File"))
} |
ForEach-Object {
[pscustomobject]@{
appName = [string]$_.appName
binaryPath = [string]$_.binaryPath
targetType = [string]$_.targetType
}
}
)
}
function Get-ThemeModeFromJson($SettingsJson, [string]$DefaultValue = "System") {
if ($null -eq $SettingsJson -or $null -eq $SettingsJson.PSObject.Properties["themeMode"]) {
return $DefaultValue
}
$themeMode = [string]$SettingsJson.themeMode
if ($themeMode -in @("System", "Light", "Dark")) {
return $themeMode
}
return $DefaultValue
}
function Get-LaunchSettings {
$configDir = Ensure-LaunchConfigDirectory
$settingsFilePath = Join-Path $configDir "launchan-settings.json"
$stateFilePath = Join-Path $configDir "launchan-state.json"
$settings = [pscustomobject]@{
WindowWidth = 790
WindowHeight = 280
WindowPosition = "BottomLeft"
DataDirectory = "launchan-data"
DataDirectoryPath = Resolve-LaunchDataDirectoryPath -DataDirectory "launchan-data"
SelectAllTextOnFocus = $false
ThemeMode = "System"
SearchPathActions = @(Get-DefaultSearchPathActions)
ConfigDirectoryPath = $configDir
SettingsFilePath = $settingsFilePath
StateFilePath = $stateFilePath
}
if (-not (Test-Path -LiteralPath $settingsFilePath -PathType Leaf)) {
Write-JsonFile -Path $settingsFilePath -Value ([pscustomobject]@{
dataDirectory = $settings.DataDirectory
selectAllTextOnFocus = $settings.SelectAllTextOnFocus
themeMode = $settings.ThemeMode
searchPathActions = $settings.SearchPathActions
})
}
$settingsJson = Read-JsonFile -Path $settingsFilePath
$hasObsoleteLaunchAtLoginSetting = $null -ne $settingsJson -and $null -ne $settingsJson.PSObject.Properties["launchAtLogin"]
if ($null -ne $settingsJson -and -not [string]::IsNullOrWhiteSpace([string]$settingsJson.dataDirectory)) {
$settings.DataDirectory = [string]$settingsJson.dataDirectory
$settings.DataDirectoryPath = Resolve-LaunchDataDirectoryPath -DataDirectory $settings.DataDirectory
}
if ($null -ne $settingsJson) {
$settings.SelectAllTextOnFocus = Get-JsonBoolValue -Object $settingsJson -Name "selectAllTextOnFocus" -DefaultValue $settings.SelectAllTextOnFocus
$settings.ThemeMode = Get-ThemeModeFromJson -SettingsJson $settingsJson -DefaultValue $settings.ThemeMode
$settings.SearchPathActions = @(Get-SearchPathActionsFromJson -SettingsJson $settingsJson)
}
if ($hasObsoleteLaunchAtLoginSetting -or ($null -ne $settingsJson -and ($null -eq $settingsJson.PSObject.Properties["searchPathActions"] -or $null -eq $settingsJson.PSObject.Properties["themeMode"]))) {
Save-LaunchSettings -Settings $settings
}
Ensure-LaunchDataDirectory -Settings $settings
Ensure-DefaultSampleCsv -Settings $settings
return $settings
}
function Save-LaunchSettings($Settings) {
if ($null -eq $Settings) {
return
}
Write-JsonFile -Path $Settings.SettingsFilePath -Value ([pscustomobject]@{
dataDirectory = $Settings.DataDirectory
selectAllTextOnFocus = [bool]$Settings.SelectAllTextOnFocus
themeMode = [string]$Settings.ThemeMode
searchPathActions = @($Settings.SearchPathActions)
})
}
function ConvertFrom-HtmlColor([string]$Color) {
return [System.Drawing.ColorTranslator]::FromHtml($Color)
}
function Test-SystemDarkAppTheme {
try {
$personalizeKeyPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
$personalize = Get-ItemProperty -LiteralPath $personalizeKeyPath -ErrorAction Stop
if ($null -ne $personalize.AppsUseLightTheme) {
return ([int]$personalize.AppsUseLightTheme -eq 0)
}
}
catch {
Write-Debug $_
}
return $false
}
function Resolve-ThemeMode([string]$ThemeMode) {
if ($ThemeMode -eq "Dark") {
return "Dark"
}
if ($ThemeMode -eq "System" -and (Test-SystemDarkAppTheme)) {
return "Dark"
}
return "Light"
}
function Get-LaunchThemePalette([string]$ThemeMode) {
$resolvedThemeMode = Resolve-ThemeMode -ThemeMode $ThemeMode
if ($resolvedThemeMode -eq "Dark") {
return [pscustomobject]@{
Mode = "Dark"
WindowBack = ConvertFrom-HtmlColor "#202124"
ControlBack = ConvertFrom-HtmlColor "#2b2d30"
ControlFore = ConvertFrom-HtmlColor "#f1f3f4"
Border = ConvertFrom-HtmlColor "#5f6368"
GridBack = ConvertFrom-HtmlColor "#202124"
GridAltBack = ConvertFrom-HtmlColor "#26282b"
GridFore = ConvertFrom-HtmlColor "#f1f3f4"
GridHeaderBack = ConvertFrom-HtmlColor "#303134"
GridHeaderFore = ConvertFrom-HtmlColor "#f1f3f4"
GridLine = ConvertFrom-HtmlColor "#3c4043"
GridSelectionBack = ConvertFrom-HtmlColor "#264f78"
GridSelectionFore = ConvertFrom-HtmlColor "#ffffff"
GridHoverBack = ConvertFrom-HtmlColor "#34383d"
MenuBack = ConvertFrom-HtmlColor "#2b2d30"
MenuFore = ConvertFrom-HtmlColor "#f1f3f4"
MenuSelectedBack = ConvertFrom-HtmlColor "#264f78"
}
}
return [pscustomobject]@{
Mode = "Light"
WindowBack = [System.Drawing.SystemColors]::Control
ControlBack = [System.Drawing.SystemColors]::Window
ControlFore = [System.Drawing.SystemColors]::WindowText
Border = [System.Drawing.SystemColors]::ControlDark
GridBack = [System.Drawing.Color]::White
GridAltBack = [System.Drawing.Color]::White
GridFore = [System.Drawing.SystemColors]::ControlText
GridHeaderBack = [System.Drawing.SystemColors]::Control
GridHeaderFore = [System.Drawing.SystemColors]::ControlText
GridLine = [System.Drawing.SystemColors]::ControlLight
GridSelectionBack = [System.Drawing.SystemColors]::Highlight
GridSelectionFore = [System.Drawing.SystemColors]::HighlightText
GridHoverBack = [System.Drawing.Color]::AliceBlue
MenuBack = [System.Drawing.SystemColors]::Menu
MenuFore = [System.Drawing.SystemColors]::MenuText
MenuSelectedBack = [System.Drawing.SystemColors]::Highlight
}
}
function Apply-LaunchTheme($App) {
if ($null -eq $App -or $null -eq $App.Settings) {
return
}
$palette = Get-LaunchThemePalette -ThemeMode $App.Settings.ThemeMode
$App.ThemePalette = $palette
if ($null -ne $App.Form) {
$App.Form.BackColor = $palette.WindowBack
$App.Form.ForeColor = $palette.ControlFore
[LaunchanNativeMethods]::SetImmersiveDarkMode($App.Form.Handle, ($palette.Mode -eq "Dark")) | Out-Null
}
foreach ($button in @($App.SearchPathMenuButton, $App.MenuButton)) {
if ($null -eq $button) {
continue
}
$button.UseVisualStyleBackColor = $false
$button.BackColor = $palette.ControlBack
$button.ForeColor = $palette.ControlFore
$button.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$button.FlatAppearance.BorderColor = $palette.Border
}
if ($null -ne $App.TextBox) {
$App.TextBox.BackColor = $palette.ControlBack
$App.TextBox.ForeColor = $palette.ControlFore
$App.TextBox.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
}
if ($null -ne $App.Grid) {
$grid = $App.Grid
$grid.BackgroundColor = $palette.WindowBack
$grid.GridColor = $palette.GridLine
$grid.DefaultCellStyle.BackColor = $palette.GridBack
$grid.DefaultCellStyle.ForeColor = $palette.GridFore
$grid.DefaultCellStyle.SelectionBackColor = $palette.GridSelectionBack
$grid.DefaultCellStyle.SelectionForeColor = $palette.GridSelectionFore
$grid.AlternatingRowsDefaultCellStyle.BackColor = $palette.GridAltBack
$grid.AlternatingRowsDefaultCellStyle.ForeColor = $palette.GridFore
$grid.ColumnHeadersDefaultCellStyle.BackColor = $palette.GridHeaderBack
$grid.ColumnHeadersDefaultCellStyle.ForeColor = $palette.GridHeaderFore
$grid.ColumnHeadersDefaultCellStyle.SelectionBackColor = $palette.GridHeaderBack
$grid.ColumnHeadersDefaultCellStyle.SelectionForeColor = $palette.GridHeaderFore
$grid.RowHeadersDefaultCellStyle.BackColor = $palette.GridHeaderBack
$grid.RowHeadersDefaultCellStyle.ForeColor = $palette.GridHeaderFore
foreach ($row in $grid.Rows) {
$row.DefaultCellStyle.BackColor = $palette.GridBack
$row.DefaultCellStyle.ForeColor = $palette.GridFore
}
if ($App.LastHoverRowIndex -ge 0 -and $App.LastHoverRowIndex -lt $grid.Rows.Count) {
$grid.Rows[$App.LastHoverRowIndex].DefaultCellStyle.BackColor = $palette.GridHoverBack
}
$grid.Refresh()
}
}
function Apply-ThemeToMenu($Menu, $App) {
if ($null -eq $Menu -or $null -eq $App) {
return
}
$palette = if ($null -ne $App.ThemePalette) { $App.ThemePalette } else { Get-LaunchThemePalette -ThemeMode $App.Settings.ThemeMode }
$Menu.BackColor = $palette.MenuBack
$Menu.ForeColor = $palette.MenuFore
$Menu.RenderMode = [System.Windows.Forms.ToolStripRenderMode]::Professional
$Menu.Renderer = New-Object LaunchanToolStripRenderer -ArgumentList @($palette.MenuBack, $palette.Border, $palette.MenuSelectedBack)
foreach ($item in $Menu.Items) {
Apply-ThemeToMenuItem -Item $item -Palette $palette
}
}
function Apply-ThemeToMenuItem($Item, $Palette) {
if ($null -eq $Item -or $null -eq $Palette) {
return
}
$Item.BackColor = $Palette.MenuBack
$Item.ForeColor = $Palette.MenuFore
if ($Item -is [System.Windows.Forms.ToolStripDropDownItem]) {
$Item.DropDown.BackColor = $Palette.MenuBack
$Item.DropDown.ForeColor = $Palette.MenuFore
$Item.DropDown.RenderMode = [System.Windows.Forms.ToolStripRenderMode]::Professional
$Item.DropDown.Renderer = New-Object LaunchanToolStripRenderer -ArgumentList @($Palette.MenuBack, $Palette.Border, $Palette.MenuSelectedBack)
foreach ($childItem in $Item.DropDownItems) {
Apply-ThemeToMenuItem -Item $childItem -Palette $Palette
}
}
}
function Get-LaunchAtLoginShortcutPath {
$startupDirectoryPath = [Environment]::GetFolderPath([Environment+SpecialFolder]::Startup)
if ([string]::IsNullOrWhiteSpace($startupDirectoryPath)) {
return ""
}
return (Join-Path $startupDirectoryPath "launchan.lnk")
}
function Get-LaunchAtLoginTargetPath {
return (Join-Path $PSScriptRoot "launchan.vbs")
}
function Test-LaunchAtLoginRegistered {
$shortcutPath = Get-LaunchAtLoginShortcutPath
return (-not [string]::IsNullOrWhiteSpace($shortcutPath)) -and (Test-Path -LiteralPath $shortcutPath -PathType Leaf)
}
function Show-LaunchAtLoginInformation($Owner, [string]$Message) {
if ($null -ne $Owner) {
[System.Windows.Forms.MessageBox]::Show(
$Owner,
$Message,
(Get-String -Key "AppTitle"),
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Information
) | Out-Null
return
}
[System.Windows.Forms.MessageBox]::Show(
$Message,
(Get-String -Key "AppTitle"),
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Information
) | Out-Null
}
function Confirm-LaunchAtLoginAction($Owner, [string]$Message) {
$result = if ($null -ne $Owner) {
[System.Windows.Forms.MessageBox]::Show(
$Owner,
$Message,
(Get-String -Key "AppTitle"),
[System.Windows.Forms.MessageBoxButtons]::YesNo,
[System.Windows.Forms.MessageBoxIcon]::Question
)
}
else {
[System.Windows.Forms.MessageBox]::Show(
$Message,
(Get-String -Key "AppTitle"),
[System.Windows.Forms.MessageBoxButtons]::YesNo,
[System.Windows.Forms.MessageBoxIcon]::Question
)
}
return ($result -eq [System.Windows.Forms.DialogResult]::Yes)
}
function New-LaunchAtLoginShortcut([string]$ShortcutPath, [string]$TargetPath) {
$shortcutDirectoryPath = Split-Path -Path $ShortcutPath -Parent
if (-not (Test-Path -LiteralPath $shortcutDirectoryPath -PathType Container)) {
New-Item -ItemType Directory -Path $shortcutDirectoryPath -Force | Out-Null
}
$shell = $null
$shortcut = $null
try {
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($ShortcutPath)
$shortcut.TargetPath = $TargetPath
$shortcut.WorkingDirectory = $PSScriptRoot
$shortcut.Description = "Launch launchan at login"
$shortcut.Save()
}
finally {
Release-ComObject -ComObject $shortcut
Release-ComObject -ComObject $shell
}
}
function Set-LaunchAtLoginRegistration([bool]$Enable, $Owner = $null) {
$shortcutPath = Get-LaunchAtLoginShortcutPath
$targetPath = Get-LaunchAtLoginTargetPath
if ([string]::IsNullOrWhiteSpace($shortcutPath)) {
$failureKey = if ($Enable) { "LaunchAtLoginRegisterFailed" } else { "LaunchAtLoginUnregisterFailed" }
Show-LaunchAtLoginInformation -Owner $Owner -Message (Get-String -Key $failureKey)
return
}
if ($Enable) {
if (Test-Path -LiteralPath $shortcutPath -PathType Leaf) {
Show-LaunchAtLoginInformation -Owner $Owner -Message (Get-String -Key "LaunchAtLoginAlreadyRegistered" -Args @($shortcutPath))
return
}
if (-not (Test-Path -LiteralPath $targetPath -PathType Leaf)) {
Show-LaunchAtLoginInformation -Owner $Owner -Message (Get-String -Key "LaunchAtLoginTargetNotFound" -Args @($targetPath))
return
}
if (-not (Confirm-LaunchAtLoginAction -Owner $Owner -Message (Get-String -Key "ConfirmRegisterLaunchAtLogin" -Args @($shortcutPath, $targetPath)))) {
return
}
try {
New-LaunchAtLoginShortcut -ShortcutPath $shortcutPath -TargetPath $targetPath
}
catch {
Write-AppLog -Lines @(
(Get-String -Key "LaunchAtLoginRegisterFailed"),
$_.Exception.Message
)
Show-LaunchAtLoginInformation -Owner $Owner -Message ((Get-String -Key "LaunchAtLoginRegisterFailed") + [Environment]::NewLine + [Environment]::NewLine + $_.Exception.Message)
}
return
}
if (-not (Test-Path -LiteralPath $shortcutPath -PathType Leaf)) {
Show-LaunchAtLoginInformation -Owner $Owner -Message (Get-String -Key "LaunchAtLoginAlreadyUnregistered" -Args @($shortcutPath))
return
}
if (-not (Confirm-LaunchAtLoginAction -Owner $Owner -Message (Get-String -Key "ConfirmUnregisterLaunchAtLogin" -Args @($shortcutPath)))) {
return
}
try {
Remove-Item -LiteralPath $shortcutPath -Force
}
catch {
Write-AppLog -Lines @(
(Get-String -Key "LaunchAtLoginUnregisterFailed"),
$_.Exception.Message
)
Show-LaunchAtLoginInformation -Owner $Owner -Message ((Get-String -Key "LaunchAtLoginUnregisterFailed") + [Environment]::NewLine + [Environment]::NewLine + $_.Exception.Message)
}
}
function Get-LaunchState([string]$StateFilePath) {
$state = Read-JsonFile -Path $StateFilePath
if ($null -eq $state) {
return [pscustomobject]@{
window = $null
grid = $null
}
}
return $state
}
function Main {
$settings = Get-LaunchSettings
$state = Get-LaunchState -StateFilePath $settings.StateFilePath
$showBuiltIn = Get-JsonBoolValue -Object $state -Name "showBuiltIn" -DefaultValue $true
$table = Get-LaunchTableWithRecovery -Settings $settings -IncludeBuiltIn $showBuiltIn
if ($null -eq $table) {
return
}
if ($null -eq $table.DefaultView) {
Write-Debug "table.DefaultView is null"
return
}
$windowWidth = Get-JsonIntValue -Object $state.window -Name "width" -DefaultValue $settings.WindowWidth
$windowHeight = Get-JsonIntValue -Object $state.window -Name "height" -DefaultValue $settings.WindowHeight
$layout = Get-Size -Width $windowWidth -Height $windowHeight
$app = [pscustomobject]@{
Table = $table
LastHoverRowIndex = -1
Form = $null
TextBox = $null
SearchPathMenuButton = $null
MenuButton = $null
Grid = $null
Settings = $settings
State = $state
ShowBuiltIn = $showBuiltIn
ThemePalette = $null
}
$form = New-MainForm -Layout $layout -WindowPosition $settings.WindowPosition
Restore-WindowPlacement -Form $form -State $state
$textBox = New-SearchTextBox -Layout $layout
$searchPathMenuButton = New-SearchPathMenuButton -Layout $layout
$menuButton = New-AppMenuButton -Layout $layout
$grid = New-ResultGrid -Table $table -Layout $layout
$app.Form = $form
$app.TextBox = $textBox
$app.SearchPathMenuButton = $searchPathMenuButton
$app.MenuButton = $menuButton
$app.Grid = $grid
Apply-LaunchTheme -App $app
$form.Controls.AddRange(@($textBox, $searchPathMenuButton, $menuButton, $grid))
Register-SearchEvents -App $app
Register-SearchPathMenu -Button $searchPathMenuButton -App $app
Register-AppMenu -Button $menuButton -App $app
Register-GridEvents -Form $form -Grid $grid -State $app
Register-ContextMenu -Grid $grid -App $app
Register-FormEvents -Form $form -Grid $grid -App $app
[void]$form.ShowDialog()
Save-LaunchState -App $app
}
function Import-LaunchTable([string]$DataDirectoryName, [bool]$IncludeBuiltIn = $true) {
$csvDir = Resolve-LaunchDataDirectoryPath -DataDirectory $DataDirectoryName
if (-not (Test-Path -LiteralPath $csvDir -PathType Container)) {
throw (Get-String -Key "DataDirectoryNotFound" -Args @($csvDir))
}
$csvFiles = @(
Get-ChildItem -Path $csvDir -Filter *.csv |
Where-Object { -not $_.BaseName.StartsWith("_") }
)
$excelFiles = @(
Get-ChildItem -Path $csvDir -File |
Where-Object { -not $_.BaseName.StartsWith("_") -and $_.Extension -match '^\.(xlsx|xlsm|xlsb)$' }
)
if ($csvFiles.Count -eq 0 -and $excelFiles.Count -eq 0 -and -not $IncludeBuiltIn) {
throw (Get-String -Key "NoCsvFilesFound" -Args @($csvDir))
}
$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) {
Ensure-LaunchTableColumns -Table $table -Columns (Get-LaunchItemColumnNames)
[void]$table.Columns.Add("Source")
$isColumnsAdded = $true
}
foreach ($row in $rows) {
$dataRow = $table.NewRow()
foreach ($columnName in Get-LaunchItemColumnNames) {
if ($null -ne $row.PSObject.Properties[$columnName]) {
$dataRow[$columnName] = $row.$columnName
}
}
$dataRow["Source"] = $csvFile.Name
$table.Rows.Add($dataRow)
}
}
Import-ExcelLaunchFiles -Table $table -ExcelFiles $excelFiles -IsColumnsAdded ([ref]$isColumnsAdded)
if ($IncludeBuiltIn) {
Ensure-LaunchTableColumns -Table $table -Columns (Get-LaunchItemColumnNames)
Ensure-LaunchTableColumn -Table $table -ColumnName "Source"
Add-BuiltInLaunchRows -Table $table
}
if ($table.Rows.Count -eq 0) {
throw (Get-String -Key "NoDataInCsvFiles")
}
return , $table
}
function Get-LaunchTableWithRecovery($Settings, [bool]$IncludeBuiltIn, $Owner = $null) {
if ($null -eq $Settings) {
return $null
}
while ($true) {
try {
$table = Import-LaunchTable -DataDirectoryName $Settings.DataDirectoryPath -IncludeBuiltIn $IncludeBuiltIn
return , $table
}
catch {
$result = Show-DataDirectoryRecoveryDialog -Owner $Owner -Message $_.Exception.Message
switch ($result) {
([System.Windows.Forms.DialogResult]::Yes) {
$selectedPath = Select-LaunchDataDirectoryPath -InitialPath $Settings.DataDirectoryPath -Owner $Owner
if ([string]::IsNullOrWhiteSpace($selectedPath)) {
return $null
}
Set-LaunchDataDirectory -Settings $Settings -DataDirectoryPath $selectedPath
continue
}
([System.Windows.Forms.DialogResult]::No) {
Reset-LaunchDataDirectoryToDefault -Settings $Settings
continue
}
default {
return $null
}
}
}
}
}
function Show-DataDirectoryRecoveryDialog($Owner, [string]$Message) {
$prompt = Get-String -Key "DataDirectoryRecoveryPrompt" -Args @($Message)
if ($null -ne $Owner) {
return [System.Windows.Forms.MessageBox]::Show(
$Owner,
$prompt,
(Get-String -Key "AppTitle"),
[System.Windows.Forms.MessageBoxButtons]::YesNoCancel,
[System.Windows.Forms.MessageBoxIcon]::Error
)
}
return [System.Windows.Forms.MessageBox]::Show(
$prompt,
(Get-String -Key "AppTitle"),
[System.Windows.Forms.MessageBoxButtons]::YesNoCancel,
[System.Windows.Forms.MessageBoxIcon]::Error
)
}
function Get-LaunchItemColumnNames {
return @("Title", "Command", "Params", "Keywords")
}
function Ensure-LaunchTableColumn([System.Data.DataTable]$Table, [string]$ColumnName) {
if ($null -eq $Table -or [string]::IsNullOrWhiteSpace($ColumnName) -or $Table.Columns.Contains($ColumnName)) {
return
}
[void]$Table.Columns.Add($ColumnName)
}
function Ensure-LaunchTableColumns([System.Data.DataTable]$Table, [string[]]$Columns) {
foreach ($column in $Columns) {
Ensure-LaunchTableColumn -Table $Table -ColumnName $column
}
}
function Import-ExcelLaunchFiles([System.Data.DataTable]$Table, [object[]]$ExcelFiles, [ref]$IsColumnsAdded) {
if ($null -eq $ExcelFiles -or $ExcelFiles.Count -eq 0) {
return
}
if ([string]::IsNullOrWhiteSpace((Get-ExcelExecutablePath))) {
Write-AppLog -Lines @(
"Skipped Excel Data files because Excel is not installed:",
($ExcelFiles | ForEach-Object { "- {0}" -f $_.Name })
)
return
}
$excel = $null
$skippedFiles = @()
try {
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $false
$excel.DisplayAlerts = $false
$excel.AskToUpdateLinks = $false
foreach ($excelFile in $ExcelFiles) {
$readableSheetCount = Import-ExcelLaunchFile -Table $Table -Excel $excel -ExcelFile $excelFile -IsColumnsAdded $IsColumnsAdded
if ($readableSheetCount -eq 0) {
$skippedFiles += $excelFile.Name
}
}
}
catch {
Write-AppLog -Lines @(
"Failed to read Excel Data files:",
$_.Exception.Message
)
}
finally {
if ($null -ne $excel) {
try {
$excel.Quit()
}
catch {
Write-Debug $_
}
Release-ComObject -ComObject $excel
}
}
if ($skippedFiles.Count -gt 0) {
$lines = @(
(Get-String -Key "ExcelDataFilesSkipped"),
"",
("{0}:" -f (Get-String -Key "NoReadableExcelSheets"))
) + ($skippedFiles | ForEach-Object { "- $_" })
Write-AppLog -Lines $lines
[System.Windows.Forms.MessageBox]::Show(
($lines -join [Environment]::NewLine),
(Get-String -Key "AppTitle"),
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Warning
) | Out-Null
}
}
function Import-ExcelLaunchFile([System.Data.DataTable]$Table, $Excel, $ExcelFile, [ref]$IsColumnsAdded) {
$workbook = $null
$readableSheetCount = 0
try {
$workbook = $Excel.Workbooks.Open($ExcelFile.FullName, 0, $true)
foreach ($worksheet in $workbook.Worksheets) {
try {
if (Import-ExcelLaunchWorksheet -Table $Table -Worksheet $worksheet -SourceName $ExcelFile.Name -IsColumnsAdded $IsColumnsAdded) {
$readableSheetCount += 1
}
}
finally {
Release-ComObject -ComObject $worksheet
}
}
}
catch {
Write-AppLog -Lines @(
("Failed to read Excel Data file: {0}" -f $ExcelFile.Name),
$_.Exception.Message
)
}
finally {
if ($null -ne $workbook) {
try {
$workbook.Close($false)
}
catch {
Write-Debug $_
}
Release-ComObject -ComObject $workbook
}
}
return $readableSheetCount
}
function Import-ExcelLaunchWorksheet([System.Data.DataTable]$Table, $Worksheet, [string]$SourceName, [ref]$IsColumnsAdded) {
$header = Find-ExcelLaunchHeader -Worksheet $Worksheet
if ($null -eq $header) {
return $false
}
$usedRange = $Worksheet.UsedRange
try {
$lastRow = $usedRange.Row + $usedRange.Rows.Count - 1
}
finally {
Release-ComObject -ComObject $usedRange
}
if (-not $IsColumnsAdded.Value) {
Ensure-LaunchTableColumns -Table $Table -Columns $header.Columns
Ensure-LaunchTableColumn -Table $Table -ColumnName "Source"
$IsColumnsAdded.Value = $true
}
else {
Ensure-LaunchTableColumns -Table $Table -Columns $header.Columns
Ensure-LaunchTableColumn -Table $Table -ColumnName "Source"
}
for ($rowIndex = $header.Row + 1; $rowIndex -le $lastRow; $rowIndex++) {
$values = @{}
$hasRequiredValue = $false
foreach ($columnName in $header.Columns) {
$cellValue = Get-ExcelCellText -Worksheet $Worksheet -Row $rowIndex -Column $header.ColumnMap[$columnName]
$values[$columnName] = $cellValue
if ($columnName -in @("Title", "Command", "Params", "Keywords") -and -not [string]::IsNullOrWhiteSpace($cellValue)) {
$hasRequiredValue = $true
}
}
if (-not $hasRequiredValue) {
continue
}
$dataRow = $Table.NewRow()
foreach ($columnName in $header.Columns) {
$dataRow[$columnName] = $values[$columnName]
}
$dataRow["Source"] = $SourceName
$Table.Rows.Add($dataRow)
}
return $true
}
function Find-ExcelLaunchHeader($Worksheet) {
for ($rowIndex = 1; $rowIndex -le 5; $rowIndex++) {
for ($columnIndex = 1; $columnIndex -le 5; $columnIndex++) {
if ((Get-ExcelCellText -Worksheet $Worksheet -Row $rowIndex -Column $columnIndex) -eq "Title") {
$header = Get-ExcelHeaderFromCell -Worksheet $Worksheet -Row $rowIndex -StartColumn $columnIndex
if ($null -ne $header) {
return $header
}
}
}
}
return $null
}
function Get-ExcelHeaderFromCell($Worksheet, [int]$Row, [int]$StartColumn) {
$requiredColumns = Get-LaunchItemColumnNames
$columnMap = @{}
$columns = @()
$columnIndex = $StartColumn
$maxColumnIndex = $StartColumn + 255
while ($columnIndex -le $maxColumnIndex) {
$columnName = Get-ExcelCellText -Worksheet $Worksheet -Row $Row -Column $columnIndex
if ([string]::IsNullOrWhiteSpace($columnName)) {
break
}
if (-not $columnMap.ContainsKey($columnName)) {
$columnMap[$columnName] = $columnIndex
$columns += $columnName
}
$columnIndex += 1
}
foreach ($requiredColumn in $requiredColumns) {
if (-not $columnMap.ContainsKey($requiredColumn)) {
return $null
}
}
return [pscustomobject]@{
Row = $Row
ColumnMap = $columnMap
Columns = $requiredColumns
}
}
function Get-ExcelCellText($Worksheet, [int]$Row, [int]$Column) {
$cell = $null
try {
$cell = $Worksheet.Cells.Item($Row, $Column)
if ($null -eq $cell -or $null -eq $cell.Text) {
return ""
}
return ([string]$cell.Text).Trim()
}
finally {
Release-ComObject -ComObject $cell
}
}
function Release-ComObject($ComObject) {
if ($null -eq $ComObject) {
return
}
try {
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($ComObject)
}
catch {
Write-Debug $_
}
}
function Add-BuiltInLaunchRows([System.Data.DataTable]$Table) {
if ($null -eq $Table) {
return
}
foreach ($row in Get-BuiltInLaunchRows) {
$dataRow = $Table.NewRow()
foreach ($column in $Table.Columns) {
$columnName = $column.ColumnName
if ($columnName -eq "Source") {
$dataRow[$columnName] = "(Built-in)"
continue
}
if ($null -ne $row.PSObject.Properties[$columnName]) {
$dataRow[$columnName] = $row.$columnName
}
}
$Table.Rows.Add($dataRow)
}
}
function Get-BuiltInLaunchRows {
@"
Title,Command,Params,Keywords
Google (Default Browser),https://www.google.com/,,web
Google (Edge),msedge,https://www.google.com/,web
Google (Chrome),chrome,https://www.google.com/,web
Environment Variable > Home,%USERPROFILE%,,
Environment Variable > LocalAppData,%LOCALAPPDATA%,,
"VSCode (""code"")",code,,
VSCode (exe directly),%LOCALAPPDATA%\Programs\Microsoft VS Code\Code.exe,,
"""Send To"" Menu",shell:sendto,,"SendTo,Context Menu"
Startup,shell:startup,,start up
ODBC Data Source (32bit),%windir%\syswow64\odbcad32.exe,,
ODBC Data Source (64bit),%windir%\system32\odbcad32.exe,,
Edit /etc/hosts,notepad,%windir%\system32\drivers\etc\hosts,
"@ | ConvertFrom-Csv
}
function Get-Size([int]$Width, [int]$Height) {
$leftPadding = 10
$rightPadding = 30
$controlGap = 10
$topPadding = 10
$menuButtonWidth = 28
$menuButtonHeight = 20
$pathButtonWidth = 28
$pathButtonHeight = 20
$inputHeight = 20
$clientWidth = $Width
$menuButtonX = $clientWidth - $rightPadding - $menuButtonWidth
$pathButtonX = $menuButtonX - $controlGap - $pathButtonWidth
return @{
FormSize = [System.Drawing.Size]::new($Width, $Height)
TextBoxSize = [System.Drawing.Size]::new(($pathButtonX - $controlGap - $leftPadding), $inputHeight)
SearchPathMenuButtonSize = [System.Drawing.Size]::new($pathButtonWidth, $pathButtonHeight)
MenuButtonSize = [System.Drawing.Size]::new($menuButtonWidth, $menuButtonHeight)
GridSize = [System.Drawing.Size]::new($Width - 40, $Height - 90)
TextBoxLocation = [System.Drawing.Point]::new($leftPadding, $topPadding)
SearchPathMenuButtonLocation = [System.Drawing.Point]::new($pathButtonX, $topPadding)
MenuButtonLocation = [System.Drawing.Point]::new($menuButtonX, $topPadding)
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.Size = $Layout.TextBoxSize
$textBox.Anchor = "Top, Left, Right"
$textBox.ShortcutsEnabled = $true
return $textBox
}
function New-SearchPathMenuButton($Layout) {
$button = New-Object System.Windows.Forms.Button
$button.Text = ">"
$button.Location = $Layout.SearchPathMenuButtonLocation
$button.Size = $Layout.SearchPathMenuButtonSize
$button.Anchor = "Top, Right"
$button.TabStop = $false
$button.Visible = $false
return $button
}
function New-AppMenuButton($Layout) {
$button = New-Object System.Windows.Forms.Button
$button.Text = "..."
$button.Location = $Layout.MenuButtonLocation
$button.Size = $Layout.MenuButtonSize
$button.Anchor = "Top, Right"
$button.TabStop = $false
# $button.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
# $button.Padding = [System.Windows.Forms.Padding]::new(0)
# $button.Font = [System.Drawing.Font]::new($button.Font.FontFamily, 12)
# $button.Font = [System.Drawing.Font]::new("Consolas", 8)
return $button
}
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.MultiSelect = $false
$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
$Grid = $App.Grid
$resetGridHoverState = ${function:Reset-GridHoverState}
$setLaunchFilter = ${function:Set-LaunchFilter}
$invokeLaunchItem = ${function:Invoke-LaunchItem}
$moveGridSelectionFromSearchBox = ${function:Move-GridSelectionFromSearchBox}
$clearTextBoxSelectionToCaret = ${function:Clear-TextBoxSelectionToCaret}
$selectAllSearchTextIfNeeded = ${function:Select-AllSearchTextIfNeeded}
$updateSearchPathMenuButton = ${function:Update-SearchPathMenuButton}
$textChangedHandler = {
& $resetGridHoverState -Grid $Grid -State $App
& $setLaunchFilter -Table $App.Table -Query $TextBox.Text
& $updateSearchPathMenuButton -App $App
}.GetNewClosure()
$TextBox.Add_TextChanged($textChangedHandler)
& $updateSearchPathMenuButton -App $App
$enterHandler = {
& $selectAllSearchTextIfNeeded -App $App
}.GetNewClosure()
$TextBox.Add_Enter($enterHandler)
$keyDownHandler = {
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) {
& $invokeLaunchItem -Row $Grid.SelectedRows[0]
$e.SuppressKeyPress = $true
$e.Handled = $true
}
return
}
if ($e.Control -and $e.KeyCode -eq [System.Windows.Forms.Keys]::P) {
& $moveGridSelectionFromSearchBox -Grid $Grid -KeyCode ([System.Windows.Forms.Keys]::Up)
$e.SuppressKeyPress = $true
$e.Handled = $true
return
}
if ($e.Control -and $e.KeyCode -eq [System.Windows.Forms.Keys]::N) {
& $moveGridSelectionFromSearchBox -Grid $Grid -KeyCode ([System.Windows.Forms.Keys]::Down)
$e.SuppressKeyPress = $true
$e.Handled = $true
return
}
if ($e.Control -and $e.KeyCode -eq [System.Windows.Forms.Keys]::Up) {
& $moveGridSelectionFromSearchBox -Grid $Grid -KeyCode ([System.Windows.Forms.Keys]::Home)
$e.SuppressKeyPress = $true
$e.Handled = $true
return
}
if ($e.Control -and $e.KeyCode -eq [System.Windows.Forms.Keys]::Down) {
& $moveGridSelectionFromSearchBox -Grid $Grid -KeyCode ([System.Windows.Forms.Keys]::End)
$e.SuppressKeyPress = $true
$e.Handled = $true
return
}
if ($e.Alt -and $e.KeyCode -eq [System.Windows.Forms.Keys]::Up) {
& $moveGridSelectionFromSearchBox -Grid $Grid -KeyCode ([System.Windows.Forms.Keys]::Home)
$e.SuppressKeyPress = $true
$e.Handled = $true
return
}
if ($e.Alt -and $e.KeyCode -eq [System.Windows.Forms.Keys]::Down) {
& $moveGridSelectionFromSearchBox -Grid $Grid -KeyCode ([System.Windows.Forms.Keys]::End)
$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
)) {
& $moveGridSelectionFromSearchBox -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
)) {
& $clearTextBoxSelectionToCaret -TextBox $TextBox
}
}.GetNewClosure()
$TextBox.Add_KeyDown($keyDownHandler)
}
function Select-AllSearchTextIfNeeded($App) {
if ($null -eq $App -or $null -eq $App.TextBox -or -not [bool]$App.Settings.SelectAllTextOnFocus) {
return
}
$textBox = $App.TextBox
$selectAllAction = {
if ($textBox.Focused -and [bool]$App.Settings.SelectAllTextOnFocus) {
$textBox.SelectAll()
}
}.GetNewClosure()
[void]$textBox.BeginInvoke([System.Action]$selectAllAction)
}
function Update-SearchPathMenuButton($App) {
if ($null -eq $App -or $null -eq $App.SearchPathMenuButton -or $null -eq $App.TextBox) {
return
}
$showSearchPathMenuButton = Test-LooksLikeSearchPath -Text $App.TextBox.Text
$App.SearchPathMenuButton.Visible = $showSearchPathMenuButton
$rightEdge = if ($showSearchPathMenuButton) {
$App.SearchPathMenuButton.Left
}
elseif ($null -ne $App.MenuButton) {
$App.MenuButton.Left
}
else {
$App.TextBox.Right
}
$newWidth = $rightEdge - $App.TextBox.Left - 10
if ($newWidth -gt 20 -and $App.TextBox.Width -ne $newWidth) {
$App.TextBox.Width = $newWidth
}
}
function Register-SearchPathMenu($Button, $App) {
if ($null -eq $Button -or $null -eq $App) {
return
}
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$menu.ShowImageMargin = $false
$showSearchPathMenu = ${function:Show-SearchPathMenu}
Apply-ThemeToMenu -Menu $menu -App $App
$mouseDownHandler = {
param($s, $e)
if ($e.Button -ne [System.Windows.Forms.MouseButtons]::Left) {
return
}
& $showSearchPathMenu -Menu $menu -Button $Button -App $App
}.GetNewClosure()
$Button.Add_MouseDown($mouseDownHandler)
}
function Register-AppMenu($Button, $App) {
if ($null -eq $Button -or $null -eq $App) {
return
}
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$menu.ShowImageMargin = $true
$configDirectoryPath = Get-LaunchConfigDirectoryPath
$reloadLaunchData = ${function:Reload-LaunchData}
$saveLaunchSettings = ${function:Save-LaunchSettings}
$applyLaunchTheme = ${function:Apply-LaunchTheme}
$applyThemeToMenu = ${function:Apply-ThemeToMenu}
$changeLaunchDataDirectory = ${function:Change-LaunchDataDirectory}
$getConfigFolderPath = ${function:Get-ConfigFolderPath}
$testLaunchAtLoginRegistered = ${function:Test-LaunchAtLoginRegistered}
$setLaunchAtLoginRegistration = ${function:Set-LaunchAtLoginRegistration}
$selectAllTextOnFocusItem = New-Object System.Windows.Forms.ToolStripMenuItem
$selectAllTextOnFocusItem.Text = Get-String -Key "SelectAllTextOnFocus"
$selectAllTextOnFocusItem.CheckOnClick = $true
$selectAllTextOnFocusItem.Checked = [bool]$App.Settings.SelectAllTextOnFocus
$selectAllTextOnFocusItem.Add_Click({
$App.Settings.SelectAllTextOnFocus = [bool]$selectAllTextOnFocusItem.Checked
& $saveLaunchSettings -Settings $App.Settings
}.GetNewClosure())
$menu.Items.Add($selectAllTextOnFocusItem) | Out-Null
$showBuiltInItem = New-Object System.Windows.Forms.ToolStripMenuItem
$showBuiltInItem.Text = Get-String -Key "ShowBuiltIn"
$showBuiltInItem.CheckOnClick = $true
$showBuiltInItem.Checked = [bool]$App.ShowBuiltIn
$showBuiltInItem.Add_Click({
$App.ShowBuiltIn = [bool]$showBuiltInItem.Checked
& $reloadLaunchData -App $App
}.GetNewClosure())
$menu.Items.Add($showBuiltInItem) | Out-Null
$themeMenuItem = New-Object System.Windows.Forms.ToolStripMenuItem
$themeMenuItem.Text = Get-String -Key "ThemeMenu"
$themeItems = @{}
foreach ($themeMode in @("System", "Light", "Dark")) {
$themeItem = New-Object System.Windows.Forms.ToolStripMenuItem
$themeItem.Text = Get-String -Key ("Theme{0}" -f $themeMode)
$themeItem.Tag = $themeMode
$themeItem.CheckOnClick = $false
$themeItem.Add_Click({
param($s, $e)
$App.Settings.ThemeMode = [string]$s.Tag
& $saveLaunchSettings -Settings $App.Settings
& $applyLaunchTheme -App $App
}.GetNewClosure())
$themeItems[$themeMode] = $themeItem
$themeMenuItem.DropDownItems.Add($themeItem) | Out-Null
}
$menu.Items.Add($themeMenuItem) | Out-Null
$openConfigLogDirectoryItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openConfigLogDirectoryItem.Text = Get-String -Key "OpenConfigLogFolder"
$openConfigLogDirectoryItem.Enabled = -not [string]::IsNullOrWhiteSpace($configDirectoryPath)
$openConfigLogDirectoryItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($configDirectoryPath)) {
Start-Process -FilePath "explorer.exe" -ArgumentList @($configDirectoryPath)
}
}.GetNewClosure())
$menu.Items.Add($openConfigLogDirectoryItem) | Out-Null
$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
$launchAtLoginItem = New-Object System.Windows.Forms.ToolStripMenuItem
$launchAtLoginItem.Text = Get-String -Key "LaunchAtLogin"
$launchAtLoginItem.CheckOnClick = $false
$launchAtLoginItem.Checked = & $testLaunchAtLoginRegistered
$launchAtLoginItem.Add_Click({
$enable = -not (& $testLaunchAtLoginRegistered)
& $setLaunchAtLoginRegistration -Enable $enable -Owner $App.Form
$launchAtLoginItem.Checked = & $testLaunchAtLoginRegistered
}.GetNewClosure())
$menu.Items.Add($launchAtLoginItem) | Out-Null
$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
$openDataDirectoryItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openDataDirectoryItem.Text = Get-String -Key "OpenConfigFolder"
$openDataDirectoryItem.Enabled = $false
$openDataDirectoryItem.Add_Click({
$currentDataDirectoryPath = & $getConfigFolderPath -DataDirectoryName $App.Settings.DataDirectoryPath
if (-not [string]::IsNullOrWhiteSpace($currentDataDirectoryPath)) {
Start-Process -FilePath "explorer.exe" -ArgumentList @($currentDataDirectoryPath)
}
}.GetNewClosure())
$menu.Items.Add($openDataDirectoryItem) | Out-Null
$changeDataDirectoryItem = New-Object System.Windows.Forms.ToolStripMenuItem
$changeDataDirectoryItem.Text = Get-String -Key "ChangeDataDirectory"
$changeDataDirectoryItem.Add_Click({
& $changeLaunchDataDirectory -App $App
}.GetNewClosure())
$menu.Items.Add($changeDataDirectoryItem) | Out-Null
$mouseDownHandler = {
param($s, $e)
if ($e.Button -ne [System.Windows.Forms.MouseButtons]::Left) {
return
}
$selectAllTextOnFocusItem.Checked = [bool]$App.Settings.SelectAllTextOnFocus
$showBuiltInItem.Checked = [bool]$App.ShowBuiltIn
foreach ($themeMode in @("System", "Light", "Dark")) {
$themeItems[$themeMode].Checked = ([string]$App.Settings.ThemeMode -eq $themeMode)
}
$launchAtLoginItem.Checked = & $testLaunchAtLoginRegistered
$currentDataDirectoryPath = & $getConfigFolderPath -DataDirectoryName $App.Settings.DataDirectoryPath
$openDataDirectoryItem.Enabled = -not [string]::IsNullOrWhiteSpace($currentDataDirectoryPath)
& $applyThemeToMenu -Menu $menu -App $App
$menu.Show($Button, [System.Drawing.Point]::new(0, $Button.Height))
}.GetNewClosure()
$Button.Add_MouseDown($mouseDownHandler)
}
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
& $setCellToolTipIfNeeded -Grid $Grid -RowIndex $e.RowIndex -ColumnIndex $e.ColumnIndex
}
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)
$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}
$showTableContextMenu = ${function:Show-TableContextMenu}
Apply-ThemeToMenu -Menu $menu -App $App
$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) {
$Grid.ClearSelection()
& $showTableContextMenu -Menu $menu -Grid $Grid -App $App -Location $e.Location
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, $App) {
$setGridColumns = ${function:Set-GridColumns}
$selectAllSearchTextIfNeeded = ${function:Select-AllSearchTextIfNeeded}
$applyLaunchTheme = ${function:Apply-LaunchTheme}
$shownHandler = {
try {
& $setGridColumns -Grid $Grid -State $App.State
}
catch {
Write-Debug $_
}
}.GetNewClosure()
$Form.Add_Shown($shownHandler)
$activatedHandler = {
if ($null -ne $App.Settings -and [string]$App.Settings.ThemeMode -eq "System") {
& $applyLaunchTheme -App $App
}
if ($null -ne $App.TextBox -and $Form.ActiveControl -eq $App.TextBox) {
& $selectAllSearchTextIfNeeded -App $App
}
}.GetNewClosure()
$Form.Add_Activated($activatedHandler)
}
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 Reset-GridHoverState($Grid, $State) {
if ($null -eq $State) {
return
}
$lastRowIndex = $State.LastHoverRowIndex
$State.LastHoverRowIndex = -1
if ($null -eq $Grid -or $lastRowIndex -lt 0 -or $lastRowIndex -ge $Grid.Rows.Count) {
return
}
$backColor = if ($null -ne $State.ThemePalette) { $State.ThemePalette.GridBack } else { [System.Drawing.Color]::White }
$Grid.Rows[$lastRowIndex].DefaultCellStyle.BackColor = $backColor
}
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", "Keywords")
$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) {
if ($null -eq $Grid -or $null -eq $State -or $RowIndex -lt 0 -or $RowIndex -ge $Grid.Rows.Count) {
return
}
$lastRowIndex = $State.LastHoverRowIndex
if ($lastRowIndex -ge 0 -and $lastRowIndex -lt $Grid.Rows.Count -and $lastRowIndex -ne $RowIndex) {
$backColor = if ($null -ne $State.ThemePalette) { $State.ThemePalette.GridBack } else { [System.Drawing.Color]::White }
$Grid.Rows[$lastRowIndex].DefaultCellStyle.BackColor = $backColor
}
$hoverBackColor = if ($null -ne $State.ThemePalette) { $State.ThemePalette.GridHoverBack } else { [System.Drawing.Color]::AliceBlue }
$Grid.Rows[$RowIndex].DefaultCellStyle.BackColor = $hoverBackColor
$State.LastHoverRowIndex = $RowIndex
}
function Clear-GridHoverRow($Grid, $State) {
if ($null -eq $Grid -or $null -eq $State) {
return
}
$lastRowIndex = $State.LastHoverRowIndex
if ($lastRowIndex -ge 0 -and $lastRowIndex -lt $Grid.Rows.Count) {
$backColor = if ($null -ne $State.ThemePalette) { $State.ThemePalette.GridBack } else { [System.Drawing.Color]::White }
$Grid.Rows[$lastRowIndex].DefaultCellStyle.BackColor = $backColor
}
$State.LastHoverRowIndex = -1
}
function Reload-LaunchData($App) {
if ($null -eq $App) {
return
}
$table = Get-LaunchTableWithRecovery -Settings $App.Settings -IncludeBuiltIn $App.ShowBuiltIn -Owner $App.Form
if ($null -eq $table -or $null -eq $table.DefaultView) {
return
}
$currentQuery = [string]$App.TextBox.Text
$App.Table = $table
$App.LastHoverRowIndex = -1
$App.Grid.DataSource = $table.DefaultView
Set-LaunchFilter -Table $table -Query $currentQuery
Set-GridColumns -Grid $App.Grid -State $App.State
Apply-LaunchTheme -App $App
}
function Select-LaunchDataDirectoryPath([string]$InitialPath, $Owner = $null) {
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
try {
$dialog.Description = Get-String -Key "ChooseDataDirectoryDescription"
$dialog.ShowNewFolderButton = $true
if (-not [string]::IsNullOrWhiteSpace($InitialPath) -and (Test-Path -LiteralPath $InitialPath -PathType Container)) {
$dialog.SelectedPath = $InitialPath
}
$result = if ($null -ne $Owner) {
$dialog.ShowDialog($Owner)
}
else {
$dialog.ShowDialog()
}
if ($result -ne [System.Windows.Forms.DialogResult]::OK -or [string]::IsNullOrWhiteSpace($dialog.SelectedPath)) {
return ""
}
return [System.IO.Path]::GetFullPath($dialog.SelectedPath)
}
finally {
if ($null -ne $dialog) {
$dialog.Dispose()
}
}
}
function Change-LaunchDataDirectory($App) {
if ($null -eq $App -or $null -eq $App.Settings) {
return
}
$selectedPath = Select-LaunchDataDirectoryPath -InitialPath $App.Settings.DataDirectoryPath -Owner $App.Form
if ([string]::IsNullOrWhiteSpace($selectedPath)) {
return
}
$currentPath = [System.IO.Path]::GetFullPath($App.Settings.DataDirectoryPath)
if ([string]::Equals($selectedPath.TrimEnd('\'), $currentPath.TrimEnd('\'), [System.StringComparison]::OrdinalIgnoreCase)) {
return
}
Set-LaunchDataDirectory -Settings $App.Settings -DataDirectoryPath $selectedPath
Reload-LaunchData -App $App
}
function Get-SearchPathInfo([string]$Text) {
$path = Resolve-SearchInputPath -Text $Text
if ([string]::IsNullOrWhiteSpace($path)) {
return $null
}
$isDirectory = [System.IO.Directory]::Exists($path)
$isFile = [System.IO.File]::Exists($path)
if (-not $isDirectory -and -not $isFile) {
return $null
}
$directoryPath = if ($isDirectory) { $path } else { Split-Path -Path $path -Parent }
$filename = Split-Path -Path $path -Leaf
return [pscustomobject]@{
Path = $path
IsDirectory = $isDirectory
IsFile = $isFile
DirectoryPath = $directoryPath
Filename = $filename
}
}
function Test-LooksLikeSearchPath([string]$Text) {
if ([string]::IsNullOrWhiteSpace($Text)) {
return $false
}
$value = $Text.Trim().Trim('"', "'")
if ([string]::IsNullOrWhiteSpace($value)) {
return $false
}
if ($value -match '^[A-Za-z]:[\\/]' -or $value -match '^\\\\[^\\]+\\[^\\]+' -or $value -match '^%[^%]+%[\\/]' -or $value -match '^~([\\/]|$)' -or $value -match '^\.{1,2}[\\/]' -or $value -match '[\\/]') {
return $true
}
return $false
}
function Resolve-SearchInputPath([string]$Text) {
if ([string]::IsNullOrWhiteSpace($Text)) {
return ""
}
$path = $Text.Trim().Trim('"', "'")
if ($path.StartsWith("~")) {
$homePath = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)
if ($path -eq "~") {
$path = $homePath
}
elseif ($path.StartsWith("~\") -or $path.StartsWith("~/")) {
$path = Join-Path $homePath $path.Substring(2)
}
}
return [Environment]::ExpandEnvironmentVariables($path)
}
function Show-SearchPathMenu($Menu, $Button, $App) {
if ($null -eq $Menu -or $null -eq $Button -or $null -eq $App -or $null -eq $App.TextBox) {
return
}
$pathInfo = Get-SearchPathInfo -Text $App.TextBox.Text
if ($null -eq $pathInfo) {
$Button.Visible = $false
return
}
$Menu.Items.Clear()
$openMenuItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openMenuItem.Text = Get-String -Key "OpenMenu"
$openAssociatedItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openAssociatedItem.Text = Get-String -Key "OpenAssociatedApp"
$openAssociatedItem.Enabled = [bool]$pathInfo.IsFile
$openAssociatedItem.Add_Click({
if ($pathInfo.IsFile) {
Start-Process -FilePath $pathInfo.Path
}
}.GetNewClosure())
$openMenuItem.DropDownItems.Add($openAssociatedItem) | Out-Null
$openMenuItem.DropDownItems.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
$openDirectoryExplorerItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openDirectoryExplorerItem.Text = Get-String -Key "OpenDirectoryExplorer"
$openDirectoryExplorerItem.Enabled = -not [string]::IsNullOrWhiteSpace($pathInfo.DirectoryPath)
$openDirectoryExplorerItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($pathInfo.DirectoryPath)) {
Start-Process -FilePath "explorer.exe" -ArgumentList @($pathInfo.DirectoryPath)
}
}.GetNewClosure())
$openMenuItem.DropDownItems.Add($openDirectoryExplorerItem) | Out-Null
$openDirectoryCommandPromptItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openDirectoryCommandPromptItem.Text = Get-String -Key "OpenDirectoryCommandPrompt"
$openDirectoryCommandPromptItem.Enabled = -not [string]::IsNullOrWhiteSpace($pathInfo.DirectoryPath)
$openDirectoryCommandPromptItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($pathInfo.DirectoryPath)) {
Start-Process -FilePath "cmd.exe" -ArgumentList @("/K", "cd /d `"$($pathInfo.DirectoryPath)`"")
}
}.GetNewClosure())
$openMenuItem.DropDownItems.Add($openDirectoryCommandPromptItem) | Out-Null
$openDirectoryPowerShellPromptItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openDirectoryPowerShellPromptItem.Text = Get-String -Key "OpenDirectoryPowerShellPrompt"
$openDirectoryPowerShellPromptItem.Enabled = -not [string]::IsNullOrWhiteSpace($pathInfo.DirectoryPath)
$openDirectoryPowerShellPromptItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($pathInfo.DirectoryPath)) {
Start-Process -FilePath "powershell.exe" -WorkingDirectory $pathInfo.DirectoryPath
}
}.GetNewClosure())
$openMenuItem.DropDownItems.Add($openDirectoryPowerShellPromptItem) | Out-Null
foreach ($action in @($App.Settings.SearchPathActions)) {
$targetPath = Get-SearchPathActionTargetPath -PathInfo $pathInfo -Action $action
$executablePath = Resolve-SearchPathActionExecutable -BinaryPath $action.binaryPath
$actionTargetPath = $targetPath
$actionExecutablePath = $executablePath
$actionItem = New-Object System.Windows.Forms.ToolStripMenuItem
$actionItem.Text = if ($action.targetType -eq "File") {
Get-String -Key "OpenFileWithApp" -Args @($action.appName)
}
else {
Get-String -Key "OpenDirectoryWithApp" -Args @($action.appName)
}
$actionItem.Enabled = (-not [string]::IsNullOrWhiteSpace($actionTargetPath)) -and (-not [string]::IsNullOrWhiteSpace($actionExecutablePath))
$actionItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($actionTargetPath) -and -not [string]::IsNullOrWhiteSpace($actionExecutablePath)) {
Start-Process -FilePath $actionExecutablePath -ArgumentList @($actionTargetPath)
}
}.GetNewClosure())
$openMenuItem.DropDownItems.Add($actionItem) | Out-Null
}
$Menu.Items.Add($openMenuItem) | Out-Null
$excelMenuItem = New-Object System.Windows.Forms.ToolStripMenuItem
$excelMenuItem.Text = Get-String -Key "ExcelMenu"
$excelExecutablePath = Get-ExcelExecutablePath
$canOpenWithExcel = $pathInfo.IsFile -and (Test-IsExcelFilePath -Path $pathInfo.Path) -and -not [string]::IsNullOrWhiteSpace($excelExecutablePath)
Add-SearchPathExcelMenuItem -MenuItem $excelMenuItem -Text (Get-String -Key "OpenReadOnlyExcel") -Enabled $canOpenWithExcel -ExcelExecutablePath $excelExecutablePath -Arguments @("/r", $pathInfo.Path)
Add-SearchPathExcelMenuItem -MenuItem $excelMenuItem -Text (Get-String -Key "OpenSeparateExcel") -Enabled $canOpenWithExcel -ExcelExecutablePath $excelExecutablePath -Arguments @("/x", $pathInfo.Path)
Add-SearchPathExcelMenuItem -MenuItem $excelMenuItem -Text (Get-String -Key "OpenSeparateReadOnlyExcel") -Enabled $canOpenWithExcel -ExcelExecutablePath $excelExecutablePath -Arguments @("/x", "/r", $pathInfo.Path)
$Menu.Items.Add($excelMenuItem) | Out-Null
$copyMenuItem = New-Object System.Windows.Forms.ToolStripMenuItem
$copyMenuItem.Text = Get-String -Key "CopyToClipboard"
$copyDirectoryItem = New-Object System.Windows.Forms.ToolStripMenuItem
$copyDirectoryItem.Text = Get-String -Key "CopyDirectory"
$copyDirectoryItem.Enabled = -not [string]::IsNullOrWhiteSpace($pathInfo.DirectoryPath)
$copyDirectoryItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($pathInfo.DirectoryPath)) {
[System.Windows.Forms.Clipboard]::SetText($pathInfo.DirectoryPath)
}
}.GetNewClosure())
$copyMenuItem.DropDownItems.Add($copyDirectoryItem) | Out-Null
$copyFilenameItem = New-Object System.Windows.Forms.ToolStripMenuItem
$copyFilenameItem.Text = Get-String -Key "CopyFilename"
$copyFilenameItem.Enabled = -not [string]::IsNullOrWhiteSpace($pathInfo.Filename)
$copyFilenameItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($pathInfo.Filename)) {
[System.Windows.Forms.Clipboard]::SetText($pathInfo.Filename)
}
}.GetNewClosure())
$copyMenuItem.DropDownItems.Add($copyFilenameItem) | Out-Null
$Menu.Items.Add($copyMenuItem) | Out-Null
Apply-ThemeToMenu -Menu $Menu -App $App
$Menu.Show($Button, [System.Drawing.Point]::new(0, $Button.Height))
}
function Add-SearchPathExcelMenuItem($MenuItem, [string]$Text, [bool]$Enabled, [string]$ExcelExecutablePath, [string[]]$Arguments) {
$item = New-Object System.Windows.Forms.ToolStripMenuItem
$item.Text = $Text
$item.Enabled = $Enabled
$item.Add_Click({
if ($Enabled) {
Start-Process -FilePath $ExcelExecutablePath -ArgumentList $Arguments
}
}.GetNewClosure())
$MenuItem.DropDownItems.Add($item) | Out-Null
}
function Get-SearchPathActionTargetPath($PathInfo, $Action) {
if ($null -eq $PathInfo -or $null -eq $Action) {
return ""
}
if ([string]$Action.targetType -eq "File") {
if ($PathInfo.IsFile) {
return $PathInfo.Path
}
return ""
}
return $PathInfo.DirectoryPath
}
function Resolve-SearchPathActionExecutable([string]$BinaryPath) {
if ([string]::IsNullOrWhiteSpace($BinaryPath)) {
return ""
}
$expandedPath = [Environment]::ExpandEnvironmentVariables($BinaryPath)
if ([System.IO.Path]::IsPathRooted($expandedPath) -or $expandedPath.Contains("\") -or $expandedPath.Contains("/")) {
if (Test-Path -LiteralPath $expandedPath -PathType Leaf) {
return $expandedPath
}
return ""
}
$command = Get-Command $expandedPath -ErrorAction SilentlyContinue | Select-Object -First 1
if ($null -ne $command -and -not [string]::IsNullOrWhiteSpace($command.Source)) {
return $command.Source
}
return ""
}
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-TableContextMenu($Menu, $Grid, $App, $Location) {
if ($null -eq $Menu -or $null -eq $Grid -or $null -eq $App) {
return
}
$reloadLaunchData = ${function:Reload-LaunchData}
$dataDirectoryPath = Get-ConfigFolderPath -DataDirectoryName $App.Settings.DataDirectoryPath
$Menu.Items.Clear()
$reloadDataDirectoryItem = New-Object System.Windows.Forms.ToolStripMenuItem
$reloadDataDirectoryItem.Text = Get-String -Key "ReloadConfig"
$reloadDataDirectoryItem.Add_Click({
& $reloadLaunchData -App $App
}.GetNewClosure())
$Menu.Items.Add($reloadDataDirectoryItem) | Out-Null
$openDataDirectoryItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openDataDirectoryItem.Text = Get-String -Key "OpenConfigFolder"
$openDataDirectoryItem.Enabled = -not [string]::IsNullOrWhiteSpace($dataDirectoryPath)
$openDataDirectoryItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($dataDirectoryPath)) {
Start-Process -FilePath "explorer.exe" -ArgumentList @($dataDirectoryPath)
}
}.GetNewClosure())
$Menu.Items.Add($openDataDirectoryItem) | Out-Null
Apply-ThemeToMenu -Menu $Menu -App $App
$Menu.Show($Grid, $Location)
}
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]
$clickedColumnName = if ($ColumnIndex -ge 0) { $Grid.Columns[$ColumnIndex].Name } else { "" }
$commandPath = Get-ExpandedRowValue -Row $row -ColumnName "Command"
$sourceFilePath = Get-SourceFilePath -Row $row -DataDirectoryName $App.Settings.DataDirectoryPath
$sourceName = Get-GridCellText -Grid $Grid -RowIndex $RowIndex -ColumnIndex $Grid.Columns["Source"].Index
$configFolderPath = Get-ConfigFolderPath -DataDirectoryName $App.Settings.DataDirectoryPath
$cellText = Get-GridCellText -Grid $Grid -RowIndex $RowIndex -ColumnIndex $ColumnIndex
$launchCommandText = Get-LaunchCommandText -Row $row
$rowText = Get-GridRowText -Row $row
$parentFolderPath = Get-PreferredParentFolderPath -Row $row -ClickedColumnName $clickedColumnName
$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
$copyRowItem = New-Object System.Windows.Forms.ToolStripMenuItem
$copyRowItem.Text = Get-String -Key "CopyRow"
$copyRowItem.Enabled = -not [string]::IsNullOrWhiteSpace($rowText)
$copyRowItem.Add_Click({
if (-not [string]::IsNullOrWhiteSpace($rowText)) {
[System.Windows.Forms.Clipboard]::SetText($rowText)
}
}.GetNewClosure())
$copyMenuItem.DropDownItems.Add($copyRowItem) | 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
$Menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
$openSourceItem = New-Object System.Windows.Forms.ToolStripMenuItem
$openSourceItem.Text = Get-String -Key "OpenSourceCsv" -Args @($sourceName)
$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
Apply-ThemeToMenu -Menu $Menu -App $App
$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-GridRowText($Row) {
if ($null -eq $Row) {
return ""
}
$visibleCells = @(
$Row.Cells |
Where-Object { $_.Visible } |
Sort-Object { $_.OwningColumn.DisplayIndex }
)
return (($visibleCells | ForEach-Object {
if ($null -eq $_.Value) {
""
}
else {
[string]$_.Value
}
}) -join "`t")
}
function Get-SourceFilePath($Row, [string]$DataDirectoryName) {
if ($null -eq $Row) {
return ""
}
$sourceName = [string]$Row.Cells["Source"].Value
if ([string]::IsNullOrWhiteSpace($sourceName)) {
return ""
}
$sourceFileName = ($sourceName -split ':', 2)[0]
$sourceFilePath = Join-Path (Resolve-LaunchDataDirectoryPath -DataDirectory $DataDirectoryName) $sourceFileName
if (-not (Test-Path -LiteralPath $sourceFilePath -PathType Leaf)) {
return ""
}
return $sourceFilePath
}
function Get-ConfigFolderPath([string]$DataDirectoryName) {
$configFolderPath = Resolve-LaunchDataDirectoryPath -DataDirectory $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-ExistingFileSystemPathFromRowValue($Row, [string]$ColumnName) {
$path = Get-ExpandedRowValue -Row $Row -ColumnName $ColumnName
if ([string]::IsNullOrWhiteSpace($path)) {
return ""
}
if (Test-Path -LiteralPath $path) {
return $path
}
return ""
}
function Get-PreferredParentFolderPath($Row, [string]$ClickedColumnName) {
$paramsPath = Get-ExistingFileSystemPathFromRowValue -Row $Row -ColumnName "Params"
$commandPath = Get-ExistingFileSystemPathFromRowValue -Row $Row -ColumnName "Command"
$targetPath = if ($ClickedColumnName -eq "Params" -and -not [string]::IsNullOrWhiteSpace($paramsPath)) {
$paramsPath
}
else {
$commandPath
}
return Get-ParentFolderPath -Path $targetPath
}
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 Restore-WindowPlacement($Form, $State) {
if ($null -eq $Form -or $null -eq $State -or $null -eq $State.window) {
return
}
$unset = [int]::MinValue
$x = Get-JsonIntValue -Object $State.window -Name "x" -DefaultValue $unset
$y = Get-JsonIntValue -Object $State.window -Name "y" -DefaultValue $unset
if ($x -eq $unset -or $y -eq $unset) {
return
}
$Form.Location = [System.Drawing.Point]::new($x, $y)
}
function Get-GridColumnWidths($Grid) {
$widths = [ordered]@{}
if ($null -eq $Grid) {
return [pscustomobject]$widths
}
foreach ($column in $Grid.Columns) {
$widths[$column.Name] = $column.Width
}
return [pscustomobject]$widths
}
function Save-LaunchState($App) {
if ($null -eq $App -or $null -eq $App.Form -or $null -eq $App.Settings) {
return
}
try {
Ensure-LaunchConfigDirectory | Out-Null
$bounds = if ($App.Form.WindowState -eq [System.Windows.Forms.FormWindowState]::Normal) {
$App.Form.Bounds
}
else {
$App.Form.RestoreBounds
}
$state = [pscustomobject]@{
showBuiltIn = [bool]$App.ShowBuiltIn
window = [pscustomobject]@{
width = $bounds.Width
height = $bounds.Height
x = $bounds.X
y = $bounds.Y
}
grid = [pscustomobject]@{
columnWidths = Get-GridColumnWidths -Grid $App.Grid
}
}
Write-JsonFile -Path $App.Settings.StateFilePath -Value $state
}
catch {
Write-Debug $_
}
}
function Set-GridColumns($Grid, $State = $null) {
$Grid.Columns["Title"].DisplayIndex = 0
$Grid.Columns["Command"].DisplayIndex = 1
$Grid.Columns["Params"].DisplayIndex = 2
$Grid.Columns["Keywords"].DisplayIndex = 3
$Grid.Columns["Source"].DisplayIndex = 4
$Grid.AutoSizeColumnsMode = 'None'
$Grid.ScrollBars = 'Both'
$defaultWidths = @{
Title = 200
Command = 150
Params = 100
Keywords = 100
Source = 80
}
foreach ($columnName in @("Title", "Command", "Params", "Keywords", "Source")) {
$width = $defaultWidths[$columnName]
if ($null -ne $State -and $null -ne $State.grid -and $null -ne $State.grid.columnWidths) {
$width = Get-JsonIntValue -Object $State.grid.columnWidths -Name $columnName -DefaultValue $width
}
$Grid.Columns[$columnName].Width = [Math]::Max(20, $width)
}
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 = Get-LogDirectoryPath
if (-not (Test-Path -LiteralPath $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
}
return (Join-Path $logDir "launchan-error.log")
}
function Write-AppLog([string[]]$Lines) {
try {
$logFilePath = Get-LogFilePath
$logLines = @(
("[{0}]" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"))
) + $Lines + @("")
Add-Content -Path $logFilePath -Value $logLines -Encoding UTF8
}
catch {
Write-Debug $_
}
}
function Get-LogDirectoryPath {
return (Ensure-LaunchConfigDirectory)
}
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($ErrorRecord) {
$messageLines = @(
(Get-String -Key "UnexpectedError")
)
if ($null -ne $ErrorRecord) {
$messageLines += @(
"",
("Message: {0}" -f $ErrorRecord.Exception.Message),
("Script: {0}" -f $ErrorRecord.InvocationInfo.ScriptName),
("Line: {0}" -f $ErrorRecord.InvocationInfo.ScriptLineNumber)
)
}
[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 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 -ErrorRecord $_
}
launchan.vbs
' Copyright (c) 2026 takaaki024
' Licensed under the MIT License
Set fso = CreateObject("Scripting.FileSystemObject")
Set shell = CreateObject("WScript.Shell")
ps1Path = fso.GetParentFolderName(WScript.ScriptFullName) & "\launchan.ps1"
configDirPath = fso.GetParentFolderName(WScript.ScriptFullName) & "\launchan-config"
If Not fso.FolderExists(configDirPath) Then
fso.CreateFolder(configDirPath)
End If
logPath = configDirPath & "\launchan-bootstrap.log"
shell.Run "cmd.exe /c powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & ps1Path & """ 1>""" & logPath & """ 2>>&1", 0, False