How to run internal commands after a third-party program? (I want Change icon)

English support forum

Moderators: Hacker, petermad, Stefan2, white

Post Reply
Mapaler
Junior Member
Junior Member
Posts: 17
Joined: 2022-01-29, 06:05 UTC

How to run internal commands after a third-party program? (I want Change icon)

Post by *Mapaler »

Background
I often need to change a numeric parameters passed into the program.
Image: https://github.com/user-attachments/assets/16059180-6316-4fbe-86db-dd7a8063d1f6
Image: https://github.com/user-attachments/assets/01736bac-dab8-468f-a825-77d8814784dc

At the same time, there are 3 softwares need to make such modifications. It's inconvenient to change it every time.
Image: https://github.com/user-attachments/assets/00e889cd-770b-4f2f-b91b-d6221e506b1d

So I decided to let the parameters read the environment variables and write a script to set the environment variables.
powershell
-ExecutionPolicy Bypass -File "%|OneDrive|\icon\change_length.ps1"
Image: https://github.com/user-attachments/assets/f2e46944-2e28-49ca-96d4-83883aeea3a5
This is my script.

Code: Select all

$EnvironmentVariableName = "filename_postfix_length"
$EnvironmentVariableScope = "User"
function InputLength {
	param(
		[Parameter(Mandatory,
			ValueFromPipeline)]
		[ValidateRange(1,9)]
		[byte]$length
	)
	PROCESS {
        Write-Output $length
    }
}

Write-Host "Current filename postfix length is $([Environment]::GetEnvironmentVariable($EnvironmentVariableName, $EnvironmentVariableScope))。"

$length = Read-Host -Prompt "Input new postfix length, only input 1-9" | InputLength
[Environment]::SetEnvironmentVariable($EnvironmentVariableName, $length, $EnvironmentVariableScope)

Write-Host "The new postfix length is $([Environment]::GetEnvironmentVariable($EnvironmentVariableName, $EnvironmentVariableScope))。"
Then I don't have to change the parameters of the button every time.
%ProgramFree%\SkyScan\DataViewer\DataViewer.exe
/n%|filename_postfix_length| %P%S
Image: https://github.com/user-attachments/assets/e02e634e-9e6c-4154-9b1b-28a5ca2167b2

Target
After changing the number each time, change the icon on the button so that I can see what the current number is.
Image: https://github.com/user-attachments/assets/72a97df5-6957-4e96-bf33-a823239b8911

Change the button icon path in Vertical.bar

Code: Select all

[Buttonbar]
button1=%OneDrive%\icon\8.ico
Then run the cm_ReloadBarIcons could reload bar icons.

How to pass the path of the Vertical.bar and the index number of the button to my script?

I don't want to write a fixed path, the path is different on different computers.

Or I can use environment in filename

Code: Select all

[Buttonbar]
button1=%OneDrive%\icon\%filename_postfix_length%.ico
But it's alse need to run cm_ReloadBarIcons.

How to auto run cm_ReloadBarIcons after the script?

I don't know how to run internal commands after a third-party program.
User avatar
ghisler(Author)
Site Admin
Site Admin
Posts: 50390
Joined: 2003-02-04, 09:46 UTC
Location: Switzerland
Contact:

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *ghisler(Author) »

You can send a message to the Total Commander main window with message number 1075 and WPARAM set to the command you want to execute. The numeric value of cm_ReloadBarIcons would be 2945.
Author of Total Commander
https://www.ghisler.com
Fla$her
Power Member
Power Member
Posts: 2982
Joined: 2020-01-18, 04:03 UTC

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *Fla$her »

Something like this:

Code: Select all

Add-Type -TypeDefinition @"
    using System;
    using System.Runtime.InteropServices;

    public static class User32
    {
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr GetForegroundWindow();
        public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    }
"@

[User32]::SendMessage([User32]::GetForegroundWindow(), 1075, 2945, 0)
Overquoting is evil! 👎
User avatar
beb
Power Member
Power Member
Posts: 579
Joined: 2009-09-20, 08:03 UTC
Location: Odesa, Ukraine

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *beb »

2Mapaler
Since you have already got $filename_postfix_length variable defined in/by your PowerShell script--you could let that same script do all the other required things.

Your PowerShell script (which I named iconSwitch.ps1 within my test) with a little bit more commands:

Code: Select all

