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.

Files changed (52) hide show
  1. machineconfig/jobs/installer/custom/yazi.py +120 -0
  2. machineconfig/jobs/installer/custom_dev/nerdfont.py +1 -1
  3. machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +26 -12
  4. machineconfig/jobs/installer/custom_dev/sysabc.py +0 -5
  5. machineconfig/jobs/installer/installer_data.json +162 -94
  6. machineconfig/jobs/installer/powershell_scripts/install_fonts.ps1 +129 -34
  7. machineconfig/profile/create_helper.py +0 -12
  8. machineconfig/profile/mapper.toml +2 -2
  9. machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +1 -0
  10. machineconfig/scripts/python/croshell.py +4 -4
  11. machineconfig/scripts/python/env_manager/env_manager_tui.py +204 -0
  12. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  13. machineconfig/scripts/python/helpers_devops/cli_config.py +10 -0
  14. machineconfig/scripts/python/helpers_devops/cli_nw.py +15 -16
  15. machineconfig/scripts/python/helpers_devops/cli_self.py +4 -4
  16. machineconfig/scripts/python/helpers_devops/cli_terminal.py +2 -5
  17. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfg +2 -2
  18. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfg.ps1 +58 -1
  19. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +1 -1
  20. machineconfig/scripts/python/helpers_repos/count_lines.py +40 -11
  21. machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
  22. machineconfig/scripts/python/helpers_utils/path.py +7 -4
  23. machineconfig/scripts/python/msearch.py +37 -7
  24. machineconfig/scripts/python/utils.py +3 -3
  25. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  26. machineconfig/settings/yazi/init.lua +4 -0
  27. machineconfig/settings/yazi/keymap_linux.toml +11 -4
  28. machineconfig/settings/yazi/theme.toml +4 -0
  29. machineconfig/settings/yazi/yazi_linux.toml +84 -0
  30. machineconfig/settings/yazi/yazi_windows.toml +58 -0
  31. machineconfig/setup_linux/web_shortcuts/interactive.sh +10 -10
  32. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +10 -10
  33. machineconfig/utils/installer_utils/github_release_bulk.py +104 -62
  34. machineconfig/utils/installer_utils/install_from_url.py +122 -102
  35. machineconfig/utils/installer_utils/installer_class.py +15 -72
  36. machineconfig/utils/installer_utils/installer_cli.py +29 -44
  37. machineconfig/utils/installer_utils/installer_helper.py +100 -0
  38. machineconfig/utils/installer_utils/installer_runner.py +5 -8
  39. machineconfig/utils/ssh_utils/abc.py +2 -2
  40. machineconfig/utils/ssh_utils/wsl.py +44 -2
  41. {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/METADATA +2 -2
  42. {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/RECORD +45 -47
  43. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfag +0 -17
  44. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfrga +0 -21
  45. machineconfig/scripts/python/helpers_msearch/scripts_linux/skrg +0 -4
  46. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfb.ps1 +0 -3
  47. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfrga.bat +0 -20
  48. machineconfig/settings/yazi/yazi.toml +0 -17
  49. machineconfig/setup_linux/others/cli_installation.sh +0 -137
  50. {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/WHEEL +0 -0
  51. {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/entry_points.txt +0 -0
  52. {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) { $installedFonts[(Normalize-FontName $f.BaseName)] = 1 }
88
+ $installedFonts = @()
89
+ foreach ($f in $installedFontFiles) {
90
+ $installedFonts = Merge-UniqueForms $installedFonts (Get-FontIdentityForms $f.BaseName)
91
+ }
24
92
 
25
- Write-Host "Existing related fonts detected:" ($installedFonts.Keys | Where-Object { $_ -match 'caskaydiacove|cascadiacode' } | Sort-Object | Get-Unique) -ForegroundColor DarkGray
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 -notmatch 'pfb$') {
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
- # 3. Original heuristic set: in-memory list
56
- if ($installedFonts.ContainsKey($candidateNorm)) { Write-Host "Skip (norm map) $($File.Name)" -ForegroundColor Green; continue }
57
- if ($installedFonts.Keys | Where-Object { $_ -match [Regex]::Escape($candidateNorm) }) { Write-Host "Skip (norm regex) $($File.Name)" -ForegroundColor Green; continue }
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
- Write-Host "Installing font (no matches) $($File.Name) | norm=$candidateNorm" -ForegroundColor Yellow
60
- $objFolder.CopyHere($File.FullName)
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/yazi.toml'}
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/yazi.toml'}
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.79" """
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.79" """
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.79"
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.79" """
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()
@@ -2,7 +2,7 @@
2
2
  # /// script
3
3
  # requires-python = ">=3.13"
4
4
  # dependencies = [
5
- # "machineconfig>=7.79",
5
+ # "machineconfig>=7.83",
6
6
  # "textual",
7
7
  # "pyperclip",
8
8
  # ]
@@ -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
- from pathlib import Path
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
- script = f"""
46
- ipinfo myip --json > "{tmp_file.as_posix()}"
47
- """
48
- from machineconfig.utils.code import run_shell_script
49
- run_shell_script(script=script)
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(tmp_file.read_text(encoding="utf-8"))
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.main)
180
- nw_apps.command(name="t", help="Share terminal via web browser", hidden=True)(cli_terminal.main)
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="📶 [w] WiFi connection utility.")(wifi_select)
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="☁️ [r] Reset Cloudflare tunnel service")(reset_cloudflare_tunnel)
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="🚫 [p] Add IP exclusion to WARP")(add_ip_exclusion_to_warp)
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.79" """)
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.79" """)
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.79,textual" """
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 main(
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(main)
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. Ripgrep> ' \
16
+ --prompt '1. ripgrep> ' \
17
17
  --delimiter : \
18
- --header '╱ CTRL-R (Ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
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
  )