It is currently October 9th, 2024, 3:26 pm

Tips & Tricks Thread

Discuss the use of Lua in Script measures.
User avatar
MerlinTheRed
Rainmeter Sage
Posts: 889
Joined: September 6th, 2011, 6:34 am

Re: Tips & Tricks Thread

Post by MerlinTheRed »

What would you do with that?
Have more fun creating skins with Sublime Text 2 and the Rainmeter Package!
User avatar
thatsIch
Posts: 446
Joined: August 7th, 2012, 9:18 pm

Re: Tips & Tricks Thread

Post by thatsIch »

MerlinTheRed wrote:What would you do with that?
Generating readable tables or have you ever tried

print(table)

in Lua?
User avatar
thatsIch
Posts: 446
Joined: August 7th, 2012, 9:18 pm

Re: Tips & Tricks Thread

Post by thatsIch »

Some time ago (like 2 years ago) I found a very usefull usage of metatables on this forum here which enables the creator a very easy way to manipulate the meters, measures and variables. It was just kinda buggy or more missing parts which where more corner cases.

I added some comments about maybe confusing parts and have decrypted most parts

Usage Examples:
  • meters.meterExample.Text resolves to SKIN:GetMeter('meterExample'):GetOption('Text')
  • meters.meterExample.Text = 'Example' >SKIN:Bang('!SetOption', 'meterExample', 'Text', 'Example', '#CURRENTCONFIG#')
  • measures.measureExample.UpdateMeasure() > SKIN:Bang('!UpdateMeasure', 'measureExample', '#CURRENTCONFIG#')
  • variables.CURRENTCONFIG > SKIN:GetVariables('CURRENTCONFIG')
So as you can see, it offers more an OOP approach instead of typing SKIN: etc all the time

you might want to compact it back again into 4 rows, because it pretty much offers just as an interface :)

€ Updated

Usage Meters, Measures, Variables = dofile(SKIN:GetVariable('@').."Scripts\\libs\\InterfaceOOPAccess.lua")(SKIN)

InterfaceOOPAccess.lua

Code: Select all

return function(SKIN)
	local ms = {
		__index = function(table,key) 
			-- catch recursive call		
			if key == '__section' and #table < 2 then
				return

			elseif key == '__sectionname' and #table < 2 then
				return

			-- catch update()
			elseif key == 'update' and SKIN:GetMeasure(table.__sectionname) then 
				return function() SKIN:Bang('!UpdateMeasure',table.__sectionname, SKIN:GetVariable('CURRENTCONFIG')) end 

			-- catch update()	
			elseif key == 'update' and SKIN:GetMeter(table.__sectionname) then 
				return function() SKIN:Bang('!UpdateMeter',table.__sectionname, SKIN:GetVariable('CURRENTCONFIG')) end 

			-- catch isMeter()
			elseif key == 'isMeter' then
				return function() return SKIN:GetMeter(table.__sectionname) and true or false end

			-- catch isMeter()
			elseif key == 'isMeasure' then
				return function() return SKIN:GetMeasure(table.__sectionname) and true or false end

			-- catch hide()
			elseif key == 'hide' and SKIN:GetMeter(table.__sectionname) then
				return function() SKIN:Bang('!HideMeter',table.__sectionname, SKIN:GetVariable('CURRENTCONFIG')) end

			-- catch show()
			elseif key == 'show' and SKIN:GetMeter(table.__sectionname) then
				return function() SKIN:Bang('!ShowMeter',table.__sectionname, SKIN:GetVariable('CURRENTCONFIG')) end

			-- catch Rainmeter Native Build-In functions
			-- Show, Hide, SetXYWH, GetXYWH, GetName, GetOption (though special case), Enable, Disable, GetValueRange, GetRelativeValue, GetMaxValue, 
			elseif table.__section[key] then 
				return function(...) return table.__section[key](table.__section,...) end 
			
			-- catch meter options
			elseif table.__section.GetOption then
				return table.__section:GetOption(key)

			-- catch measure options
			elseif table.__section.GetNumberOption then
				return (table.__section.GetNumberOption and table.__section:GetNumberOption(key,nil))
			
			-- unknown case
			else
				print('Unkown Section Case: ' .. key) 
				return
			end 
		end, 
		__newindex = function(table,key,value) SKIN:Bang('!SetOption',table.__sectionname,key,value,SKIN:GetVariable('CURRENTCONFIG')) end
	}
	local sections = {
		__index = function(table,key) 
			if key == 'redraw' then
				return function() SKIN:Bang('!Redraw', SKIN:GetVariable('CURRENTCONFIG')) end
			else
				sections[key] = {}

				-- store meter/measure
				sections[key].__section = SKIN:GetMeasure(key) or SKIN:GetMeter(key)
				
				-- store meter/measurename
				sections[key].__sectionname = key 
				setmetatable(sections[key],ms) 

				return sections[key]
			end
		end,
		isMeter = function(meter)
			return SKIN:GetMeter(meter) and true or false
		end,
		isMeasure = function(measure)
			return SKIN:GetMeasure(measure) and true or false
		end,
		toggleGroup = function(meterGroup)
			SKIN:Bang('!ToggleMeterGroup', meterGroup,SKIN:GetVariable('CURRENTCONFIG'))
		end
	}
	local variables = {
		__index = function(table,key) 

			-- catch variables.ReplaceVariables()
			if key == 'ReplaceVariables' then
				return function(param) return SKIN:ReplaceVariables(param) end

			-- catch variable			
			elseif SKIN:GetVariable(key) then
				return SKIN:GetVariable(key) 

			-- unknown case
			else 
				print('Unkown Variable Case: ' .. key)
				return
			end
		end, 
		__newindex = function(table,key,value) SKIN:Bang('!SetVariable',key,value, SKIN:GetVariable('CURRENTCONFIG')) end
	}
	setmetatable(variables,variables)
	setmetatable(sections,sections)

	return sections, sections, variables