<#
$EnvironmentVariableName = "filename_postfix_length"
$EnvironmentVariableScope = "User"
function InputLength {
	param(
		[Parameter(Mandatory,
			ValueFromPipeline)]
		[ValidateRange(1,9)]
		[byte]$length
	)
	PROCESS {
        Write-Output $length
    }
}

Write-Host "Current filename postfix length is $([Environment]::GetEnvironmentVariable($EnvironmentVariableName, $EnvironmentVariableScope))。"

$length = Read-Host -Prompt "Input new postfix length, only input 1-9" | InputLength
[Environment]::SetEnvironmentVariable($EnvironmentVariableName, $length, $EnvironmentVariableScope)

Write-Host "The new postfix length is $([Environment]::GetEnvironmentVariable($EnvironmentVariableName, $EnvironmentVariableScope))。"
#>

# in your case $filename_postfix_length variable is defined by your code above.
# here in my example, for the sake of convenience, I would not use your code,
# and define it (the variable) instead as a random number within 0..241 (nothing special, just reflecting how many icons I used in the test):
$filename_postfix_length = (Get-Random -min 0 -max 241)

# then I would use it to define an $icon suitable for the Total Commander toolbar button
$icon = '%OneDrive%\Icons\'+$filename_postfix_length.toString()+'.ico'

# define the button bar within the Total Commander path to deal with
$buttonBar   = [IO.Path]::combine($env:commander_path,'Custom\myButton.bar') # in your case it could be ...combine($env:commander_path,'VERTICAL.BAR')
# define icon source path within OneDrive
$iconLibrary = [IO.Path]::combine($env:OneDrive,'Icons')

# some encoding examples (highly recommend to keep your .bar files as UTF-16LE with BOM)
$ansi    = [Text.Encoding]::getEncoding("windows-1251")
$utf8    = [Text.Encoding]::getEncoding("utf-8")
$unicode = [Text.Encoding]::getEncoding("Unicode")

# read contents of the $buttonBar as the $text array
$text  = [IO.File]::ReadAllText($buttonBar)

# define a pattern (to understand we are dealing with the right button)
$pattern = 'button33=' # in your case it could be 'button1=' 

# analyze the $buttonBar contents (defined as the $text array) line by line
foreach  ($line in $text) {
if ($line -match $pattern) {
# in $line that matches $pattern change icon path to $icon and that becomes an $update 
$update = $line -replace "button33=(.*)","button33=$icon" # there are too many literals here in this line: I was way lazy to automate it properly, sorry
# then, in its turn, in the $text we have we redefine that $line as the $update
$text = $text.replace($line,$update)}}

# write the updated $text back to $buttonBar
[IO.File]::WriteAllText($buttonBar,$text,$unicode)

<# this command in the script has been commented after adding the cm_wait command into the TC commands chain
# wait a bit just in case (to make sure the updated $buttonBar is properly saved)
sleep -m 1000 # 1000 milliseconds :: you may change this to whatever suits your environment
#>

#pause
User-command em_iconSwitch (in usercmd.ini)

Code: Select all

[em_iconSwitch]
cmd=powershell -c "%commander_path%\Plugins\PowerShell\iconSwitch.ps1"
User-button with chained em_iconSwitch and cm_ReloadBarIcons commands (also the cm_wait command has been added into the sequence)

Code: Select all

TOTALCMD#BAR#DATA
em_iconSwitch,cm_wait 1000,cm_ReloadBarIcons

WCMICON2.DLL
em_iconSwitch,cm_wait 1000,cm_ReloadBarIcons chain


-1

