diff --git a/.github/workflows/tinyclips-arm64-validation.yml b/.github/workflows/tinyclips-arm64-validation.yml new file mode 100644 index 0000000000000..015870f9122e5 --- /dev/null +++ b/.github/workflows/tinyclips-arm64-validation.yml @@ -0,0 +1,89 @@ +name: Tiny Clips winget reproduction + +on: + push: + branches: + - diagnostics/tinyclips-arm64-validation + paths: + - .github/workflows/tinyclips-arm64-validation.yml + - Tools/TinyClipsValidation.ps1 + +permissions: + contents: read + +jobs: + reproduce: + if: github.repository == 'jamesmontemagno/winget-pkgs' + name: Standard-user launch (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + runner: windows-11-arm + - arch: x64 + runner: windows-latest + env: + ARCH: ${{ matrix.arch }} + steps: + - uses: actions/checkout@v6 + with: + sparse-checkout: | + Tools/TinyClipsValidation.ps1 + manifests/r/Refractored/TinyClips/1.8.2.0 + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Download and verify the submitted signed package + id: package + shell: pwsh + run: | + $manifest = Get-Content 'manifests\r\Refractored\TinyClips\1.8.2.0\Refractored.TinyClips.installer.yaml' -Raw + $block = [regex]::Match($manifest, "(?ms)^- Architecture: $env:ARCH\r?\n(?.*?)(?=^- Architecture:|\z)") + if (-not $block.Success) { throw "No $env:ARCH installer in the submitted manifest." } + $url = [regex]::Match($block.Groups['installer'].Value, '(?m)^\s+InstallerUrl:\s*(\S+)').Groups[1].Value + $expected = [regex]::Match($block.Groups['installer'].Value, '(?m)^\s+InstallerSha256:\s*([a-fA-F0-9]{64})').Groups[1].Value + if (-not $url -or -not $expected) { throw 'Installer URL or hash is missing.' } + Invoke-WebRequest -Uri $url -OutFile package.msix + if ((Get-FileHash package.msix -Algorithm SHA256).Hash -ine $expected) { throw 'Package SHA256 does not match the submitted manifest.' } + "Testing signed 1.8.2.0 $env:ARCH package; SHA256 verified." + + - name: Prepare the interactive runner desktop + shell: pwsh + run: | + $session = (Get-Process -Id $PID).SessionId + Get-Process | Where-Object { + $_.SessionId -eq $session -and $_.ProcessName -in @('msoobe', 'CloudExperienceHost', 'WWAHost') + } | ForEach-Object { Stop-Process -Id $_.Id -Force } + if (-not (Get-Process explorer -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $session })) { + Start-Process "$env:WINDIR\explorer.exe" + } + Start-Sleep -Seconds 15 + + - name: Install and exercise launches as a standard user + shell: pwsh + run: | + & .\Tools\TinyClipsValidation.ps1 -MsixPath (Resolve-Path package.msix).Path ` + -OutputDirectory (Join-Path $env:GITHUB_WORKSPACE 'diagnostics\standard-user') -WaitSeconds 60 + + - name: Runner-account control (not a standard-user result) + if: always() && steps.package.outcome == 'success' + shell: pwsh + run: | + & .\Tools\TinyClipsValidation.ps1 -StandardUser -RunnerControl ` + -MsixPath (Resolve-Path package.msix).Path ` + -OutputDirectory (Join-Path $env:GITHUB_WORKSPACE 'diagnostics\runner-control') -WaitSeconds 60 + + - name: Upload text diagnostics only + if: always() + uses: actions/upload-artifact@v7 + with: + name: tinyclips-validation-${{ matrix.arch }} + path: | + diagnostics/**/*.json + diagnostics/**/*.log + diagnostics/**/*.txt + if-no-files-found: warn + retention-days: 7 diff --git a/Tools/TinyClipsValidation.ps1 b/Tools/TinyClipsValidation.ps1 new file mode 100644 index 0000000000000..b4e670a3f7e7f --- /dev/null +++ b/Tools/TinyClipsValidation.ps1 @@ -0,0 +1,388 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $MsixPath, + + [Parameter(Mandatory)] + [string] $OutputDirectory, + + [ValidateRange(20, 120)] + [int] $WaitSeconds = 60, + + [switch] $StandardUser, + + [switch] $RunnerControl +) + +$ErrorActionPreference = 'Stop' +if ($env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted') { + throw 'Run this diagnostic only on a disposable GitHub-hosted runner.' +} +if ($RunnerControl -and -not $StandardUser) { throw 'RunnerControl is only valid for the worker mode.' } +New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null +if ($StandardUser) { + "Worker entered the script (PID $PID)." | Set-Content (Join-Path $OutputDirectory 'bootstrap.log') +} + +Add-Type -TypeDefinition @' +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +public static class TinyClipsLimitedProcess +{ + [StructLayout(LayoutKind.Sequential)] + private struct SidAndAttributes { public IntPtr Sid; public uint Attributes; } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct StartupInfo + { + public uint Size; + public string Reserved, Desktop, Title; + public uint X, Y, XSize, YSize, XCountChars, YCountChars, FillAttribute, Flags; + public ushort ShowWindow, ReservedSize; + public IntPtr ReservedPointer, Input, Output, Error; + } + + [StructLayout(LayoutKind.Sequential)] + private struct ProcessInformation + { + public IntPtr Process, Thread; + public uint ProcessId, ThreadId; + } + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token); + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool GetTokenInformation(IntPtr token, int kind, IntPtr data, uint size, out uint needed); + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool CreateProcessWithTokenW(IntPtr token, uint logonFlags, string application, + StringBuilder commandLine, uint flags, IntPtr environment, string directory, + ref StartupInfo startup, out ProcessInformation process); + [DllImport("kernel32.dll")] + private static extern IntPtr GetCurrentProcess(); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr handle); + private static void Check(bool success, string operation) + { + if (!success) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, operation + ": " + new Win32Exception(error).Message); + } + } + + public static uint IntegrityLevel() + { + IntPtr token = IntPtr.Zero, buffer = IntPtr.Zero; + try + { + Check(OpenProcessToken(GetCurrentProcess(), 0x8, out token), "OpenProcessToken(current worker)"); + uint size; + GetTokenInformation(token, 25, IntPtr.Zero, 0, out size); + if (size == 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); } + buffer = Marshal.AllocHGlobal(checked((int)size)); + Check(GetTokenInformation(token, 25, buffer, size, out size), "GetTokenInformation(TokenIntegrityLevel)"); + var sid = Marshal.PtrToStructure(buffer).Sid; + int count = Marshal.ReadByte(sid, 1); + return unchecked((uint)Marshal.ReadInt32(sid, 8 + (count - 1) * 4)); + } + finally + { + if (buffer != IntPtr.Zero) { Marshal.FreeHGlobal(buffer); } + if (token != IntPtr.Zero) { CloseHandle(token); } + } + } + + public static Process Start(string executable, string arguments, string directory) + { + IntPtr original = IntPtr.Zero, limited = IntPtr.Zero, linked = IntPtr.Zero, environment = IntPtr.Zero; + ProcessInformation process = default(ProcessInformation); + try + { + Check(OpenProcessToken(GetCurrentProcess(), 0x8, out original), "OpenProcessToken(controller)"); + linked = Marshal.AllocHGlobal(IntPtr.Size); + uint needed; + Check(GetTokenInformation(original, 19, linked, checked((uint)IntPtr.Size), out needed), "GetTokenInformation(TokenLinkedToken)"); + limited = Marshal.ReadIntPtr(linked); + var variables = new List(); + foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables()) + { + variables.Add(variable.Key + "=" + variable.Value); + } + variables.Sort(StringComparer.OrdinalIgnoreCase); + environment = Marshal.StringToHGlobalUni(string.Join("\0", variables) + "\0\0"); + var startup = new StartupInfo { Size = checked((uint)Marshal.SizeOf()), Flags = 1, ShowWindow = 0 }; + var commandLine = new StringBuilder("\"" + executable + "\" " + arguments); + Check(CreateProcessWithTokenW(limited, 1, executable, commandLine, 0x00000410, + environment, directory, ref startup, out process), "CreateProcessWithTokenW(non-admin worker)"); + return Process.GetProcessById(checked((int)process.ProcessId)); + } + finally + { + if (process.Thread != IntPtr.Zero) { CloseHandle(process.Thread); } + if (process.Process != IntPtr.Zero) { CloseHandle(process.Process); } + if (environment != IntPtr.Zero) { Marshal.FreeHGlobal(environment); } + if (linked != IntPtr.Zero) { Marshal.FreeHGlobal(linked); } + if (limited != IntPtr.Zero) { CloseHandle(limited); } + if (original != IntPtr.Zero) { CloseHandle(original); } + } + } +} +'@ +if ($StandardUser) { + 'Native interop compiled.' | Add-Content (Join-Path $OutputDirectory 'bootstrap.log') +} + +if (-not $StandardUser) { + $root = 'C:\TinyClipsValidation' + $process = $null + $rootCreated = $false + try { + if (Test-Path $root) { throw "Refusing to reuse existing directory $root." } + New-Item -ItemType Directory -Path $root | Out-Null + $rootCreated = $true + $workerOutput = Join-Path $root 'diagnostics' + New-Item -ItemType Directory -Path $workerOutput | Out-Null + Copy-Item $MsixPath (Join-Path $root 'package.msix') + Copy-Item $PSCommandPath (Join-Path $root 'worker.ps1') + $userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value + & icacls.exe $root /grant "*${userSid}:(OI)(CI)M" | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Unable to grant the validation user access to test files.' } + + $pwsh = (Get-Process -Id $PID).Path + $arguments = "-NoLogo -NoProfile -NonInteractive -STA -ExecutionPolicy Bypass -File `"$root\worker.ps1`" -StandardUser -MsixPath `"$root\package.msix`" -OutputDirectory `"$workerOutput`" -WaitSeconds $WaitSeconds" + # Use Windows' existing unelevated token rather than constructing a synthetic logon. + $process = [TinyClipsLimitedProcess]::Start($pwsh, $arguments, $root) + $null = $process.Handle + $readyDeadline = (Get-Date).AddSeconds(60) + while (-not (Test-Path (Join-Path $workerOutput 'bootstrap.log')) -and -not $process.HasExited -and (Get-Date) -lt $readyDeadline) { + Start-Sleep -Seconds 1 + } + if (-not (Test-Path (Join-Path $workerOutput 'bootstrap.log'))) { + throw 'The non-admin worker did not enter the script within 60 seconds.' + } + if (-not $process.WaitForExit(720000)) { throw 'Standard-user validation exceeded 12 minutes.' } + if (-not (Test-Path (Join-Path $workerOutput 'results.json'))) { + throw "Standard-user validation produced no results (exit code $($process.ExitCode))." + } + Get-Content (Join-Path $workerOutput 'results.json') + if ($process.ExitCode -ne 0) { throw "Standard-user validation failed (exit code $($process.ExitCode))." } + } + catch { + $_ | Out-String | Set-Content (Join-Path $OutputDirectory 'controller-error.txt') + $workerResultsExist = $rootCreated -and (Test-Path "$root\diagnostics\results.json") + if (-not $workerResultsExist) { + ConvertTo-Json -InputObject @([pscustomobject]@{ + AccountMode = 'standard-user' + Status = 'worker-unavailable' + AppLaunchTestExecuted = $false + Error = $_.ToString() + }) -Depth 5 | Set-Content (Join-Path $OutputDirectory 'results.json') + } + throw + } + finally { + if ($process -and -not $process.HasExited) { Stop-Process -Id $process.Id -Force } + if ($rootCreated -and (Test-Path "$root\diagnostics")) { + Copy-Item "$root\diagnostics\*" $OutputDirectory -Force + } + } + return +} + +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() +$principal = [Security.Principal.WindowsPrincipal]::new($identity) +$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if ($isAdmin -and -not $RunnerControl) { throw 'Worker must run as a non-elevated standard user.' } +$integrity = [TinyClipsLimitedProcess]::IntegrityLevel() +if (-not $RunnerControl -and $integrity -ne 8192) { throw "Expected medium integrity, observed $integrity." } + +$start = Get-Date +$results = [Collections.Generic.List[object]]::new() +$failure = $false +$mappedDrive = $false +$packageInstalled = $false +$ownedProcesses = [Collections.Generic.List[Diagnostics.Process]]::new() +$log = Join-Path $OutputDirectory 'worker.log' +Start-Transcript -Path $log | Out-Null + +try { + "Account mode: $(if ($RunnerControl) { 'runner-control' } else { 'standard-user' }); elevated: $isAdmin" + "Integrity level: $integrity; standard-user mode uses the existing desktop account with its Windows UAC-linked token." + "Process architecture: $([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" + "Session: $((Get-Process -Id $PID).SessionId); interactive: $([Environment]::UserInteractive)" + "This is a reproduction approximation, not Microsoft's private validation harness." + $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) + $workingDirectory = 'C:\TinyClipsValidation\cwd' + New-Item -ItemType Directory -Path $workingDirectory -Force | Out-Null + if (-not (Test-Path 'E:\')) { + & subst.exe E: $workingDirectory + if ($LASTEXITCODE -ne 0 -or -not (Test-Path 'E:\')) { throw 'Unable to provide E: for the working-directory scenarios.' } + $mappedDrive = $true + } + "E: drive: $(if ($mappedDrive) { 'user-scoped SUBST mapping' } else { 'existing runner volume' })" + + Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; +public static class TinyClipsValidationWindow +{ + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr window, int command); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr window); + + [ComImport, Guid("2E941141-7F97-4756-BA1D-9DECDE894A3D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IActivationManager + { + [PreserveSig] int ActivateApplication( + [MarshalAs(UnmanagedType.LPWStr)] string appId, + [MarshalAs(UnmanagedType.LPWStr)] string arguments, + uint options, out uint processId); + } + + public static uint Activate(string appId) + { + var type = Type.GetTypeFromCLSID(new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C"), true); + var manager = (IActivationManager)Activator.CreateInstance(type); + try + { + uint processId; + Marshal.ThrowExceptionForHR(manager.ActivateApplication(appId, null, 2, out processId)); + return processId; + } + finally { Marshal.ReleaseComObject(manager); } + } +} +'@ + + foreach ($scenario in @( + @{ Name = 'unrelated-directory'; Directory = $workingDirectory }, + @{ Name = 'e-root'; Directory = 'E:\' }, + @{ Name = 'e-drive-relative'; Directory = 'E:' }, + @{ Name = 'registered-activation-control'; Directory = $null } + )) { + $ownedProcesses.Clear() + $scenarioStart = Get-Date + try { + if (Get-AppxPackage -Name Refractored.TinyClips) { throw 'Expected a fresh validation account without Tiny Clips installed.' } + Add-AppxPackage -Path $MsixPath + $packageInstalled = $true + $package = Get-AppxPackage -Name Refractored.TinyClips + if (-not $package -or $package.Version -ne '1.8.2.0') { throw 'The submitted Tiny Clips version did not register.' } + "Scenario $($scenario.Name): $($package.PackageFullName); cwd=$($scenario.Directory)" + + $exe = Join-Path $package.InstallLocation 'TinyClips.App.exe' + $launchMethod = if ($scenario.Directory) { 'ShellExecute' } else { 'registered-AUMID' } + foreach ($launch in @('first', 'second')) { + if ($scenario.Directory) { + $info = [Diagnostics.ProcessStartInfo]::new() + $info.FileName = $exe + $info.WorkingDirectory = $scenario.Directory + $info.UseShellExecute = $true + $process = [Diagnostics.Process]::Start($info) + } + else { + $appId = "$($package.PackageFamilyName)!App" + $activatedPid = [TinyClipsValidationWindow]::Activate($appId) + $process = Get-Process -Id $activatedPid + } + $null = $process.Handle + $ownedProcesses.Add($process) + "Launched $launch instance; PID $($process.Id)" + Start-Sleep -Seconds 15 + $process.Refresh() + if (-not $process.HasExited -and $process.MainWindowHandle -ne [IntPtr]::Zero) { + [void][TinyClipsValidationWindow]::ShowWindow($process.MainWindowHandle, 5) + $foreground = [TinyClipsValidationWindow]::SetForegroundWindow($process.MainWindowHandle) + "Foreground request for $launch instance: $foreground" + } + } + $deadline = (Get-Date).AddSeconds($WaitSeconds) + while ((Get-Date) -lt $deadline) { + Start-Sleep -Seconds 2 + if (@($ownedProcesses | Where-Object { -not $_.HasExited }).Count -eq 0) { break } + } + for ($index = 0; $index -lt $ownedProcesses.Count; $index++) { + $process = $ownedProcesses[$index] + $process.Refresh() + $exited = $process.HasExited + $code = if ($exited) { $process.ExitCode } else { $null } + $status = if (-not $exited) { 'alive' } elseif ($index -eq 1 -and $code -eq 0) { 'clean-secondary-exit' } else { 'unexpected-exit' } + if ($status -eq 'unexpected-exit') { $failure = $true } + $results.Add([pscustomobject]@{ + Scenario = $scenario.Name + WorkingDirectory = $scenario.Directory + AccountMode = if ($RunnerControl) { 'runner-control' } else { 'standard-user' } + LaunchMethod = $launchMethod + Launch = if ($index -eq 0) { 'first' } else { 'second' } + Status = $status + ExitCode = $code + ExitCodeHex = if ($exited) { '0x{0:X8}' -f $code } else { $null } + ObservedSeconds = [int]((Get-Date) - $scenarioStart).TotalSeconds + MappedEDrive = $mappedDrive + }) + } + } + catch { + $failure = $true + $results.Add([pscustomobject]@{ + Scenario = $scenario.Name + AccountMode = if ($RunnerControl) { 'runner-control' } else { 'standard-user' } + Status = 'harness-or-install-error' + Error = $_.ToString() + }) + Write-Warning $_ + } + finally { + foreach ($process in $ownedProcesses) { + if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force } + $process.Dispose() + } + if ($packageInstalled) { + $package = Get-AppxPackage -Name Refractored.TinyClips + if ($package) { + foreach ($crashLog in @( + "$localAppData\TinyClips\Logs\crash.log", + "$localAppData\Packages\$($package.PackageFamilyName)\LocalCache\Local\TinyClips\Logs\crash.log" + )) { + if (Test-Path $crashLog) { + Get-Content $crashLog | Add-Content (Join-Path $OutputDirectory "$($scenario.Name)-crash.log") + } + } + Remove-AppxPackage -Package $package.PackageFullName + } + $packageInstalled = $false + } + } + } +} +catch { + $failure = $true + $results.Add([pscustomobject]@{ Status = 'harness-error'; Error = $_.ToString() }) + Write-Warning $_ +} +finally { + $eventErrors = @() + $events = Get-WinEvent -FilterHashtable @{ LogName = 'Application'; StartTime = $start } ` + -ErrorAction SilentlyContinue -ErrorVariable eventErrors + if ($eventErrors) { + $eventErrors | Out-String | Set-Content (Join-Path $OutputDirectory 'event-read-status.txt') + } + $events | Where-Object { + $_.ProviderName -in @('.NET Runtime', 'Application Error', 'Windows Error Reporting') -and + $_.Message -match 'TinyClips|Tiny Clips' + } | Select-Object TimeCreated, ProviderName, Id, Message | + Format-List | Out-String -Width 240 | Set-Content (Join-Path $OutputDirectory 'application-events.txt') + ConvertTo-Json -InputObject $results.ToArray() -Depth 5 | Set-Content (Join-Path $OutputDirectory 'results.json') + if ($mappedDrive) { + & subst.exe E: /D + if ($LASTEXITCODE -ne 0) { Write-Warning 'Unable to remove the test E: mapping.' } + } + Stop-Transcript | Out-Null +} + +if ($failure) { exit 1 } diff --git a/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.installer.yaml b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.installer.yaml new file mode 100644 index 0000000000000..150ce21f776a9 --- /dev/null +++ b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.installer.yaml @@ -0,0 +1,22 @@ +# Created using wingetcreate 1.12.13.0 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json + +PackageIdentifier: Refractored.TinyClips +PackageVersion: 1.8.2.0 +MinimumOSVersion: 10.0.22000.0 +InstallerType: msix +InstallModes: +- silent +- silentWithProgress +PackageFamilyName: Refractored.TinyClips_vmshqmcyy894t +Installers: +- Architecture: x64 + InstallerUrl: https://github.com/jamesmontemagno/tiny-clips/releases/download/v1.8.2-windows/TinyClips-1.8.2-x64.msix + InstallerSha256: bbec5fd75360b0f729a391f1172189c3f90ebab8d33198e1a01149b1a7d7734b + SignatureSha256: 9c121f71d4a6a3703cb94c576073fc1ee32d5b1d1a39206cef2e576e9ec03b46 +- Architecture: arm64 + InstallerUrl: https://github.com/jamesmontemagno/tiny-clips/releases/download/v1.8.2-windows/TinyClips-1.8.2-arm64.msix + InstallerSha256: 8a1f9818988415457c94c286273b53448dc79ef732fd2b7753b991a2cec4be1d + SignatureSha256: 58419077f732dee590db402897a375aaadc97ca3c49e2e093d554ecba26a9dc5 +ManifestType: installer +ManifestVersion: 1.12.0 diff --git a/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.locale.en-US.yaml b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.locale.en-US.yaml new file mode 100644 index 0000000000000..21b615628aeb6 --- /dev/null +++ b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.locale.en-US.yaml @@ -0,0 +1,28 @@ +# Created using wingetcreate 1.12.13.0 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.12.0.schema.json + +PackageIdentifier: Refractored.TinyClips +PackageVersion: 1.8.2.0 +PackageLocale: en-US +Publisher: Refractored LLC +PublisherUrl: https://github.com/jamesmontemagno +PublisherSupportUrl: https://github.com/jamesmontemagno/tiny-clips/issues +PrivacyUrl: https://tinyclips.app/privacy.html +Author: James Montemagno +PackageName: Tiny Clips +PackageUrl: https://github.com/jamesmontemagno/tiny-clips +License: MIT +LicenseUrl: https://github.com/jamesmontemagno/tiny-clips/blob/main/LICENSE +ShortDescription: Capture screenshots, video, and GIFs of any screen region from the system tray. +Description: |- + Tiny Clips is a lightweight system-tray app for quickly capturing screenshots, + screen recordings (MP4), and animated GIFs. Pick a region or the full screen, + use global hotkeys, and save straight to your Pictures/Videos folders. +Tags: +- screenshot +- screen-recorder +- gif +- capture +- productivity +ManifestType: defaultLocale +ManifestVersion: 1.12.0 diff --git a/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.yaml b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.yaml new file mode 100644 index 0000000000000..d4a40c72c5957 --- /dev/null +++ b/manifests/r/Refractored/TinyClips/1.8.2.0/Refractored.TinyClips.yaml @@ -0,0 +1,8 @@ +# Created using wingetcreate 1.12.13.0 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.12.0.schema.json + +PackageIdentifier: Refractored.TinyClips +PackageVersion: 1.8.2.0 +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.12.0