end
You do not have the required permissions to view the files attached to this post.
ailia
Posts: 34
Joined: August 6th, 2012, 9:56 pm
Location: Lurking

Re: Tips & Tricks Thread

Post by ailia »

thatsIch wrote:

Code: Select all

return function(SKIN)
	local ms = {
		__index = function(table,key) 
			-- catch recursive call		
			if key == '__section' and #table < 2 then
				return

			elseif key == '__sectionname' and #table < 2 then
				return

			-- catch update()
			elseif key == 'update' and SKIN:GetMeasure(table.__sectionname) then 
				return function() SKIN:Bang('!UpdateMeasure',table.__sectionname, SKIN:GetVariable('CURRENTCONFIG')) end 

			-- catch update()	
			elseif key == 'update' and SKIN:GetMeter(table.__sectionname) then 
				return function() SKIN:Bang('!UpdateMeter',table.__sectionname, SKIN:GetVariable('CURRENTCONFIG')) end 

			-- catch isMeter()
			elseif key == 'isMeter' then
				return function() return SKIN:GetMeter(table.__sectionname) and true or false end

			-- catch isMeter()
			elseif key == 'isMeasure' then
				return function() return SKIN:GetMeasure(table.__sectionname) and true or false end

			-- catch hide()
			elseif key == 'hide' and SKIN:GetMeter(table.__sectionname) then
				return function() SKIN:Bang('!HideMeter',table.__sectionname, SKIN:GetVariable('CURRENTCONFIG')) end

			-- catch show()
			elseif key == 'show' and SKIN:GetMeter(table.__sectionname) then
				return function() SKIN:Bang('!ShowMeter',table.__sectionname, SKIN:GetVariable('CURRENTCONFIG')) end

			-- catch Rainmeter Native Build-In functions
			-- Show, Hide, SetXYWH, GetXYWH, GetName, GetOption (though special case), Enable, Disable, GetValueRange, GetRelativeValue, GetMaxValue, 
			elseif table.__section[key] then 
				return function(...) return table.__section[key](table.__section,...) end 
			
			-- catch meter options
			elseif table.__section.GetOption then
				return table.__section:GetOption(key)

			-- catch measure options
			elseif table.__section.GetNumberOption then
				return (table.__section.GetNumberOption and table.__section:GetNumberOption(key,nil))
			
			-- unknown case
			else
				print('Unkown Section Case: ' .. key) 
				return
			end 
		end, 
		__newindex = function(table,key,value) SKIN:Bang('!SetOption',table.__sectionname,key,value,SKIN:GetVariable('CURRENTCONFIG')) end
	}
	local sections = {
		__index = function(table,key) 
			if key == 'redraw' then
				return function() SKIN:Bang('!Redraw', SKIN:GetVariable('CURRENTCONFIG')) end
			else
				sections[key] = {}

				-- store meter/measure
				sections[key].__section = SKIN:GetMeasure(key) or SKIN:GetMeter(key)
				
				-- store meter/measurename
				sections[key].__sectionname = key 
				setmetatable(sections[key],ms) 

				return sections[key]
			end
		end,
		isMeter = function(meter)
			return SKIN:GetMeter(meter) and true or false
		end,
		isMeasure = function(measure)
			return SKIN:GetMeasure(measure) and true or false
		end,
		toggleGroup = function(meterGroup)
			SKIN:Bang('!ToggleMeterGroup', meterGroup,SKIN:GetVariable('CURRENTCONFIG'))
		end
	}
	local variables = {
		__index = function(table,key) 

			-- catch variables.ReplaceVariables()
			if key == 'ReplaceVariables' then
				return function(param) return SKIN:ReplaceVariables(param) end

			-- catch variable			
			elseif SKIN:GetVariable(key) then
				return SKIN:GetVariable(key) 

			-- unknown case
			else 
				print('Unkown Variable Case: ' .. key)
				return
			end
		end, 
		__newindex = function(table,key,value) SKIN:Bang('!SetVariable',key,value, SKIN:GetVariable('CURRENTCONFIG')) end
	}
	setmetatable(variables,variables)
	setmetatable(sections,sections)

	return sections, sections, variables