Here's how it works (video illustration):
Image: https://i.imgur.com/btqdkq4.mp4 (before adding the cm_wait command)
Image: https://i.imgur.com/XNxGs0a.mp4 (same as above with the cm_wait command added into the TC command sequence)
Note: The rightmost button on the toolbar is where we expect icon switching to occur (it's button 33 on my toolbar), and the button next to it (the second to the right) is the command button that runs the em_iconSwitch,cm_wait,cm_ReloadBarIcons command sequence.

Note 2: A friendly piece of advice, if such an option is under your control, switch from Windows PowerShell to cross-platform PowerShell:
https://learn.microsoft.com/en-us/powershell/scripting/whats-new/migrating-from-windows-powershell-51-to-powershell-7
https://learn.microsoft.com/en-us/powershell/scripting/whats-new/differences-from-windows-powershell
https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows
https://github.com/PowerShell/PowerShell/releases
This is a user-friendly upgrade:
You still keep the outdated Windows PowerShell (moreover, I won't recommend deleting it, sometimes it is useful for testing, etc.),
while having the modern alternative--the cross-platform one, that is in active development.
On a user level (after the upgrade):
- whenever you type "powershell" (including TotalCommander user commands) you are using Windows PowerShell,
- whenever you type "pwsh" you are using cross-platform PowerShell, and that's it.

Note 3. By the way, it's been a pretty interesting challenge to complete. Thanks.
#278521 User License
Total Commander [always the latest version, including betas] x86/x64 on Win10 x64/Android 10/15
Mapaler
Junior Member
Junior Member
Posts: 17
Joined: 2022-01-29, 06:05 UTC

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *Mapaler »

Fla$her wrote: 2024-12-26, 09:08 UTC Something like this:

Code: Select all

Add-Type -TypeDefinition @"
    using System;
    using System.Runtime.InteropServices;

    public static class User32
    {
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr GetForegroundWindow();
        public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    }
"@

[User32]::SendMessage([User32]::GetForegroundWindow(), 1075, 2945, 0)
Thanks, @ghisler's explanation is so low-level that I don't understand what it means without @Fla$her's code examples.

But GetForegroundWindow gets the powershell itself, so I need to use FindWindow

Code: Select all

$User32 = Add-Type -Name Funcs -Namespace Win32 -PassThru -MemberDefinition @"
	[DllImport("user32.dll", CharSet = CharSet.Unicode)]
	public static extern IntPtr FindWindow(string className, string windowName);
	[DllImport("user32.dll", CharSet = CharSet.Auto)]
	public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
"@

$TCmessage = 1075;
$cm_ReloadBarIcons = 2945;

$hWnd_TC = $User32::FindWindow("TTOTAL_CMD", [NullString]::Value)
$User32::SendMessage($hWnd_TC, $TCmessage, $cm_ReloadBarIcons, 0)
Image: https://github.com/user-attachments/assets/f7243db1-38cd-42ed-9d5c-b3fbfd85111b
Fla$her
Power Member
Power Member
Posts: 2982
Joined: 2020-01-18, 04:03 UTC

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *Fla$her »

Mapaler wrote: 2025-01-13, 02:16 UTC But GetForegroundWindow gets the powershell itself, so I need to use FindWindow
The powershell window was supposed to be hidden.
Overquoting is evil! 👎
Mapaler
Junior Member
Junior Member
Posts: 17
Joined: 2022-01-29, 06:05 UTC

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *Mapaler »

Fla$her wrote: 2025-01-13, 03:08 UTC
Mapaler wrote: 2025-01-13, 02:16 UTC But GetForegroundWindow gets the powershell itself, so I need to use FindWindow
The powershell window was supposed to be hidden.
In my environment, it stayed running for a while, so the hWnd I got was inconsistent with the TC window. I use FindWindow and it already works.
Fla$her
Power Member
Power Member
Posts: 2982
Joined: 2020-01-18, 04:03 UTC

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *Fla$her »

Mapaler wrote: 2025-01-13, 03:23 UTC In my environment, it stayed running for a while
It's about launching in a hidden form, in case you didn't understand.
I always do this if I need to use console applications for TC.
Mapaler wrote: 2025-01-13, 03:23 UTC I use FindWindow and it already works.
You are searching by class, and there may be several windows with this class.
Overquoting is evil! 👎
Mapaler
Junior Member
Junior Member
Posts: 17
Joined: 2022-01-29, 06:05 UTC

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *Mapaler »

Fla$her wrote: 2025-01-13, 03:27 UTC
Mapaler wrote: 2025-01-13, 03:23 UTC In my environment, it stayed running for a while
It's about launching in a hidden form, in case you didn't understand.
I always do this if I need to use console applications for TC.
Mapaler wrote: 2025-01-13, 03:23 UTC I use FindWindow and it already works.
You are searching by class, and there may be several windows with this class.
I'm not a professional programmer and can only imitate existing code. Your code will get an error when I run it directly(see the image), so I changed it to another format which I found on Internet.
Image: https://github.com/user-attachments/assets/5d1ebd30-2d45-4096-82c2-e1686580d590

I need to input a number before I execute the subsequent commands, so I can't hide the terminal window.
I'll only run one TC window, so using classes should solve most of my problems.
Fla$her
Power Member
Power Member
Posts: 2982
Joined: 2020-01-18, 04:03 UTC

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *Fla$her »

2Mapaler
I need to input a number before I execute the subsequent commands, so I can't hide the terminal window.
You can do this via InputBox so that you don't have to access the terminal.
It's also possible to process the list by matching numbers from names.
I'll only run one TC window, so using classes should solve most of my problems.
I doubt it. You make it sound like you've never run at least 2 instances for tests.
In addition, DC has the same class as TC. But you probably say that you don't use it. In any case, it's right to choose the approach that eliminates potential problems.

P.S.: Instead of overquoting, it's better to use the nickname of the interlocutor.
Overquoting is evil! 👎
Mapaler
Junior Member
Junior Member
Posts: 17
Joined: 2022-01-29, 06:05 UTC

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *Mapaler »

beb wrote: 2024-12-26, 14:56 UTC User-command em_iconSwitch (in usercmd.ini)

Code: Select all

[em_iconSwitch]
cmd=powershell -c "%commander_path%\Plugins\PowerShell\iconSwitch.ps1"
User-button with chained em_iconSwitch and cm_ReloadBarIcons commands (also the cm_wait command has been added into the sequence)

Code: Select all

TOTALCMD#BAR#DATA
em_iconSwitch,cm_wait 1000,cm_ReloadBarIcons

WCMICON2.DLL
em_iconSwitch,cm_wait 1000,cm_ReloadBarIcons chain


-1

Thanks for the reply, for the first time I learned that it was possible to write custom commands.

After I tried it with my script, I found that it was always async.
If I enter a number longer than cm_wait, the cm_ReloadBarIcons will run first, causing the icon replacement to fail.
Adding chain after the command doesn't make any difference.
beb wrote: 2024-12-26, 14:56 UTC Your PowerShell script (which I named iconSwitch.ps1 within my test) with a little bit more commands:

Code: Select all

$text  = [IO.File]::ReadAllText($buttonBar)
$pattern = 'button33=' 
foreach  ($line in $text) {
if ($line -match $pattern) {
$update = $line -replace "button33=(.*)","button33=$icon"
$text = $text.replace($line,$update)}}
[IO.File]::WriteAllText($buttonBar,$text,$unicode)
I like to use Win32API to edit INI.

Code: Select all

$ini = Add-Type -memberDefinition @"
[DllImport("Kernel32")]
public static extern long WritePrivateProfileString (string section, string key, string val, string filePath );
[DllImport("Kernel32")]
public static extern int GetPrivateProfileString (string section, string key, string def, StringBuilder retVal, int size, string filePath ); 
"@ -PassThru -name ini -UsingNamespace System.Text

function ReadIni {
	[CmdletBinding()]
	param(
		[Parameter(Mandatory)][string]$section,
		[Parameter(Mandatory)][string]$key,
		[Parameter(Mandatory)][string]$filePath
	)
	process {
		$retVal=New-Object -TypeName "System.Text.StringBuilder"
		$null = $ini::GetPrivateProfileString($section,$key,"",$retVal,255,$filePath)
		Write-Output $retVal.tostring()
	}
}
function WriteIni {
	[CmdletBinding()]
	param(
		[Parameter(Mandatory)][string]$section,
		[Parameter(Mandatory)][string]$key,
		[Parameter(Mandatory)][string]$value,
		[Parameter(Mandatory)][string]$filePath
	)
	process {
		$null=$ini::WritePrivateProfileString($section,$key,$value,$filePath)
	}
}
$section="Buttonbar"
$key="button1"
$value = "%OneDrive%\icon\$length.ico"
$filePath="C:\Program Files\TotalCMD64\Vertical.bar"
WriteIni -section $section -key $key -value $value -filePath $filePath

beb wrote: 2024-12-26, 14:56 UTC Note 2: A friendly piece of advice, if such an option is under your control, switch from Windows PowerShell to cross-platform PowerShell:
I installed powershell 7, but it wasn't localized. Image: https://github.com/user-attachments/assets/a11d4fe9-ee5d-44b6-9f8d-16e9214831e3
My English is not good, I wrote Chinese first for this post, and then used translation software to translate it into English.
Both TC and my professional software only run on Windows, cross-platform doesn't make any sense to me.
And I didn't see any functional differences, so I used the localized powershell that built-in with Windows.
Mapaler
Junior Member
Junior Member
Posts: 17
Joined: 2022-01-29, 06:05 UTC

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *Mapaler »

@Fla$her
You can do this via InputBox so that you don't have to access the terminal.
I only know that WScript + VBS can use InputBox, but its syntax is so old and painful that I don't want to use it anymore.
When I use CMD/BAT or PowerShell, a terminal window pops up. Maybe it will flash close but can't be completely hidden.
It's also possible to process the list by matching numbers from names.
Do you mean that I pass the file that needs to be opened as a parameter to the script, and the script recognize the number of digits in the file name?
This is indeed a solution.
In addition, DC has the same class as TC.
I don't know who is DC.
Fla$her
Power Member
Power Member
Posts: 2982
Joined: 2020-01-18, 04:03 UTC

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *Fla$her »

2Mapaler
I only know that WScript + VBS can use InputBox, but its syntax is so old and painful that I don't want to use it anymore.
Using the API, you can recreate almost any window, including InputBox.

Code: Select all

Add-Type -AssemblyName Microsoft.VisualBasic
$inputText = [Microsoft.VisualBasic.Interaction]::InputBox("Enter some value:", "Window Title", "Default value")
But syntax is the last thing you should focus on when implementing the required tasks. The efficiency/performance itself is much more important. In a number of custom tasks, powershell only hinders with its slow appearance and console, so it's better to use non-console interpreters, be it wscript, AutoIt3, AutoHotkeyU*, etc. But purely administrative tasks for deep recursions, OS and network maintenance, or where the console itself is an auxiliary tool for analyzing tabular data, it's better, of course, to use powershell.
And in general, the obsolescence of syntax is nothing more than speculation. Syntactically, Basic underlies many current languages (and not only branches from it), including scripting languages (for example, AutoIt or, say, the Autorun plugin language).
but can't be completely hidden.
Why did you decide that? I also wrote that you can launch the window in a hidden form, through a utility [ hide, for example ] or js/vbs [ WshShell.Run('powerhell ...', 0) ].
Do you mean that I pass the file that needs to be opened as a parameter to the script
Only, considering %S is not a file, but files. You can also go through the entire list of files in the current folder of the active panel.
I don't know who is DC.

Code: Select all

DC = Double Commander
FC = Free   Commander
MC = Multi  Commander
SC = Speed  Commander
TC = Total  Commander
etc.
Overquoting is evil! 👎
User avatar
beb
Power Member
Power Member
Posts: 579
Joined: 2009-09-20, 08:03 UTC
Location: Odesa, Ukraine

Re: How to run internal commands after a third-party program? (I want Change icon)

Post by *beb »

beb wrote: 2024-12-26, 14:56 UTC ... PowerShell script (iconSwitch.ps1)...

Code: Select all

...
# read contents of the $buttonBar as the $text string array
$text  = [IO.File]::ReadAllText($buttonBar)
...
# analyze the $buttonBar contents (defined as the $text string array) line by line
foreach  ($line in $text) {
if ($line -match $pattern) {
# in $line that matches $pattern change icon path to $icon 
$update = $line -replace "button33=(.*)","button33=$icon"
...}}
# write the updated $text back to $buttonBar
[IO.File]::WriteAllText($buttonBar,$text,$unicode)

Important note for the future readers

Unfortunately, I cannot edit that message, so I'm putting this here.

The cited code worked in this particular use case but not exactly in the way that was described.
That is because of the [IO.File]::ReadAllText() method nature:
[IO.File]::ReadAllText() reads the entire file into memory as a single string (including the line break characters).
https://learn.microsoft.com/en-us/dotnet/api/system.io.file.readalltext
Therefore the code may fail in similar scenarios whenever user-applied regular expressions (for -match and -replace operations) would catch something out of the target line range.

To get the script to behave namely in the described manner (to read $text as an array of lines, and then to process it line by line):
The [IO.File]::ReadAllLines() method should be used instead for the initial reading.
https://learn.microsoft.com/en-us/dotnet/api/system.io.file.readalllines
Correspondingly, the [IO.File]::WriteAllLines() method should be used for the finalizing writing.
https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writealllines


So the PowerShell (iconSwitch.ps1) script code becomes as follows:

Code: Select all

# read contents of the $buttonBar as the $text array
$text  = [IO.File]::ReadAllLines($buttonBar)
...
# analyze the $buttonBar contents (defined as the $text array) line by line
foreach  ($line in $text) {
if ($line -match $pattern) {
# in $line that matches $pattern change icon path to $icon 
$update = $line -replace "button33=(.*)","button33=$icon"
...}}
# write the updated $text back to $buttonBar
[IO.File]::WriteAllLines($buttonBar,$text,$unicode)
I somehow messed with the methods during my experiments and posted here not accurate version of the code. Sorry.
#278521 User License
Total Commander [always the latest version, including betas] x86/x64 on Win10 x64/Android 10/15
Post Reply