machineconfig 7.79__py3-none-any.whl → 7.83__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/jobs/installer/custom/yazi.py +120 -0
- machineconfig/jobs/installer/custom_dev/nerdfont.py +1 -1
- machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +26 -12
- machineconfig/jobs/installer/custom_dev/sysabc.py +0 -5
- machineconfig/jobs/installer/installer_data.json +162 -94
- machineconfig/jobs/installer/powershell_scripts/install_fonts.ps1 +129 -34
- machineconfig/profile/create_helper.py +0 -12
- machineconfig/profile/mapper.toml +2 -2
- machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +1 -0
- machineconfig/scripts/python/croshell.py +4 -4
- machineconfig/scripts/python/env_manager/env_manager_tui.py +204 -0
- machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
- machineconfig/scripts/python/helpers_devops/cli_config.py +10 -0
- machineconfig/scripts/python/helpers_devops/cli_nw.py +15 -16
- machineconfig/scripts/python/helpers_devops/cli_self.py +4 -4
- machineconfig/scripts/python/helpers_devops/cli_terminal.py +2 -5
- machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfg +2 -2
- machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfg.ps1 +58 -1
- machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +1 -1
- machineconfig/scripts/python/helpers_repos/count_lines.py +40 -11
- machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
- machineconfig/scripts/python/helpers_utils/path.py +7 -4
- machineconfig/scripts/python/msearch.py +37 -7
- machineconfig/scripts/python/utils.py +3 -3
- machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
- machineconfig/settings/yazi/init.lua +4 -0
- machineconfig/settings/yazi/keymap_linux.toml +11 -4
- machineconfig/settings/yazi/theme.toml +4 -0
- machineconfig/settings/yazi/yazi_linux.toml +84 -0
- machineconfig/settings/yazi/yazi_windows.toml +58 -0
- machineconfig/setup_linux/web_shortcuts/interactive.sh +10 -10
- machineconfig/setup_windows/web_shortcuts/interactive.ps1 +10 -10
- machineconfig/utils/installer_utils/github_release_bulk.py +104 -62
- machineconfig/utils/installer_utils/install_from_url.py +122 -102
- machineconfig/utils/installer_utils/installer_class.py +15 -72
- machineconfig/utils/installer_utils/installer_cli.py +29 -44
- machineconfig/utils/installer_utils/installer_helper.py +100 -0
- machineconfig/utils/installer_utils/installer_runner.py +5 -8
- machineconfig/utils/ssh_utils/abc.py +2 -2
- machineconfig/utils/ssh_utils/wsl.py +44 -2
- {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/METADATA +2 -2
- {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/RECORD +45 -47
- machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfag +0 -17
- machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfrga +0 -21
- machineconfig/scripts/python/helpers_msearch/scripts_linux/skrg +0 -4
- machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfb.ps1 +0 -3
- machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfrga.bat +0 -20
- machineconfig/settings/yazi/yazi.toml +0 -17
- machineconfig/setup_linux/others/cli_installation.sh +0 -137
- {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/WHEEL +0 -0
- {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/entry_points.txt +0 -0
- {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,19 @@ $objShell = New-Object -ComObject Shell.Application
|
|
|
6
6
|
$objFolder = $objShell.Namespace($FONTS)
|
|
7
7
|
$Fontdir = Get-ChildItem -Path $Path -File
|
|
8
8
|
|
|
9
|
+
$invalidCharPattern = "[{0}]" -f ([Regex]::Escape(([string]::Join('', [System.IO.Path]::GetInvalidFileNameChars()))))
|
|
10
|
+
|
|
11
|
+
function Get-FontBaseNameSafe {
|
|
12
|
+
param([string]$Input)
|
|
13
|
+
if ([string]::IsNullOrWhiteSpace($Input)) { return "" }
|
|
14
|
+
$candidate = ($Input -split ',')[0].Trim()
|
|
15
|
+
try { return [System.IO.Path]::GetFileNameWithoutExtension($candidate) }
|
|
16
|
+
catch {
|
|
17
|
+
$sanitized = [Regex]::Replace($candidate, $invalidCharPattern, '')
|
|
18
|
+
return ($sanitized -replace '\\.[^.]+$')
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
# Normalization helper: remove spaces, underscores, hyphens, 'nerd', 'font', and collapse 'nf' for broad matching
|
|
10
23
|
function Normalize-FontName {
|
|
11
24
|
param([string]$Name)
|
|
@@ -17,47 +30,129 @@ function Normalize-FontName {
|
|
|
17
30
|
return $n
|
|
18
31
|
}
|
|
19
32
|
|
|
33
|
+
function Get-FontIdentityForms {
|
|
34
|
+
param([string]$Name)
|
|
35
|
+
if ([string]::IsNullOrWhiteSpace($Name)) { return @() }
|
|
36
|
+
$normalized = Normalize-FontName $Name
|
|
37
|
+
if ([string]::IsNullOrWhiteSpace($normalized)) { return @() }
|
|
38
|
+
|
|
39
|
+
$forms = @()
|
|
40
|
+
$forms += $normalized
|
|
41
|
+
|
|
42
|
+
$stylePattern = '(regular|italic|oblique|bold|bolditalic|semibold|semilight|light|extrabold|extralight|medium|thin|black|book|ultra|heavy|demi|retina|condensed|narrow|windowscompatible|complete|windowscomp|compatibility)+$'
|
|
43
|
+
$stem = $normalized
|
|
44
|
+
while ($stem -match $stylePattern) {
|
|
45
|
+
$stem = [Regex]::Replace($stem, $stylePattern, '')
|
|
46
|
+
if ([string]::IsNullOrWhiteSpace($stem)) { break }
|
|
47
|
+
$forms += $stem
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
$stemForVariants = $stem
|
|
51
|
+
foreach ($tail in @('mono', 'propo')) {
|
|
52
|
+
$tmp = $stemForVariants
|
|
53
|
+
while (-not [string]::IsNullOrWhiteSpace($tmp) -and $tmp.EndsWith($tail)) {
|
|
54
|
+
$tmp = $tmp.Substring(0, $tmp.Length - $tail.Length)
|
|
55
|
+
if ([string]::IsNullOrWhiteSpace($tmp)) { break }
|
|
56
|
+
$forms += $tmp
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return ($forms | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Test-FontFormsOverlapFuzzy {
|
|
64
|
+
param(
|
|
65
|
+
[string[]]$CandidateForms,
|
|
66
|
+
[string[]]$ExistingForms
|
|
67
|
+
)
|
|
68
|
+
foreach ($candidate in $CandidateForms) {
|
|
69
|
+
if ([string]::IsNullOrWhiteSpace($candidate)) { continue }
|
|
70
|
+
foreach ($existing in $ExistingForms) {
|
|
71
|
+
if ([string]::IsNullOrWhiteSpace($existing)) { continue }
|
|
72
|
+
if ($existing.Contains($candidate) -or $candidate.Contains($existing)) { return $true }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return $false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function Merge-UniqueForms {
|
|
79
|
+
param(
|
|
80
|
+
[string[]]$Existing,
|
|
81
|
+
[string[]]$Additional
|
|
82
|
+
)
|
|
83
|
+
return (($Existing + $Additional) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
|
|
84
|
+
}
|
|
85
|
+
|
|
20
86
|
# Cache installed font basenames once (raw + normalized map)
|
|
21
87
|
$installedFontFiles = Get-ChildItem C:\Windows\Fonts -File
|
|
22
|
-
$installedFonts = @
|
|
23
|
-
foreach ($f in $installedFontFiles) {
|
|
88
|
+
$installedFonts = @()
|
|
89
|
+
foreach ($f in $installedFontFiles) {
|
|
90
|
+
$installedFonts = Merge-UniqueForms $installedFonts (Get-FontIdentityForms $f.BaseName)
|
|
91
|
+
}
|
|
24
92
|
|
|
25
|
-
|
|
93
|
+
$relatedInstalled = $installedFonts | Where-Object { $_ -match 'caskaydiacove|cascadiacode' } | Sort-Object | Get-Unique
|
|
94
|
+
Write-Host "Existing related fonts detected:" $relatedInstalled -ForegroundColor DarkGray
|
|
95
|
+
|
|
96
|
+
$registryFonts = @()
|
|
97
|
+
$fontReg = $null
|
|
98
|
+
try {
|
|
99
|
+
$fontReg = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' -ErrorAction Stop
|
|
100
|
+
} catch {
|
|
101
|
+
Write-Host "Registry font list unavailable: $($_.Exception.Message)" -ForegroundColor DarkYellow
|
|
102
|
+
}
|
|
103
|
+
if ($null -ne $fontReg) {
|
|
104
|
+
foreach ($prop in $fontReg.PSObject.Properties) {
|
|
105
|
+
$val = ($prop.Value | Out-String).Trim()
|
|
106
|
+
$nm = ($prop.Name | Out-String).Trim()
|
|
107
|
+
$registryFonts = Merge-UniqueForms $registryFonts (Get-FontIdentityForms (Get-FontBaseNameSafe $val))
|
|
108
|
+
$registryFonts = Merge-UniqueForms $registryFonts (Get-FontIdentityForms (Get-FontBaseNameSafe $nm))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
26
111
|
|
|
27
112
|
foreach ($File in $Fontdir) {
|
|
28
|
-
if ($File.Name -
|
|
29
|
-
$candidateRaw = $File.BaseName
|
|
30
|
-
$candidateNorm = Normalize-FontName $candidateRaw
|
|
31
|
-
|
|
32
|
-
# 1. Exact file existence check (handles .ttf/.otf pairs) before invoking Shell CopyHere.
|
|
33
|
-
$destFile = Join-Path -Path 'C:\Windows\Fonts' -ChildPath $File.Name
|
|
34
|
-
if (Test-Path -LiteralPath $destFile) { Write-Host "Skip (file exists) $($File.Name)" -ForegroundColor Green; continue }
|
|
35
|
-
|
|
36
|
-
# 2. Registry check: Fonts are registered under HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts
|
|
37
|
-
try {
|
|
38
|
-
$fontReg = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' -ErrorAction Stop
|
|
39
|
-
$regMatch = $false
|
|
40
|
-
foreach ($prop in $fontReg.PSObject.Properties) {
|
|
41
|
-
$val = ($prop.Value | Out-String).Trim()
|
|
42
|
-
$nm = ($prop.Name | Out-String).Trim()
|
|
43
|
-
$valNorm = Normalize-FontName ( [System.IO.Path]::GetFileNameWithoutExtension($val) )
|
|
44
|
-
$nmNorm = Normalize-FontName $nm
|
|
45
|
-
if ($valNorm -eq $candidateNorm -or $nmNorm -eq $candidateNorm -or $nmNorm -match [Regex]::Escape($candidateNorm)) { $regMatch = $true; break }
|
|
46
|
-
}
|
|
47
|
-
if ($regMatch) {
|
|
48
|
-
Write-Host "Skip (registry) $($File.Name)" -ForegroundColor Green
|
|
49
|
-
continue
|
|
50
|
-
}
|
|
51
|
-
} catch {
|
|
52
|
-
Write-Host "Registry font query failed: $($_.Exception.Message) (continuing)" -ForegroundColor DarkYellow
|
|
53
|
-
}
|
|
113
|
+
if ($File.Name -match 'pfb$') { continue }
|
|
54
114
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
115
|
+
$candidateForms = Get-FontIdentityForms $File.BaseName
|
|
116
|
+
if ($candidateForms.Count -eq 0) { $candidateForms = @(Normalize-FontName $File.BaseName) }
|
|
117
|
+
$candidateNorm = if ($candidateForms.Count -gt 0) { $candidateForms[0] } else { '' }
|
|
58
118
|
|
|
59
|
-
|
|
60
|
-
|
|
119
|
+
# 1. Exact file existence check (handles .ttf/.otf pairs) before invoking Shell CopyHere.
|
|
120
|
+
$destFile = Join-Path -Path 'C:\Windows\Fonts' -ChildPath $File.Name
|
|
121
|
+
if (Test-Path -LiteralPath $destFile) {
|
|
122
|
+
Write-Host "Skip (file exists) $($File.Name)" -ForegroundColor Green
|
|
123
|
+
$installedFonts = Merge-UniqueForms $installedFonts $candidateForms
|
|
124
|
+
continue
|
|
61
125
|
}
|
|
126
|
+
|
|
127
|
+
# 2. Registry check: Fonts are registered under HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts
|
|
128
|
+
if ($registryFonts.Count -gt 0) {
|
|
129
|
+
$exactRegMatch = $candidateForms | Where-Object { $registryFonts -contains $_ }
|
|
130
|
+
if ($exactRegMatch.Count -gt 0) {
|
|
131
|
+
Write-Host "Skip (registry) $($File.Name)" -ForegroundColor Green
|
|
132
|
+
$installedFonts = Merge-UniqueForms $installedFonts $candidateForms
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (Test-FontFormsOverlapFuzzy $candidateForms $registryFonts) {
|
|
136
|
+
Write-Host "Skip (registry family) $($File.Name)" -ForegroundColor Green
|
|
137
|
+
$installedFonts = Merge-UniqueForms $installedFonts $candidateForms
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# 3. Original heuristic set: in-memory list
|
|
143
|
+
if (($candidateForms | Where-Object { $installedFonts -contains $_ }).Count -gt 0) {
|
|
144
|
+
Write-Host "Skip (installed map) $($File.Name)" -ForegroundColor Green
|
|
145
|
+
$installedFonts = Merge-UniqueForms $installedFonts $candidateForms
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (Test-FontFormsOverlapFuzzy $candidateForms $installedFonts) {
|
|
149
|
+
Write-Host "Skip (installed family) $($File.Name)" -ForegroundColor Green
|
|
150
|
+
$installedFonts = Merge-UniqueForms $installedFonts $candidateForms
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
Write-Host "Installing font (no matches) $($File.Name) | norm=$candidateNorm" -ForegroundColor Yellow
|
|
155
|
+
$objFolder.CopyHere($File.FullName)
|
|
156
|
+
$installedFonts = Merge-UniqueForms $installedFonts $candidateForms
|
|
62
157
|
}
|
|
63
158
|
Write-Host "Font installation script completed." -ForegroundColor Cyan
|
|
@@ -60,15 +60,3 @@ def copy_assets_to_machine(which: Literal["scripts", "settings"]) -> None:
|
|
|
60
60
|
scripts_path = CONFIG_ROOT.joinpath("scripts")
|
|
61
61
|
subprocess.run(f"chmod +x {scripts_path} -R", shell=True, capture_output=True, text=True, check=False)
|
|
62
62
|
console.print("[green]✅ Script permissions updated[/green]")
|
|
63
|
-
|
|
64
|
-
home_dir = Path.home()
|
|
65
|
-
if system_name == "windows":
|
|
66
|
-
yazi_plugins_dir = home_dir.joinpath("AppData", "Roaming", "yazi", "config")
|
|
67
|
-
else:
|
|
68
|
-
yazi_plugins_dir = home_dir.joinpath(".config", "yazi")
|
|
69
|
-
|
|
70
|
-
yazi_plugins_path = yazi_plugins_dir.joinpath("plugins")
|
|
71
|
-
if not yazi_plugins_path.exists():
|
|
72
|
-
yazi_plugins_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
-
import git
|
|
74
|
-
git.Repo.clone_from("https://github.com/yazi-rs/plugins", yazi_plugins_path)
|
|
@@ -94,7 +94,7 @@ config_again = {this = '~/.config/procs/config.toml', to_this = 'CONFIG_ROOT/set
|
|
|
94
94
|
config = {this = '~/.config/rofi/config.rasi', to_this = 'CONFIG_ROOT/settings/rofi/config.rasi'}
|
|
95
95
|
|
|
96
96
|
[yazi_windows]
|
|
97
|
-
yazi = {this = '~/AppData/Roaming/yazi/config/yazi.toml', to_this = 'CONFIG_ROOT/settings/yazi/
|
|
97
|
+
yazi = {this = '~/AppData/Roaming/yazi/config/yazi.toml', to_this = 'CONFIG_ROOT/settings/yazi/yazi_windows.toml'}
|
|
98
98
|
keymap = {this = '~/AppData/Roaming/yazi/config/keymap.toml', to_this = 'CONFIG_ROOT/settings/yazi/keymap_windows.toml'}
|
|
99
99
|
theme = {this = '~/AppData/Roaming/yazi/config/theme.toml', to_this = 'CONFIG_ROOT/settings/yazi/theme.toml'}
|
|
100
100
|
init = {this = '~/AppData/Roaming/yazi/config/init.lua', to_this = 'CONFIG_ROOT/settings/yazi/init.lua'}
|
|
@@ -102,7 +102,7 @@ init = {this = '~/AppData/Roaming/yazi/config/init.lua', to_this = 'CONFIG_ROOT/
|
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
[yazi_linux]
|
|
105
|
-
yazi = {this = '~/.config/yazi/yazi.toml', to_this = 'CONFIG_ROOT/settings/yazi/
|
|
105
|
+
yazi = {this = '~/.config/yazi/yazi.toml', to_this = 'CONFIG_ROOT/settings/yazi/yazi_linux.toml'}
|
|
106
106
|
keymap = {this = '~/.config/yazi/keymap.toml', to_this = 'CONFIG_ROOT/settings/yazi/keymap_linux.toml'}
|
|
107
107
|
theme = {this = '~/.config/yazi/theme.toml', to_this = 'CONFIG_ROOT/settings/yazi/theme.toml'}
|
|
108
108
|
init = {this = '~/.config/yazi/init.lua', to_this = 'CONFIG_ROOT/settings/yazi/init.lua'}
|
|
@@ -40,6 +40,7 @@ applyTo: "**/*.py"
|
|
|
40
40
|
* Make sure all the code is rigorous, no lazy stuff.
|
|
41
41
|
* For example, always avoid default values in arguments of functions. Those are evil and cause confusion. Always be explicit in parameter passing.
|
|
42
42
|
* Please never ever attempt to change code files by writing meta code to do string manipulation on files, e.g. with `sed` command with terminal. Please do change the files one by one, no matter how many there is. Don't worry about time, nor context window size, its okay, take your time and do the legwork. You can stop in the middle and we will have another LLM to help with the rest.
|
|
43
|
+
* Your code is minimal, no unrequested features, no bloat.
|
|
43
44
|
* Please avoid writing README files and avoid docstring and comments in code unless absolutely necessary. Use clear naming conventions instead of documenting.
|
|
44
45
|
* Always prefer to functional style of programming over OOP.
|
|
45
46
|
* When passing arguments or constructing dicts or lists or tuples, avoid breaking lines too much, try to use ~ 150 characters per line before breaking to new one.
|
|
@@ -112,7 +112,7 @@ def croshell(
|
|
|
112
112
|
fire_line = f"uv run --python 3.14 {user_uv_with_line}--with visidata,pyarrow vd {str(file_obj)}"
|
|
113
113
|
elif marimo:
|
|
114
114
|
if Path.home().joinpath("code/machineconfig").exists(): requirements = f"""{user_uv_with_line} --with marimo --project "{str(Path.home().joinpath("code/machineconfig"))}" """
|
|
115
|
-
else: requirements = f"""--python 3.14 {user_uv_with_line} user_uv_with_line--with "marimo,cowsay,machineconfig[plot]>=7.
|
|
115
|
+
else: requirements = f"""--python 3.14 {user_uv_with_line} user_uv_with_line--with "marimo,cowsay,machineconfig[plot]>=7.83" """
|
|
116
116
|
fire_line = f"""
|
|
117
117
|
cd {str(pyfile.parent)}
|
|
118
118
|
uv run --python 3.14 --with "marimo" marimo convert {pyfile.name} -o marimo_nb.py
|
|
@@ -120,7 +120,7 @@ uv run {requirements} marimo edit --host 0.0.0.0 marimo_nb.py
|
|
|
120
120
|
"""
|
|
121
121
|
elif jupyter:
|
|
122
122
|
if Path.home().joinpath("code/machineconfig").exists(): requirements = f"""{user_uv_with_line} --with jupyterlab --project "{str(Path.home().joinpath("code/machineconfig"))}" """
|
|
123
|
-
else: requirements = f"""{user_uv_with_line} --with "cowsay,machineconfig[plot]>=7.
|
|
123
|
+
else: requirements = f"""{user_uv_with_line} --with "cowsay,machineconfig[plot]>=7.83" """
|
|
124
124
|
fire_line = f"uv run {requirements} jupyter-lab {str(nb_target)}"
|
|
125
125
|
elif vscode:
|
|
126
126
|
user_uv_add = f"uv add {uv_with}" if uv_with is not None else ""
|
|
@@ -128,7 +128,7 @@ uv run {requirements} marimo edit --host 0.0.0.0 marimo_nb.py
|
|
|
128
128
|
cd {str(pyfile.parent)}
|
|
129
129
|
uv init --python 3.14
|
|
130
130
|
uv venv
|
|
131
|
-
uv add "cowsay,machineconfig[plot]>=7.
|
|
131
|
+
uv add "cowsay,machineconfig[plot]>=7.83"
|
|
132
132
|
uv add {user_uv_add}
|
|
133
133
|
# code serve-web
|
|
134
134
|
code --new-window {str(pyfile)}
|
|
@@ -137,7 +137,7 @@ code --new-window {str(pyfile)}
|
|
|
137
137
|
if interpreter == "ipython": profile = f" --profile {ipython_profile} --no-banner"
|
|
138
138
|
else: profile = ""
|
|
139
139
|
if Path.home().joinpath("code/machineconfig").exists(): ve_line = f"""{user_uv_with_line} --project "{str(Path.home().joinpath("code/machineconfig"))}" """
|
|
140
|
-
else: ve_line = f"""--python 3.14 {user_uv_with_line} --with "cowsay,machineconfig[plot]>=7.
|
|
140
|
+
else: ve_line = f"""--python 3.14 {user_uv_with_line} --with "cowsay,machineconfig[plot]>=7.83" """
|
|
141
141
|
fire_line = f"uv run {ve_line} {interpreter} {interactivity} {profile} {str(pyfile)}"
|
|
142
142
|
|
|
143
143
|
from machineconfig.utils.code import exit_then_run_shell_script
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.13"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "machineconfig>=7.83",
|
|
6
|
+
# "textual",
|
|
7
|
+
# "pyperclip",
|
|
8
|
+
# ]
|
|
9
|
+
# ///
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from typing import Final
|
|
17
|
+
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
from textual import on
|
|
20
|
+
from textual.app import App, ComposeResult
|
|
21
|
+
from textual.binding import Binding
|
|
22
|
+
from textual.containers import Horizontal, Vertical
|
|
23
|
+
from textual.widgets import Footer, Header, Label, ListItem, ListView, Static
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
VALUE_PREVIEW_LIMIT: Final[int] = 4096
|
|
27
|
+
SUMMARY_LIMIT: Final[int] = 96
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def truncate_text(text: str, limit: int) -> tuple[str, int]:
|
|
31
|
+
length = len(text)
|
|
32
|
+
if length <= limit:
|
|
33
|
+
return text, 0
|
|
34
|
+
return text[:limit], length - limit
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_summary(env_key: str, env_value: str, limit: int) -> str:
|
|
38
|
+
sanitized = env_value.replace("\n", "\\n").replace("\t", "\\t")
|
|
39
|
+
preview, remainder = truncate_text(sanitized, limit)
|
|
40
|
+
if preview == "":
|
|
41
|
+
base = f"{env_key} = <empty>"
|
|
42
|
+
else:
|
|
43
|
+
base = f"{env_key} = {preview}"
|
|
44
|
+
if remainder == 0:
|
|
45
|
+
return base
|
|
46
|
+
return f"{base}... (+{remainder} chars)"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def collect_environment(env: Mapping[str, str]) -> list[tuple[str, str]]:
|
|
50
|
+
return sorted(env.items(), key=lambda pair: pair[0].lower())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EnvListItem(ListItem):
|
|
54
|
+
def __init__(self, env_key: str, summary: str) -> None:
|
|
55
|
+
super().__init__(Label(summary))
|
|
56
|
+
self._env_key = env_key
|
|
57
|
+
|
|
58
|
+
def env_key(self) -> str:
|
|
59
|
+
return self._env_key
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class EnvValuePreview(Static):
|
|
63
|
+
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
|
64
|
+
super().__init__(*args, **kwargs)
|
|
65
|
+
self.border_title = "Environment Value"
|
|
66
|
+
|
|
67
|
+
def show_value(self, env_key: str, env_value: str) -> None:
|
|
68
|
+
preview, remainder = truncate_text(env_value, VALUE_PREVIEW_LIMIT)
|
|
69
|
+
text = Text()
|
|
70
|
+
text.append(f"{env_key}\n\n", style="bold cyan")
|
|
71
|
+
if preview == "":
|
|
72
|
+
text.append("<empty>", style="dim")
|
|
73
|
+
else:
|
|
74
|
+
text.append(preview)
|
|
75
|
+
if remainder > 0:
|
|
76
|
+
text.append(f"\n... truncated {remainder} characters", style="yellow")
|
|
77
|
+
self.update(text)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class StatusBar(Static):
|
|
81
|
+
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
|
82
|
+
super().__init__(*args, **kwargs)
|
|
83
|
+
self.border_title = "Status"
|
|
84
|
+
|
|
85
|
+
def show_message(self, message: str, level: str) -> None:
|
|
86
|
+
palette = {
|
|
87
|
+
"info": "cyan",
|
|
88
|
+
"success": "green",
|
|
89
|
+
"warning": "yellow",
|
|
90
|
+
"error": "red",
|
|
91
|
+
}
|
|
92
|
+
color = palette.get(level, "white")
|
|
93
|
+
self.update(f"[{color}]{message}[/{color}]")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class EnvExplorerApp(App[None]):
|
|
97
|
+
CSS = """
|
|
98
|
+
Screen { background: $surface; }
|
|
99
|
+
Header { background: $primary; color: $text; }
|
|
100
|
+
Footer { background: $panel; }
|
|
101
|
+
#main-container { height: 100%; }
|
|
102
|
+
#left-panel { width: 50%; height: 100%; border: solid $primary; padding: 1; }
|
|
103
|
+
#right-panel { width: 50%; height: 100%; border: solid $accent; padding: 1; }
|
|
104
|
+
ListView { height: 1fr; border: solid $accent; background: $surface; }
|
|
105
|
+
ListView > ListItem { padding: 0 1; }
|
|
106
|
+
EnvValuePreview { height: 1fr; border: solid $primary; background: $surface; padding: 1; overflow-y: auto; }
|
|
107
|
+
StatusBar { height: 3; border: solid $success; background: $surface; padding: 1; }
|
|
108
|
+
Label { padding: 0 1; height: auto; }
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
BINDINGS = [
|
|
112
|
+
Binding("q", "quit", "Quit", show=True),
|
|
113
|
+
Binding("r", "refresh", "Refresh", show=True),
|
|
114
|
+
Binding("c", "copy_entry", "Copy", show=True),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
def __init__(self) -> None:
|
|
118
|
+
super().__init__()
|
|
119
|
+
self._env_pairs: list[tuple[str, str]] = []
|
|
120
|
+
self._env_lookup: dict[str, str] = {}
|
|
121
|
+
self._selected_key: str = ""
|
|
122
|
+
|
|
123
|
+
def compose(self) -> ComposeResult:
|
|
124
|
+
platform_name = platform.system()
|
|
125
|
+
yield Header(show_clock=True)
|
|
126
|
+
with Horizontal(id="main-container"):
|
|
127
|
+
with Vertical(id="left-panel"):
|
|
128
|
+
yield Label(f"🌐 Environment Variables ({platform_name})")
|
|
129
|
+
yield ListView(id="env-list")
|
|
130
|
+
with Vertical(id="right-panel"):
|
|
131
|
+
yield EnvValuePreview(id="preview")
|
|
132
|
+
yield StatusBar(id="status")
|
|
133
|
+
yield Footer()
|
|
134
|
+
|
|
135
|
+
def on_mount(self) -> None:
|
|
136
|
+
self.title = "Environment Explorer"
|
|
137
|
+
self.sub_title = f"Platform: {platform.system()}"
|
|
138
|
+
self._reload_environment()
|
|
139
|
+
self._status().show_message("Ready. Select a variable to preview its value.", "info")
|
|
140
|
+
|
|
141
|
+
def _reload_environment(self) -> None:
|
|
142
|
+
self._env_pairs = collect_environment(os.environ)
|
|
143
|
+
self._env_lookup = dict(self._env_pairs)
|
|
144
|
+
self._populate_list()
|
|
145
|
+
|
|
146
|
+
def _populate_list(self) -> None:
|
|
147
|
+
list_view = self.query_one("#env-list", ListView)
|
|
148
|
+
list_view.clear()
|
|
149
|
+
for env_key, env_value in self._env_pairs:
|
|
150
|
+
summary = format_summary(env_key, env_value, SUMMARY_LIMIT)
|
|
151
|
+
list_view.append(EnvListItem(env_key, summary))
|
|
152
|
+
self._status().show_message(f"Loaded {len(self._env_pairs)} environment variables.", "success")
|
|
153
|
+
|
|
154
|
+
def _status(self) -> StatusBar:
|
|
155
|
+
return self.query_one("#status", StatusBar)
|
|
156
|
+
|
|
157
|
+
def _preview(self) -> EnvValuePreview:
|
|
158
|
+
return self.query_one("#preview", EnvValuePreview)
|
|
159
|
+
|
|
160
|
+
@on(ListView.Highlighted)
|
|
161
|
+
def handle_highlight(self, event: ListView.Highlighted) -> None:
|
|
162
|
+
if not isinstance(event.item, EnvListItem):
|
|
163
|
+
return
|
|
164
|
+
env_key = event.item.env_key()
|
|
165
|
+
env_value = self._env_lookup.get(env_key, "")
|
|
166
|
+
self._preview().show_value(env_key, env_value)
|
|
167
|
+
self._status().show_message(f"Previewing {env_key}", "info")
|
|
168
|
+
|
|
169
|
+
@on(ListView.Selected)
|
|
170
|
+
def handle_selection(self, event: ListView.Selected) -> None:
|
|
171
|
+
if not isinstance(event.item, EnvListItem):
|
|
172
|
+
return
|
|
173
|
+
env_key = event.item.env_key()
|
|
174
|
+
self._selected_key = env_key
|
|
175
|
+
env_value = self._env_lookup.get(env_key, "")
|
|
176
|
+
self._preview().show_value(env_key, env_value)
|
|
177
|
+
self._status().show_message(f"Selected {env_key}", "success")
|
|
178
|
+
|
|
179
|
+
def action_refresh(self) -> None:
|
|
180
|
+
self._reload_environment()
|
|
181
|
+
self._status().show_message("Environment reloaded.", "success")
|
|
182
|
+
|
|
183
|
+
def action_copy_entry(self) -> None:
|
|
184
|
+
if self._selected_key == "":
|
|
185
|
+
self._status().show_message("No variable selected.", "warning")
|
|
186
|
+
return
|
|
187
|
+
env_value = self._env_lookup.get(self._selected_key, "")
|
|
188
|
+
payload = f"{self._selected_key}={env_value}"
|
|
189
|
+
try:
|
|
190
|
+
import pyperclip # type: ignore[import]
|
|
191
|
+
|
|
192
|
+
pyperclip.copy(payload)
|
|
193
|
+
self._status().show_message(f"Copied {self._selected_key} to clipboard.", "success")
|
|
194
|
+
except ImportError:
|
|
195
|
+
self._status().show_message("pyperclip unavailable. Install it for clipboard support.", "warning")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def main() -> None:
|
|
199
|
+
app = EnvExplorerApp()
|
|
200
|
+
app.run()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
main()
|
|
@@ -75,6 +75,12 @@ def copy_assets(which: Annotated[Literal["scripts", "s", "settings", "t", "both"
|
|
|
75
75
|
typer.echo(f"[red]Error:[/] Unknown asset type: {which}")
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def link_wsl_and_windows_home():
|
|
79
|
+
"""🔗 Link WSL home and Windows home directories."""
|
|
80
|
+
import machineconfig.utils.ssh_utils.wsl as wsl_utils
|
|
81
|
+
wsl_utils.link_wsl_and_windows()
|
|
82
|
+
|
|
83
|
+
|
|
78
84
|
def get_app():
|
|
79
85
|
config_apps = typer.Typer(help="⚙️ [c] configuration subcommands", no_args_is_help=True, add_help_option=False, add_completion=False)
|
|
80
86
|
config_apps.command("private", no_args_is_help=True, help="🔗 [v] Manage private configuration files.")(create_links_export.main_private_from_parser)
|
|
@@ -92,4 +98,8 @@ def get_app():
|
|
|
92
98
|
|
|
93
99
|
config_apps.command("copy-assets", no_args_is_help=True, help="🔗 [c] Copy asset files from library to machine.", hidden=False)(copy_assets)
|
|
94
100
|
config_apps.command("c", no_args_is_help=True, help="Copy asset files from library to machine.", hidden=True)(copy_assets)
|
|
101
|
+
|
|
102
|
+
config_apps.command("link-wsl-windows", no_args_is_help=False, help="🔗 [l] Link WSL home and Windows home directories.", hidden=False)(link_wsl_and_windows_home)
|
|
103
|
+
config_apps.command("l", no_args_is_help=False, help="Link WSL home and Windows home directories.", hidden=True)(link_wsl_and_windows_home)
|
|
104
|
+
|
|
95
105
|
return config_apps
|
|
@@ -34,21 +34,20 @@ def add_ssh_identity():
|
|
|
34
34
|
helper.main()
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def show_address():
|
|
37
|
+
def show_address() -> None:
|
|
38
38
|
"""📌 Show this computer addresses on network"""
|
|
39
39
|
from machineconfig.utils.installer_utils.installer_cli import install_if_missing
|
|
40
|
-
|
|
41
|
-
from machineconfig.utils.accessories import randstr
|
|
42
|
-
tmp_file = Path.home().joinpath("tmp_results/tmp_files/ipinfo_result_" + randstr(8) + ".json")
|
|
43
|
-
tmp_file.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
import subprocess
|
|
44
41
|
install_if_missing("ipinfo")
|
|
45
|
-
|
|
46
|
-
ipinfo myip --json
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
["ipinfo", "myip", "--json"],
|
|
44
|
+
check=True,
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
)
|
|
50
49
|
import json
|
|
51
|
-
loaded_json = json.loads(
|
|
50
|
+
loaded_json = json.loads(result.stdout)
|
|
52
51
|
from rich import print_json
|
|
53
52
|
print_json(data=loaded_json)
|
|
54
53
|
|
|
@@ -176,8 +175,8 @@ sudo warp-cli connect
|
|
|
176
175
|
|
|
177
176
|
def get_app():
|
|
178
177
|
nw_apps = typer.Typer(help="🔐 [n] Network subcommands", no_args_is_help=True, add_help_option=False, add_completion=False)
|
|
179
|
-
nw_apps.command(name="share-terminal", help="📡 [t] Share terminal via web browser")(cli_terminal.
|
|
180
|
-
nw_apps.command(name="t", help="Share terminal via web browser", hidden=True)(cli_terminal.
|
|
178
|
+
nw_apps.command(name="share-terminal", help="📡 [t] Share terminal via web browser")(cli_terminal.share_terminal)
|
|
179
|
+
nw_apps.command(name="t", help="Share terminal via web browser", hidden=True)(cli_terminal.share_terminal)
|
|
181
180
|
|
|
182
181
|
nw_apps.command(name="share-server", help="🌐 [s] Start local/global server to share files/folders via web browser", no_args_is_help=True)(cli_share_server.web_file_explorer)
|
|
183
182
|
nw_apps.command(name="s", help="Start local/global server to share files/folders via web browser", hidden=True, no_args_is_help=True)(cli_share_server.web_file_explorer)
|
|
@@ -201,15 +200,15 @@ def get_app():
|
|
|
201
200
|
nw_apps.command(name="debug-ssh", help="🐛 [d] Debug SSH connection")(debug_ssh)
|
|
202
201
|
nw_apps.command(name="d", help="Debug SSH connection", hidden=True)(debug_ssh)
|
|
203
202
|
|
|
204
|
-
nw_apps.command(name="wifi-select", no_args_is_help=True, help="📶
|
|
203
|
+
nw_apps.command(name="wifi-select", no_args_is_help=True, help="📶 [w] WiFi connection utility.")(wifi_select)
|
|
205
204
|
nw_apps.command(name="w", no_args_is_help=True, hidden=True)(wifi_select)
|
|
206
205
|
|
|
207
206
|
nw_apps.command(name="bind-wsl-port", help="🔌 [b] Bind WSL port to Windows host", no_args_is_help=True)(bind_wsl_port)
|
|
208
207
|
nw_apps.command(name="b", help="Bind WSL port to Windows host", hidden=True, no_args_is_help=True)(bind_wsl_port)
|
|
209
208
|
|
|
210
|
-
nw_apps.command(name="reset-cloudflare-tunnel", help="☁️
|
|
209
|
+
nw_apps.command(name="reset-cloudflare-tunnel", help="☁️ [r] Reset Cloudflare tunnel service")(reset_cloudflare_tunnel)
|
|
211
210
|
nw_apps.command(name="r", help="Reset Cloudflare tunnel service", hidden=True)(reset_cloudflare_tunnel)
|
|
212
|
-
nw_apps.command(name="add-ip-exclusion-to-warp", help="🚫
|
|
211
|
+
nw_apps.command(name="add-ip-exclusion-to-warp", help="🚫 [p] Add IP exclusion to WARP")(add_ip_exclusion_to_warp)
|
|
213
212
|
nw_apps.command(name="p", help="Add IP exclusion to WARP", hidden=True)(add_ip_exclusion_to_warp)
|
|
214
213
|
|
|
215
214
|
return nw_apps
|
|
@@ -49,13 +49,13 @@ def install(no_copy_assets: Annotated[bool, typer.Option("--no-assets-copy", "-n
|
|
|
49
49
|
from machineconfig.utils.code import run_shell_script
|
|
50
50
|
from pathlib import Path
|
|
51
51
|
if Path.home().joinpath("code/machineconfig").exists():
|
|
52
|
-
run_shell_script(f"""$HOME/.local/bin/uv tool install --upgrade --editable "{str(Path.home().joinpath("code/machineconfig"))}" """)
|
|
52
|
+
run_shell_script(f""" "$HOME/.local/bin/uv" tool install --upgrade --editable "{str(Path.home().joinpath("code/machineconfig"))}" """)
|
|
53
53
|
else:
|
|
54
54
|
import platform
|
|
55
55
|
if platform.system() == "Windows":
|
|
56
|
-
run_shell_script(r"""& "$HOME\.local\bin\uv.exe" tool install --upgrade "machineconfig>=7.
|
|
56
|
+
run_shell_script(r"""& "$HOME\.local\bin\uv.exe" tool install --upgrade "machineconfig>=7.83" """)
|
|
57
57
|
else:
|
|
58
|
-
run_shell_script("""$HOME/.local/bin/uv tool install --upgrade "machineconfig>=7.
|
|
58
|
+
run_shell_script("""$HOME/.local/bin/uv tool install --upgrade "machineconfig>=7.83" """)
|
|
59
59
|
from machineconfig.profile.create_shell_profile import create_default_shell_profile
|
|
60
60
|
if not no_copy_assets:
|
|
61
61
|
create_default_shell_profile() # involves copying assets too
|
|
@@ -80,7 +80,7 @@ def navigate():
|
|
|
80
80
|
path = Path(navigator.__file__).resolve().parent.joinpath("devops_navigator.py")
|
|
81
81
|
from machineconfig.utils.code import exit_then_run_shell_script
|
|
82
82
|
if Path.home().joinpath("code/machineconfig").exists(): executable = f"""--project "{str(Path.home().joinpath("code/machineconfig"))}" --with textual"""
|
|
83
|
-
else: executable = """--with "machineconfig>=7.
|
|
83
|
+
else: executable = """--with "machineconfig>=7.83,textual" """
|
|
84
84
|
exit_then_run_shell_script(f"""uv run {executable} {path}""")
|
|
85
85
|
|
|
86
86
|
|
|
@@ -17,13 +17,11 @@ reference:
|
|
|
17
17
|
|
|
18
18
|
def display_terminal_url(local_ip_v4: str, port: int, protocol: str = "http") -> None:
|
|
19
19
|
"""Display a flashy, unmissable terminal URL announcement."""
|
|
20
|
-
|
|
21
20
|
from rich.console import Console
|
|
22
21
|
from rich.panel import Panel
|
|
23
22
|
from rich.text import Text
|
|
24
23
|
from rich.align import Align
|
|
25
24
|
console = Console()
|
|
26
|
-
|
|
27
25
|
# Create the main message with styling
|
|
28
26
|
url_text = Text(f"{protocol}://{local_ip_v4}:{port}", style="bold bright_cyan underline")
|
|
29
27
|
message = Text.assemble(
|
|
@@ -42,14 +40,13 @@ def display_terminal_url(local_ip_v4: str, port: int, protocol: str = "http") ->
|
|
|
42
40
|
padding=(1, 2),
|
|
43
41
|
expand=False
|
|
44
42
|
)
|
|
45
|
-
|
|
46
43
|
# Print with extra spacing and attention-grabbing elements
|
|
47
44
|
# console.print("\n" + "🔥" * 60 + "\n", style="bright_red bold")
|
|
48
45
|
console.print(panel)
|
|
49
46
|
# console.print("🔥" * 60 + "\n", style="bright_red bold")
|
|
50
47
|
|
|
51
48
|
|
|
52
|
-
def
|
|
49
|
+
def share_terminal(
|
|
53
50
|
port: Annotated[Optional[int], typer.Option("--port", "-p", help="Port to run the terminal server on (default: 7681)")] = None,
|
|
54
51
|
username: Annotated[Optional[str], typer.Option("--username", "-u", help="Username for terminal access (default: current user)")] = None,
|
|
55
52
|
password: Annotated[Optional[str], typer.Option("--password", "-w", help="Password for terminal access (default: from ~/dotfiles/creds/passwords/quick_password)")] = None,
|
|
@@ -150,7 +147,7 @@ def main(
|
|
|
150
147
|
|
|
151
148
|
|
|
152
149
|
def main_with_parser():
|
|
153
|
-
typer.run(
|
|
150
|
+
typer.run(share_terminal)
|
|
154
151
|
|
|
155
152
|
|
|
156
153
|
if __name__ == "__main__":
|
|
@@ -13,9 +13,9 @@ IFS=: read -ra selected < <(
|
|
|
13
13
|
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
|
14
14
|
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" \
|
|
15
15
|
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)" \
|
|
16
|
-
--prompt '1.
|
|
16
|
+
--prompt '1. ripgrep> ' \
|
|
17
17
|
--delimiter : \
|
|
18
|
-
--header '╱ CTRL-R (
|
|
18
|
+
--header '╱ CTRL-R (ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
|
19
19
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
|
20
20
|
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
|
21
21
|
)
|