end

Code: Select all

function meta()
local ms = {__index = function(tb,key) if key == 'UpdateMeasure' then return function() SKIN:Bang('!UpdateMeasure',tb.__sectionname) end elseif tb.__section[key] then return function(...) return tb.__section[key](tb.__section,...) end else return (tb.__section.GetNumberOption and tb.__section:GetNumberOption(key,nil)) or tb.__section:GetOption(key) end end, __newindex = function(tb,key,value) SKIN:Bang('!SetOption',tb.__sectionname,key,value) end}
sections,variables = {__index = function(tb,key) sections[key]={} sections[key].__section, sections[key].__sectionname = SKIN:GetMeasure(key), key setmetatable(sections[key],ms) return sections[key] end},{__index = function(tb,key) return SKIN:GetVariable(key) end, __newindex = function(tb,key,value) SKIN:Bang('!SetVariable',key,value) end}
setmetatable(variables,variables)
setmetatable(sections,sections)
meters, measures = sections, sections
end
The referenced thread: http://rainmeter.net/forum/viewtopic.php?f=99&t=13027

That looks.... a little too familiar. Sounds like at least someone got some usage from my 4 line function I wrote ages ago. Kind of goes to show what parts of the lua API I didn't touch considering what cases I missed, especially since I've still been using the metatable func in my latest skins. (UpdateMeter, Redraw, ToggleMeterGroup)

Kind of glad someone else could make sense of that cryptic mess though, especially ( function(...) return table.__section[key](table.__section,...) ) If I hadn't been working with lua so long, don't think I would have ever done something so unreadable.
kounger
Posts: 13
Joined: March 22nd, 2017, 12:14 am

io.popen replacement to get the contents of a folder

Post by kounger »

Rainmeter doesn't support the io.popen command in lua to get the contents of a folder. That's why I came up with a workaround that solves this task.

In short the workaround consists of a powershell script that writes the filepaths into a text file. The text file with its file paths can then be read into an array in lua.

Powershell is necessary to solve this task since it includes a StreamWriter that can write an output file in a fraction of a second. I first tried to create this file with a simple CMD command but CMD needs around 10 seconds to complete this write task and since powershell comes pre installed since Windows 7 there should be no reason to avoid its use.

I created a test skin where every step is explained with comments to illustrate the functionality of the workaround. The skin searches for a random text file inside one of three folders inside the @Resources folder of the skin. It then displays the names of all files from the folder where the text file was found.

There are a few things to consider:
First the output file, in this skin called OutputFile.txt, should already exist before the skin is shipped. It's not a big problem otherwise but the skin only seems to work after the second refresh if this file doesn't exist already.

Second is the way the RunCommand measure is called. If there are variables or measure references inside the RunCommand measure it first has to be updated and then called as Brian explained to me in this forum. This happens with the line OnUpdateAction=[!UpdateMeasure MeasureRunFolderContent][!CommandMeasure MeasureRunFolderContent "Run"].

Third is that the paths that are handed to the RunCommandMeasure have to be tested if they include an apostrophe ' . If that's the case the apostrophe has to be escaped with another apostrophe. Substitute="'":"''" takes care of that.

Fourth is that the meters that are affected by the script should be updated and redrawn.

Fifth is that a single RunCommandMeasure should probably not be called multiple times during one update cycle. Otherwise the RunCommand plugin will return a 101 error, saying that the script is still running and can't be called again in the moment. In this skin the measure that calls RunCommand only gets updated every 10 seconds for example after the QuotePlugin had returned another file. This is taken care of by using UpdateDivider=-1 and by calling that measure manually.

Sixth is how files are treated that have unicode characters (e.g тس流हिंが) in their file paths. This article from the Rainmeter site and this forum post by jsmorley explain how to deal with this problem in detail. Here it's necessary to encode the ini-file of the skin with UCS-2 LE BOM by using Notepad++ for example. On top of that the lua files have to be encoded in UTF-16 LE. This can be done by opening the lua files inside the standard windows editor and saving them with unicode encoding as explained in this post. This should be done last after all testing is done since the lua files with unicode encoding may can't be displayed inside your Lua IDE anymore. On top of that the output file of the powershell script should be encoded in UTF-8 with BOM which is already taken care of by defining the encoding inside the script.

Code: Select all

[Rainmeter]
Update=1000

;Encoded in UCS-2 LE BOM with Notepad++

[Metadata]
Name=FolderContent
Author=kounger  
Information=A test skin which shows a replacement for io.popen in lua. 
Version=1.0
License=CC BY-NC-SA 3.0 

;Every 10 seconds this measure returns a new random text file from the folder #@#TestFolders
[MeasureRandomTextFile]
Measure=Plugin
Plugin=QuotePlugin
PathName=#@#TestFolders
Subfolders=1
FileFilter=*.txt
UpdateDivider=10
OnUpdateAction=[!UpdateMeasure "MeasureTextFileFolder"]
DynamicVariables=1

;This measure gets the path to the folder of the file [MeasureRandomTextFile]
;and then calls [MeasureRunFolderContent] to run.
;"'":"''" to escape single quotes ' for cmd
[MeasureTextFileFolder]
Measure=String
String=[MeasureRandomTextFile]
RegExpSubstitute=1
Substitute="\\[^\\]*$":"","'":"''"
UpdateDivider=-1
DynamicVariables=1
OnUpdateAction=[!UpdateMeasure MeasureRunFolderContent][!CommandMeasure MeasureRunFolderContent "Run"]

;This measure uses a powershell script to create a text file which contains the paths of all files that can be found inside the folder [MeasureTextFileFolder].
;1st Argument: Path to the powershell script.
;2nd Argument: Path(s) to the folder(s) whose files should be listed. Multiple folder paths have to be listed with a comma and no spaces. 'Path1','Path2'
;3rd Argument: 'true' for recursive listing of files. 'false' to only list the files from this one folder.
;4th Argument: 'true' to append the output to the output file. 'false' to overwrite the output file. 
;5th Argument: Path to the output file.
;OutputType=ANSI: ANSI is necessary to be able to read error meassages inside the rainmeter logs which the script may returns.
[MeasureRunFolderContent]
Measure=Plugin
Plugin=RunCommand
Program=PowerShell.exe
Parameter=-NoProfile -ExecutionPolicy Bypass -Command "& '#@#\FolderContent.ps1' '[MeasureTextFileFolder]' 'false' 'false' '#@#\OutputFile.txt'"
OutputType=ANSI
State=Hide
FinishAction=[!UpdateMeasure MeasureLuaFileList]
DynamicVariables=1

;This measure runs a lua script that returns a list of filenames
;by using a list of file paths from the text file #@#\OutputFile.txt.
[MeasureLuaFileList]
Measure=Script
ScriptFile=#@#ListFiles.lua
TableName=NoPath
UpdateDivider=-1
OnUpdateAction=[!UpdateMeter MeterTextFileList][!Redraw]

[MeterTextFileList]
Meter=String
MeasureName=MeasureLuaFileList
FontFace=Trebuchet MS
StringEffect=Border
StringAlign=LEFT
FontColor=255,255,255,255
FontSize=10
AntiAlias=1
X=1
Y=1
Here is the powershell script that writes the contents of a folder into an output file. It offers two extra arguments that dictate if the folder content should be listed recursively and whether the output should be appended to the existing output file. All arguments are explained inside the ini file above if you scroll to [MeasureRunFolderContent].

Code: Select all

$inputFolder = $args[0]
$recursive = $args[1]
$writeMode = $args[2]
$outputPath = $args[3]


if ($recursive -eq 'true'){
    $recursive = $true
}
elseif ($recursive -eq 'false') {
    $recursive = $false
}

#Hashtable with Get-ChildItem parameters
$parameters = @{
    LiteralPath = $inputFolder
    Recurse = $recursive  
}


if ($writeMode -eq 'true'){
    $writeMode = 'Append'
}
elseif ($writeMode -eq 'false') {
    $writeMode = 'Create'
}
 
#Array with FileStream parameters
$fileStreamParas = $outputPath , $writeMode, 'Write', 'Read'

try
{
    $fileStream = New-Object IO.FileStream $fileStreamParas
    $Utf8BomEncoding = New-Object System.Text.UTF8Encoding $True
    $streamWriter = New-Object System.IO.StreamWriter($fileStream, $Utf8BomEncoding)
    Get-ChildItem @parameters | ForEach-Object { $streamWriter.WriteLine($_.FullName)  }
}
finally
{
    $streamWriter.close()       
}
  
Here is the basic form of the powershell script that can be updated with anything the Get-ChildItem command and powershell has to offer.
Get-ChildItem Documentation

Code: Select all

$inputFolder = $args[0]
$outputPath = $args[1]
 
#Array with FileStream parameters
$fileStreamParas = $outputPath , 'Create', 'Write', 'Read'

try
{
    $fileStream = New-Object IO.FileStream $fileStreamParas
    $Utf8BomEncoding = New-Object System.Text.UTF8Encoding $True
    $streamWriter = New-Object System.IO.StreamWriter($fileStream, $Utf8BomEncoding)
    Get-ChildItem -LiteralPath $inputFolder  | ForEach-Object { $streamWriter.WriteLine($_.FullName)  }
}
finally
{
    $streamWriter.close()       
}
At last I also want to add the lua script which reads the output file of the powershell script into an array and creates an output string.

Code: Select all

function Initialize()     
   TempFilePath = SKIN:MakePathAbsolute('@Resources\\OutputFile.txt')   
end

function Update()                
   local array = ReadFileLines(TempFilePath)   
   return FilesToString(array)   
end

--function takes a path to a text file which contains
--full paths to directories and files. It writes all
--file paths line by line into an array.
function ReadFileLines(FilePath)	
	--Open File
	local File = io.open(FilePath)
  
	--Handle Error opening the file
	if not File then
		print('ReadFile: unable to open file at ' .. FilePath)
		return
	end	
	 
  local Contents = {} 
   
  --Read File and write into array 'Contents'
	for Line in File:lines() do      
      table.insert(Contents, Line)    
	end
  
  --Close File
	File:close()
  
  --Delete file content
  --File = io.open(FilePath, "w")
  --File:write("")
  --File:close()

	return Contents
end

--function takes an array with file paths and returns
--a string which contains the filenames of the files 
--inside the array divided with a comma
function FilesToString(array)  
  local returnString = ""
  
  --for every file inside the array
  for i, file in ipairs(array) do
    --get file name from path
    fileName = string.match(file, '[^\\/]+$')
    --append to returnString
    if(i == 1) then 
    returnString = fileName
    elseif (i > 1) then
    returnString = returnString..', '..fileName
    end  
  end  
  return returnString
end
And that's how you can get a list of files from a folder to work with in lua. The example skin is attached below.

Edit: Updated this skin and post to make this procedure work with unicode.
You do not have the required permissions to view the files attached to this post.