machineconfig 6.82__py3-none-any.whl → 7.98__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.
- machineconfig/cluster/remote/cloud_manager.py +1 -1
- machineconfig/cluster/sessions_managers/utils/maker.py +25 -13
- machineconfig/cluster/sessions_managers/wt_local.py +16 -221
- machineconfig/cluster/sessions_managers/wt_local_manager.py +55 -193
- machineconfig/cluster/sessions_managers/wt_remote_manager.py +42 -198
- machineconfig/cluster/sessions_managers/wt_utils/manager_persistence.py +52 -0
- machineconfig/cluster/sessions_managers/wt_utils/monitoring_helpers.py +50 -0
- machineconfig/cluster/sessions_managers/wt_utils/status_reporting.py +76 -0
- machineconfig/cluster/sessions_managers/wt_utils/wt_helpers.py +199 -0
- machineconfig/cluster/sessions_managers/zellij_local_manager.py +3 -1
- machineconfig/cluster/sessions_managers/zellij_remote_manager.py +3 -2
- machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +2 -2
- machineconfig/jobs/installer/custom/boxes.py +2 -2
- machineconfig/jobs/installer/custom/hx.py +75 -18
- machineconfig/jobs/installer/custom/yazi.py +119 -0
- machineconfig/jobs/installer/custom_dev/brave.py +5 -3
- machineconfig/jobs/installer/custom_dev/cloudflare_warp_cli.py +23 -0
- machineconfig/jobs/installer/custom_dev/code.py +4 -1
- machineconfig/jobs/installer/custom_dev/dubdb_adbc.py +1 -1
- machineconfig/jobs/installer/custom_dev/nerdfont.py +1 -1
- machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +27 -22
- machineconfig/jobs/installer/custom_dev/sysabc.py +139 -0
- machineconfig/jobs/installer/custom_dev/wezterm.py +2 -19
- machineconfig/jobs/installer/custom_dev/winget.py +10 -14
- machineconfig/jobs/installer/installer_data.json +1287 -216
- machineconfig/jobs/installer/linux_scripts/q.sh +10 -7
- machineconfig/jobs/installer/linux_scripts/redis.sh +1 -0
- machineconfig/jobs/installer/package_groups.py +58 -89
- machineconfig/jobs/installer/powershell_scripts/install_fonts.ps1 +129 -34
- machineconfig/logger.py +0 -1
- machineconfig/profile/create_helper.py +43 -16
- machineconfig/profile/create_links.py +2 -1
- machineconfig/profile/create_links_export.py +64 -18
- machineconfig/profile/create_shell_profile.py +78 -127
- machineconfig/profile/mapper.toml +15 -8
- machineconfig/scripts/__init__.py +0 -4
- machineconfig/scripts/linux/wrap_mcfg +46 -0
- machineconfig/scripts/nu/wrap_mcfg.nu +69 -0
- machineconfig/scripts/python/agents.py +52 -37
- machineconfig/scripts/python/ai/initai.py +1 -1
- machineconfig/scripts/python/ai/scripts/command_runner.ps1 +33 -0
- machineconfig/scripts/python/ai/{command_runner → scripts}/command_runner.sh +1 -1
- machineconfig/scripts/python/ai/solutions/copilot/{chatmodes/Thinking-Beast-Mode.chatmode.md → agents/Thinking-Beast-Mode.agent.md} +0 -1
- machineconfig/scripts/python/ai/solutions/copilot/{chatmodes/Ultimate-Transparent-Thinking-Beast-Mode.chatmode.md → agents/Ultimate-Transparent-Thinking-Beast-Mode.agent.md} +0 -1
- machineconfig/scripts/python/ai/solutions/copilot/{chatmodes/deepResearch.chatmode.md → agents/deepResearch.agent.md} +2 -2
- machineconfig/scripts/python/ai/solutions/copilot/github_copilot.py +5 -5
- machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +4 -0
- machineconfig/scripts/python/ai/solutions/copilot/instructions/python/watch_exec.prompt.md +20 -0
- machineconfig/scripts/python/ai/solutions/generic.py +1 -1
- machineconfig/scripts/python/ai/{generate_files.py → utils/generate_files.py} +2 -2
- machineconfig/scripts/python/ai/{vscode_tasks.py → utils/vscode_tasks.py} +7 -2
- machineconfig/scripts/python/croshell.py +77 -78
- machineconfig/scripts/python/devops.py +39 -21
- machineconfig/scripts/python/devops_navigator.py +0 -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/fire_jobs.py +84 -115
- machineconfig/scripts/python/ftpx.py +42 -16
- machineconfig/scripts/python/helpers/ast_search.py +74 -0
- machineconfig/scripts/python/helpers/qr_code.py +166 -0
- machineconfig/scripts/python/helpers/repo_rag.py +325 -0
- machineconfig/scripts/python/helpers/run_py_script.py +79 -0
- machineconfig/scripts/python/helpers/symantic_search.py +25 -0
- machineconfig/scripts/python/helpers/tmp_py_scripts/a.py +26 -0
- machineconfig/scripts/python/{helpers_fire → helpers_agents}/agentic_frameworks/fire_crush.json +1 -1
- machineconfig/scripts/python/helpers_agents/agentic_frameworks/fire_crush.py +39 -0
- machineconfig/scripts/python/{helpers_fire → helpers_agents}/agentic_frameworks/fire_cursor_agents.py +3 -4
- machineconfig/scripts/python/helpers_agents/agentic_frameworks/fire_gemini.py +55 -0
- machineconfig/scripts/python/helpers_agents/agentic_frameworks/fire_qwen.py +30 -0
- machineconfig/scripts/python/{helpers_fire → helpers_agents}/fire_agents_help_launch.py +32 -13
- machineconfig/scripts/python/{helpers_fire → helpers_agents}/fire_agents_helper_types.py +11 -14
- machineconfig/scripts/python/helpers_agents/templates/prompt.txt +10 -0
- machineconfig/scripts/python/helpers_agents/templates/template.sh +32 -0
- machineconfig/scripts/python/helpers_cloud/cloud_copy.py +28 -21
- machineconfig/scripts/python/helpers_cloud/cloud_helpers.py +1 -1
- machineconfig/scripts/python/helpers_cloud/cloud_mount.py +19 -17
- machineconfig/scripts/python/helpers_cloud/cloud_sync.py +8 -7
- machineconfig/scripts/python/helpers_croshell/crosh.py +3 -3
- machineconfig/scripts/python/helpers_croshell/start_slidev.py +6 -7
- machineconfig/scripts/python/helpers_devops/cli_config.py +46 -61
- machineconfig/scripts/python/helpers_devops/cli_config_dotfile.py +67 -55
- machineconfig/scripts/python/helpers_devops/cli_nw.py +157 -16
- machineconfig/scripts/python/helpers_devops/cli_repos.py +55 -21
- machineconfig/scripts/python/helpers_devops/cli_self.py +98 -48
- machineconfig/scripts/python/helpers_devops/cli_share_file.py +137 -0
- machineconfig/scripts/python/helpers_devops/cli_share_server.py +80 -42
- machineconfig/scripts/python/helpers_devops/{cli_terminal.py → cli_share_terminal.py} +15 -17
- machineconfig/scripts/python/helpers_devops/cli_utils.py +3 -128
- machineconfig/scripts/python/helpers_devops/devops_backup_retrieve.py +4 -4
- machineconfig/scripts/python/helpers_devops/devops_status.py +7 -19
- machineconfig/scripts/python/helpers_devops/themes/choose_wezterm_theme.py +1 -1
- machineconfig/scripts/python/{helpers_fire/helpers4.py → helpers_fire_command/file_wrangler.py} +56 -20
- machineconfig/scripts/python/helpers_fire_command/fire_jobs_route_helper.py +26 -16
- machineconfig/scripts/python/helpers_msearch/__init__.py +5 -0
- machineconfig/scripts/{linux → python/helpers_msearch/scripts_linux}/fzfg +3 -3
- machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfg.ps1 +59 -0
- machineconfig/scripts/python/helpers_navigator/command_tree.py +50 -18
- machineconfig/scripts/python/helpers_network/address.py +132 -0
- machineconfig/scripts/python/{nw → helpers_network}/devops_add_ssh_key.py +24 -5
- machineconfig/scripts/python/{nw → helpers_network}/mount_nfs +0 -1
- machineconfig/scripts/python/{nw → helpers_network}/mount_nfs.py +2 -2
- machineconfig/scripts/python/{nw → helpers_network}/mount_ssh.py +1 -1
- machineconfig/scripts/python/{nw → helpers_network}/ssh_debug_linux.py +7 -7
- machineconfig/scripts/python/{nw → helpers_network}/ssh_debug_windows.py +4 -4
- machineconfig/scripts/python/{nw → helpers_network}/wifi_conn.py +1 -53
- machineconfig/scripts/python/{nw → helpers_network}/wsl_windows_transfer.py +3 -2
- machineconfig/scripts/python/helpers_repos/clone.py +0 -1
- machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +46 -19
- machineconfig/scripts/python/helpers_repos/entrypoint.py +2 -1
- machineconfig/scripts/python/helpers_repos/grource.py +1 -1
- machineconfig/scripts/python/helpers_repos/record.py +2 -1
- machineconfig/scripts/python/helpers_repos/repo_analyzer_1.py +160 -0
- machineconfig/scripts/python/helpers_repos/{count_lines.py → repo_analyzer_2.py} +113 -192
- machineconfig/scripts/python/helpers_sessions/sessions_multiprocess.py +20 -13
- machineconfig/scripts/python/helpers_utils/download.py +150 -0
- machineconfig/scripts/python/helpers_utils/path.py +185 -0
- machineconfig/scripts/python/interactive.py +19 -26
- machineconfig/scripts/python/{mcfg.py → mcfg_entry.py} +10 -0
- machineconfig/scripts/python/msearch.py +71 -0
- machineconfig/scripts/python/sessions.py +94 -25
- machineconfig/scripts/python/terminal.py +133 -0
- machineconfig/scripts/python/utils.py +28 -30
- machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
- machineconfig/scripts/windows/wrap_mcfg.ps1 +63 -0
- machineconfig/settings/broot/conf.toml +1 -1
- machineconfig/settings/helix/config.toml +16 -0
- machineconfig/settings/helix/languages.toml +13 -4
- machineconfig/settings/helix/yazi-picker.sh +12 -0
- machineconfig/settings/lf/linux/exe/lfcd.sh +1 -0
- machineconfig/settings/lf/linux/exe/previewer.sh +3 -2
- machineconfig/settings/lf/windows/lfcd.ps1 +1 -1
- machineconfig/settings/lf/windows/lfrc +14 -16
- machineconfig/settings/marimo/marimo.toml +1 -1
- machineconfig/settings/marimo/snippets/globalize.py +34 -0
- machineconfig/settings/shells/bash/init.sh +43 -11
- machineconfig/settings/shells/ipy/profiles/default/startup/playext.py +1 -1
- machineconfig/settings/shells/nushell/config.nu +2 -32
- machineconfig/settings/shells/nushell/env.nu +45 -6
- machineconfig/settings/shells/nushell/init.nu +314 -0
- machineconfig/settings/shells/pwsh/init.ps1 +40 -14
- machineconfig/settings/shells/starship/starship.toml +16 -0
- machineconfig/settings/shells/wezterm/wezterm.lua +2 -0
- machineconfig/settings/shells/wt/settings.json +14 -5
- machineconfig/settings/shells/zsh/init.sh +17 -19
- machineconfig/settings/television/cable_unix/alias.toml +8 -0
- machineconfig/settings/television/cable_unix/aws-buckets.toml +14 -0
- machineconfig/settings/television/cable_unix/aws-instances.toml +13 -0
- machineconfig/settings/television/cable_unix/bash-history.toml +8 -0
- machineconfig/settings/television/cable_unix/channels.toml +19 -0
- machineconfig/settings/television/cable_unix/dirs.toml +13 -0
- machineconfig/settings/television/cable_unix/distrobox-list.toml +42 -0
- machineconfig/settings/television/cable_unix/docker-images.toml +13 -0
- machineconfig/settings/television/cable_unix/dotfiles.toml +11 -0
- machineconfig/settings/television/cable_unix/env.toml +17 -0
- machineconfig/settings/television/cable_unix/files.toml +11 -0
- machineconfig/settings/television/cable_unix/fish-history.toml +8 -0
- machineconfig/settings/television/cable_unix/git-branch.toml +11 -0
- machineconfig/settings/television/cable_unix/git-diff.toml +10 -0
- machineconfig/settings/television/cable_unix/git-log.toml +12 -0
- machineconfig/settings/television/cable_unix/git-reflog.toml +12 -0
- machineconfig/settings/television/cable_unix/git-repos.toml +16 -0
- machineconfig/settings/television/cable_unix/guix.toml +20 -0
- machineconfig/settings/television/cable_unix/just-recipes.toml +18 -0
- machineconfig/settings/television/cable_unix/k8s-deployments.toml +36 -0
- machineconfig/settings/television/cable_unix/k8s-pods.toml +50 -0
- machineconfig/settings/television/cable_unix/k8s-services.toml +36 -0
- machineconfig/settings/television/cable_unix/man-pages.toml +24 -0
- machineconfig/settings/television/cable_unix/nu-history.toml +7 -0
- machineconfig/settings/television/cable_unix/procs.toml +20 -0
- machineconfig/settings/television/cable_unix/text.toml +17 -0
- machineconfig/settings/television/cable_unix/tldr.toml +18 -0
- machineconfig/settings/television/cable_unix/zsh-history.toml +9 -0
- machineconfig/settings/television/cable_windows/alias.toml +7 -0
- machineconfig/settings/television/cable_windows/dirs.toml +13 -0
- machineconfig/settings/television/cable_windows/docker-images.toml +13 -0
- machineconfig/settings/television/cable_windows/dotfiles.toml +11 -0
- machineconfig/settings/television/cable_windows/env.toml +17 -0
- machineconfig/settings/television/cable_windows/files.toml +14 -0
- machineconfig/settings/television/cable_windows/git-branch.toml +11 -0
- machineconfig/settings/television/cable_windows/git-diff.toml +10 -0
- machineconfig/settings/television/cable_windows/git-log.toml +11 -0
- machineconfig/settings/television/cable_windows/git-reflog.toml +11 -0
- machineconfig/settings/television/cable_windows/git-repos.toml +15 -0
- machineconfig/settings/television/cable_windows/nu-history.toml +7 -0
- machineconfig/settings/television/cable_windows/pwsh-history.toml +6 -0
- machineconfig/settings/television/cable_windows/text.toml +17 -0
- machineconfig/settings/yazi/init.lua +61 -0
- machineconfig/settings/yazi/keymap_linux.toml +94 -0
- machineconfig/settings/yazi/keymap_windows.toml +78 -0
- machineconfig/settings/yazi/shell/yazi_cd.ps1 +33 -0
- machineconfig/settings/yazi/shell/yazi_cd.sh +8 -0
- 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/__init__.py +2 -1
- machineconfig/setup_linux/web_shortcuts/interactive.sh +27 -12
- machineconfig/setup_linux/web_shortcuts/live_from_github.sh +31 -0
- machineconfig/setup_mac/__init__.py +2 -3
- machineconfig/setup_mac/apps_gui.sh +248 -0
- machineconfig/setup_windows/__init__.py +3 -3
- machineconfig/setup_windows/uv.ps1 +8 -1
- machineconfig/setup_windows/web_shortcuts/interactive.ps1 +26 -11
- machineconfig/setup_windows/web_shortcuts/live_from_github.ps1 +30 -0
- machineconfig/setup_windows/web_shortcuts/quick_init.ps1 +17 -0
- machineconfig/utils/accessories.py +7 -4
- machineconfig/utils/code.py +99 -32
- machineconfig/utils/files/ascii_art.py +1 -1
- machineconfig/utils/files/headers.py +3 -2
- machineconfig/utils/installer_utils/github_release_bulk.py +156 -119
- machineconfig/utils/installer_utils/install_from_url.py +183 -0
- machineconfig/utils/installer_utils/installer_class.py +42 -99
- machineconfig/utils/installer_utils/installer_cli.py +175 -0
- machineconfig/utils/installer_utils/installer_helper.py +129 -0
- machineconfig/utils/installer_utils/{installer_abc.py → installer_locator_utils.py} +36 -85
- machineconfig/utils/{installer.py → installer_utils/installer_runner.py} +16 -61
- machineconfig/utils/io.py +69 -1
- machineconfig/utils/links.py +56 -38
- machineconfig/utils/meta.py +33 -18
- machineconfig/utils/options.py +46 -18
- machineconfig/utils/options_tv.py +119 -0
- machineconfig/utils/path_extended.py +44 -95
- machineconfig/utils/path_helper.py +76 -23
- machineconfig/utils/procs.py +1 -1
- machineconfig/utils/scheduler.py +20 -53
- machineconfig/utils/scheduling.py +0 -2
- machineconfig/utils/schemas/fire_agents/fire_agents_input.py +1 -1
- machineconfig/utils/schemas/layouts/layout_types.py +1 -1
- machineconfig/utils/ssh.py +159 -412
- machineconfig/utils/ssh_utils/abc.py +5 -0
- machineconfig/utils/ssh_utils/copy_from_here.py +111 -0
- machineconfig/utils/ssh_utils/copy_to_here.py +302 -0
- machineconfig/utils/ssh_utils/utils.py +142 -0
- machineconfig/utils/ssh_utils/wsl.py +210 -0
- machineconfig/utils/terminal.py +1 -0
- machineconfig/utils/upgrade_packages.py +104 -28
- machineconfig/utils/ve.py +12 -4
- machineconfig-7.98.dist-info/METADATA +132 -0
- {machineconfig-6.82.dist-info → machineconfig-7.98.dist-info}/RECORD +259 -196
- {machineconfig-6.82.dist-info → machineconfig-7.98.dist-info}/entry_points.txt +4 -1
- machineconfig/jobs/installer/linux_scripts/pgsql.sh +0 -41
- machineconfig/jobs/installer/linux_scripts/timescaledb.sh +0 -71
- machineconfig/jobs/installer/powershell_scripts/archive_pygraphviz.ps1 +0 -12
- machineconfig/scripts/linux/fzf2g +0 -21
- machineconfig/scripts/linux/fzfag +0 -17
- machineconfig/scripts/linux/fzffg +0 -25
- machineconfig/scripts/linux/fzfrga +0 -21
- machineconfig/scripts/linux/mcfgs +0 -38
- machineconfig/scripts/linux/other/share_smb +0 -1
- machineconfig/scripts/linux/skrg +0 -4
- machineconfig/scripts/linux/warp-cli.sh +0 -122
- machineconfig/scripts/linux/z_ls +0 -104
- machineconfig/scripts/python/ai/command_runner/prompt.txt +0 -9
- machineconfig/scripts/python/helpers_fire/agentic_frameworks/fire_crush.py +0 -37
- machineconfig/scripts/python/helpers_fire/agentic_frameworks/fire_gemini.py +0 -44
- machineconfig/scripts/python/helpers_fire/agentic_frameworks/fire_qwen.py +0 -43
- machineconfig/scripts/python/helpers_fire/prompt.txt +0 -2
- machineconfig/scripts/python/helpers_fire/template.sh +0 -15
- machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +0 -17
- machineconfig/scripts/python/helpers_repos/secure_repo.py +0 -15
- machineconfig/scripts/python/nw/add_ssh_key.py +0 -148
- machineconfig/scripts/windows/fzfb.ps1 +0 -3
- machineconfig/scripts/windows/fzfg.ps1 +0 -2
- machineconfig/scripts/windows/fzfrga.bat +0 -20
- machineconfig/scripts/windows/mcfgs.ps1 +0 -17
- machineconfig/settings/lf/linux/exe/fzf_nano.sh +0 -16
- machineconfig/settings/lf/windows/fzf_edit.ps1 +0 -6
- machineconfig/settings/lf/windows/tst.ps1 +0 -1
- machineconfig/settings/yazi/yazi.toml +0 -4
- machineconfig/setup_linux/apps.sh +0 -66
- machineconfig/setup_linux/others/cli_installation.sh +0 -137
- machineconfig/setup_mac/apps.sh +0 -73
- machineconfig/setup_windows/apps.ps1 +0 -62
- machineconfig/utils/installer_utils/installer.py +0 -225
- machineconfig-6.82.dist-info/METADATA +0 -82
- /machineconfig/jobs/installer/linux_scripts/{warp-cli.sh → cloudflare_warp_cli.sh} +0 -0
- /machineconfig/scripts/python/{helpers_fire → ai/utils}/__init__.py +0 -0
- /machineconfig/scripts/python/{helpers_fire/agentic_frameworks → helpers_agents}/__init__.py +0 -0
- /machineconfig/scripts/python/{nw → helpers_agents/agentic_frameworks}/__init__.py +0 -0
- /machineconfig/scripts/python/{helpers_fire → helpers_agents}/fire_agents_help_search.py +0 -0
- /machineconfig/scripts/python/{helpers_fire → helpers_agents}/fire_agents_load_balancer.py +0 -0
- /machineconfig/scripts/python/{helpers_fire → helpers_agents/templates}/template.ps1 +0 -0
- /machineconfig/{settings/shells/pwsh/profile.ps1 → scripts/python/helpers_fire_command/f.py} +0 -0
- /machineconfig/{settings/yazi/keymap.toml → scripts/python/helpers_network/__init__.py} +0 -0
- /machineconfig/scripts/python/{nw → helpers_network}/devops_add_identity.py +0 -0
- /machineconfig/scripts/python/{nw → helpers_network}/mount_drive +0 -0
- /machineconfig/scripts/python/{nw → helpers_network}/mount_nw_drive +0 -0
- /machineconfig/scripts/python/{nw → helpers_network}/mount_nw_drive.py +0 -0
- /machineconfig/scripts/python/{nw → helpers_network}/mount_smb +0 -0
- /machineconfig/scripts/python/{nw → helpers_network}/onetimeshare.py +0 -0
- /machineconfig/scripts/{Restore-ThunderbirdProfile.ps1 → windows/mounts/Restore-ThunderbirdProfile.ps1} +0 -0
- /machineconfig/{jobs/installer/powershell_scripts → setup_windows/ssh}/openssh-server_add_key.ps1 +0 -0
- /machineconfig/{jobs/installer/powershell_scripts → setup_windows/ssh}/openssh-server_copy-ssh-id.ps1 +0 -0
- {machineconfig-6.82.dist-info → machineconfig-7.98.dist-info}/WHEEL +0 -0
- {machineconfig-6.82.dist-info → machineconfig-7.98.dist-info}/top_level.txt +0 -0
machineconfig/utils/options.py
CHANGED
|
@@ -1,38 +1,66 @@
|
|
|
1
|
+
|
|
1
2
|
from pathlib import Path
|
|
2
|
-
from machineconfig.utils.installer_utils.installer_abc import check_tool_exists
|
|
3
3
|
from rich.text import Text
|
|
4
4
|
from rich.panel import Panel
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
import subprocess
|
|
7
|
-
from typing import Optional, Union, Iterable, overload, Literal
|
|
7
|
+
from typing import Optional, Union, Iterable, overload, Literal, cast
|
|
8
8
|
|
|
9
9
|
@overload
|
|
10
|
-
def choose_from_options[T](
|
|
10
|
+
def choose_from_options[T](options: Iterable[T], msg: str, multi: Literal[False], custom_input: bool = False, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, tv: bool = False) -> T: ...
|
|
11
11
|
@overload
|
|
12
|
-
def choose_from_options[T](
|
|
13
|
-
def choose_from_options[T](
|
|
12
|
+
def choose_from_options[T](options: Iterable[T], msg: str, multi: Literal[True], custom_input: bool = True, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, tv: bool = False, ) -> list[T]: ...
|
|
13
|
+
def choose_from_options[T](options: Iterable[T], msg: str, multi: bool, custom_input: bool = True, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, tv: bool = False, ) -> Union[T, list[T]]:
|
|
14
14
|
# TODO: replace with https://github.com/tmbo/questionary
|
|
15
15
|
# # also see https://github.com/charmbracelet/gum
|
|
16
16
|
options_strings: list[str] = [str(x) for x in options]
|
|
17
17
|
default_string = str(default) if default is not None else None
|
|
18
18
|
console = Console()
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
19
|
+
from machineconfig.utils.installer_utils.installer_locator_utils import check_tool_exists
|
|
20
|
+
# from machineconfig.utils.installer_utils.installer_cli import check_tool_exists
|
|
21
|
+
# print("ch1")
|
|
22
|
+
if tv and check_tool_exists("tv"):
|
|
23
|
+
# from pyfzf.pyfzf import FzfPrompt
|
|
24
|
+
# fzf_prompt = FzfPrompt()
|
|
25
|
+
# nl = "\n"
|
|
26
|
+
# choice_string_multi: list[str] = fzf_prompt.prompt(choices=options_strings, fzf_options=("--multi" if multi else "") + f' --prompt "{prompt.replace(nl, " ")}" --ansi') # --border-label={msg.replace(nl, ' ')}")
|
|
27
|
+
# print("ch2")
|
|
28
|
+
from machineconfig.utils.accessories import randstr
|
|
29
|
+
options_txt_path = Path.home().joinpath("tmp_results/tmp_files/choices_" + randstr(6) + ".txt")
|
|
30
|
+
options_txt_path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
options_txt_path.write_text("\n".join(options_strings), encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
# Run `tv` interactively so the user can make selections. We redirect tv's
|
|
34
|
+
# stdout to a temporary output file so we can read the chosen lines after
|
|
35
|
+
# the interactive session completes. Do not capture_output or redirect
|
|
36
|
+
# stdin/stderr here so `tv` stays attached to the terminal.
|
|
37
|
+
tv_out_path = options_txt_path.with_name(options_txt_path.stem + "_out.txt")
|
|
38
|
+
tv_cmd = f"""cat {options_txt_path} | tv --ansi true --source-output "{{strip_ansi}}" > {tv_out_path}"""
|
|
39
|
+
res = subprocess.run(tv_cmd, shell=True)
|
|
40
|
+
|
|
41
|
+
# If tv returned a non-zero code and there is no output file, treat it as an error.
|
|
42
|
+
if res.returncode != 0 and not tv_out_path.exists():
|
|
43
|
+
raise RuntimeError(f"Got error running tv command: {tv_cmd}\nreturncode: {res.returncode}")
|
|
44
|
+
|
|
45
|
+
# Read selections (if any) from the output file created by tv.
|
|
46
|
+
out_text = tv_out_path.read_text(encoding="utf-8") if tv_out_path.exists() else ""
|
|
47
|
+
choice_string_multi = [x for x in out_text.splitlines() if x.strip() != ""]
|
|
48
|
+
|
|
49
|
+
# Cleanup temporary files
|
|
50
|
+
options_txt_path.unlink(missing_ok=True)
|
|
51
|
+
tv_out_path.unlink(missing_ok=True)
|
|
27
52
|
if not multi:
|
|
28
53
|
try:
|
|
29
54
|
choice_one_string = choice_string_multi[0]
|
|
55
|
+
if isinstance(list(options)[0], str): return cast(T, choice_one_string)
|
|
30
56
|
choice_idx = options_strings.index(choice_one_string)
|
|
31
57
|
return list(options)[choice_idx]
|
|
32
58
|
except IndexError as ie:
|
|
33
59
|
print(f"❌ Error: {options=}, {choice_string_multi=}")
|
|
34
60
|
print(f"🔍 Available choices: {choice_string_multi}")
|
|
35
61
|
raise ie
|
|
62
|
+
if isinstance(list(options)[0], str):
|
|
63
|
+
return cast(list[T], choice_string_multi)
|
|
36
64
|
choice_idx_s = [options_strings.index(x) for x in choice_string_multi]
|
|
37
65
|
return [list(options)[x] for x in choice_idx_s]
|
|
38
66
|
else:
|
|
@@ -55,7 +83,7 @@ def choose_from_options[T](msg: str, options: Iterable[T], multi: bool, custom_i
|
|
|
55
83
|
if choice_string == "":
|
|
56
84
|
if default_string is None:
|
|
57
85
|
console.print(Panel("🧨 Default option not available!", title="Error", expand=False))
|
|
58
|
-
return choose_from_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default,
|
|
86
|
+
return choose_from_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, tv=tv, multi=multi, custom_input=custom_input)
|
|
59
87
|
choice_idx = options_strings.index(default_string)
|
|
60
88
|
assert default is not None, "🧨 Default option not available!"
|
|
61
89
|
choice_one: T = default
|
|
@@ -73,7 +101,7 @@ def choose_from_options[T](msg: str, options: Iterable[T], multi: bool, custom_i
|
|
|
73
101
|
_ = ie
|
|
74
102
|
# raise ValueError(f"Unknown choice. {choice_string}") from ie
|
|
75
103
|
console.print(Panel(f"❓ Unknown choice: '{choice_string}'", title="Error", expand=False))
|
|
76
|
-
return choose_from_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default,
|
|
104
|
+
return choose_from_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, tv=tv, multi=multi, custom_input=custom_input)
|
|
77
105
|
except (TypeError, ValueError) as te: # int(choice_string) failed due to # either the number is invalid, or the input is custom.
|
|
78
106
|
if choice_string in options_strings: # string input
|
|
79
107
|
choice_idx = options_strings.index(choice_one) # type: ignore
|
|
@@ -84,7 +112,7 @@ def choose_from_options[T](msg: str, options: Iterable[T], multi: bool, custom_i
|
|
|
84
112
|
_ = te
|
|
85
113
|
# raise ValueError(f"Unknown choice. {choice_string}") from te
|
|
86
114
|
console.print(Panel(f"❓ Unknown choice: '{choice_string}'", title="Error", expand=False))
|
|
87
|
-
return choose_from_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default,
|
|
115
|
+
return choose_from_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, tv=tv, multi=multi, custom_input=custom_input)
|
|
88
116
|
console.print(Panel(f"✅ Selected option {choice_idx}: {choice_one}", title="Selected", expand=False))
|
|
89
117
|
if multi:
|
|
90
118
|
return [choice_one]
|
|
@@ -103,7 +131,7 @@ def choose_cloud_interactively() -> str:
|
|
|
103
131
|
raise ValueError(f"Got {tmp} from rclone listremotes")
|
|
104
132
|
if len(remotes) == 0:
|
|
105
133
|
raise RuntimeError("You don't have remotes. Configure your rclone first to get cloud services access.")
|
|
106
|
-
cloud: str = choose_from_options(msg="WHICH CLOUD?", multi=False, options=list(remotes), default=remotes[0],
|
|
134
|
+
cloud: str = choose_from_options(msg="WHICH CLOUD?", multi=False, options=list(remotes), default=remotes[0], tv=True)
|
|
107
135
|
console.print(Panel(f"✅ SELECTED CLOUD | {cloud}", border_style="bold blue", expand=False))
|
|
108
136
|
return cloud
|
|
109
137
|
|
|
@@ -121,4 +149,4 @@ def choose_ssh_host(multi: Literal[False]) -> str: ...
|
|
|
121
149
|
@overload
|
|
122
150
|
def choose_ssh_host(multi: Literal[True]) -> list[str]: ...
|
|
123
151
|
def choose_ssh_host(multi: bool):
|
|
124
|
-
return choose_from_options(msg="", options=get_ssh_hosts(), multi=multi,
|
|
152
|
+
return choose_from_options(msg="", options=get_ssh_hosts(), multi=multi, tv=True)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
#!/usr/bin/env python3
|
|
4
|
+
import base64
|
|
5
|
+
import pathlib
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(options_to_preview_mapping: dict[str, str]) -> str | None:
|
|
12
|
+
keys = list(options_to_preview_mapping.keys())
|
|
13
|
+
if not keys:
|
|
14
|
+
return None
|
|
15
|
+
with tempfile.TemporaryDirectory(prefix="tv_channel_") as tmpdir:
|
|
16
|
+
tempdir = pathlib.Path(tmpdir)
|
|
17
|
+
entries: list[str] = []
|
|
18
|
+
index_map: dict[int, str] = {}
|
|
19
|
+
preview_map_path = tempdir / "previews.tsv"
|
|
20
|
+
preview_rows: list[str] = []
|
|
21
|
+
for idx, key in enumerate(keys):
|
|
22
|
+
display_key = key.replace("\t", " ").replace("\n", " ")
|
|
23
|
+
entries.append(f"{idx}\t{display_key}")
|
|
24
|
+
index_map[idx] = key
|
|
25
|
+
encoded_preview = base64.b64encode(options_to_preview_mapping[key].encode("utf-8")).decode("ascii")
|
|
26
|
+
preview_rows.append(f"{idx}\t{encoded_preview}")
|
|
27
|
+
preview_map_path.write_text("\n".join(preview_rows), encoding="utf-8")
|
|
28
|
+
entries_path = tempdir / "entries.tsv"
|
|
29
|
+
entries_path.write_text("\n".join(entries), encoding="utf-8")
|
|
30
|
+
preview_script = tempdir / "preview.sh"
|
|
31
|
+
preview_script.write_text(
|
|
32
|
+
"""#!/usr/bin/env bash
|
|
33
|
+
set -euo pipefail
|
|
34
|
+
|
|
35
|
+
idx="$1"
|
|
36
|
+
script_dir="$(cd -- "$(dirname -- "$0")" && pwd)"
|
|
37
|
+
previews_file="${script_dir}/previews.tsv"
|
|
38
|
+
|
|
39
|
+
if [[ ! -f "${previews_file}" ]]; then
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
encoded_preview="$(awk -F '\t' -v idx="${idx}" '($1==idx){print $2; exit}' "${previews_file}" || true)"
|
|
44
|
+
|
|
45
|
+
if [[ -z "${encoded_preview}" ]]; then
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
preview_content="$(printf '%s' "${encoded_preview}" | base64 --decode)"
|
|
50
|
+
|
|
51
|
+
if command -v bat >/dev/null 2>&1; then
|
|
52
|
+
printf '%s' "${preview_content}" | glow -
|
|
53
|
+
elif command -v bat >/dev/null 2>&1; then
|
|
54
|
+
printf '%s' "${preview_content}" | bat --language=markdown --color=always --style=plain --paging=never
|
|
55
|
+
elif command -v glow >/dev/null 2>&1; then
|
|
56
|
+
printf '%s' "${preview_content}" | glow -
|
|
57
|
+
else
|
|
58
|
+
printf '%s' "${preview_content}"
|
|
59
|
+
fi
|
|
60
|
+
""",
|
|
61
|
+
encoding="utf-8"
|
|
62
|
+
)
|
|
63
|
+
preview_script.chmod(0o755)
|
|
64
|
+
channel_config = f"""[metadata]
|
|
65
|
+
name = "temp_options"
|
|
66
|
+
description = "Temporary channel for selecting options"
|
|
67
|
+
|
|
68
|
+
[source]
|
|
69
|
+
command = "cat '{entries_path}'"
|
|
70
|
+
display = "{{split:\\t:1}}"
|
|
71
|
+
output = "{{split:\\t:0}}"
|
|
72
|
+
|
|
73
|
+
[preview]
|
|
74
|
+
command = "{preview_script} {{split:\\t:0}}"
|
|
75
|
+
|
|
76
|
+
[ui.preview_panel]
|
|
77
|
+
size = 50
|
|
78
|
+
"""
|
|
79
|
+
channel_path = tempdir / "temp_options.toml"
|
|
80
|
+
channel_path.write_text(channel_config, encoding="utf-8")
|
|
81
|
+
env = os.environ.copy()
|
|
82
|
+
tv_config_dir = pathlib.Path.home() / ".config" / "television"
|
|
83
|
+
if not tv_config_dir.exists():
|
|
84
|
+
tv_config_dir = pathlib.Path(os.getenv("XDG_CONFIG_HOME", str(pathlib.Path.home() / ".config"))) / "television"
|
|
85
|
+
cable_dir = tv_config_dir / "cable"
|
|
86
|
+
cable_dir.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
temp_channel_link = cable_dir / "temp_options.toml"
|
|
88
|
+
if temp_channel_link.exists() or temp_channel_link.is_symlink():
|
|
89
|
+
temp_channel_link.unlink()
|
|
90
|
+
temp_channel_link.symlink_to(channel_path)
|
|
91
|
+
output_file = tempdir / "selection.txt"
|
|
92
|
+
try:
|
|
93
|
+
result = subprocess.run(["tv", "temp_options"], check=False, stdout=output_file.open("w"), text=True, env=env)
|
|
94
|
+
finally:
|
|
95
|
+
if temp_channel_link.exists() or temp_channel_link.is_symlink():
|
|
96
|
+
temp_channel_link.unlink()
|
|
97
|
+
if result.returncode not in (0, 130):
|
|
98
|
+
raise SystemExit(result.returncode)
|
|
99
|
+
if result.returncode == 130:
|
|
100
|
+
return None
|
|
101
|
+
if not output_file.exists():
|
|
102
|
+
return None
|
|
103
|
+
selected = output_file.read_text().strip()
|
|
104
|
+
if not selected:
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
index = int(selected)
|
|
108
|
+
except ValueError:
|
|
109
|
+
return None
|
|
110
|
+
return index_map.get(index)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
demo_mapping = {
|
|
115
|
+
"Option 1": "# Option 1\nThis is the preview for option 1.",
|
|
116
|
+
"Option 2": "# Option 2\nThis is the preview for option 2.",
|
|
117
|
+
"Option 3": "# Option 3\nThis is the preview for option 3."
|
|
118
|
+
}
|
|
119
|
+
main(demo_mapping)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from machineconfig.utils.accessories import randstr
|
|
2
|
+
from machineconfig.utils.io import decrypt, encrypt
|
|
2
3
|
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
import time
|
|
@@ -10,10 +11,13 @@ from platform import system
|
|
|
10
11
|
from typing import Any, Optional, Union, Callable, TypeAlias, Literal
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
|
|
13
15
|
OPLike: TypeAlias = Union[str, "PathExtended", Path, None]
|
|
14
16
|
PLike: TypeAlias = Union[str, "PathExtended", Path]
|
|
15
17
|
FILE_MODE: TypeAlias = Literal["r", "w", "x", "a"]
|
|
16
18
|
SHUTIL_FORMATS: TypeAlias = Literal["zip", "tar", "gztar", "bztar", "xztar"]
|
|
19
|
+
DECOMPRESS_SUPPORTED_FORMATS = [".tar.gz", ".tgz", ".tar", ".gz", ".tar.bz", ".tbz", ".tar.xz", ".zip", ".7z",
|
|
20
|
+
".tar.bz2", ".tbz2", ".xz"]
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
def _is_user_admin() -> bool:
|
|
@@ -54,81 +58,6 @@ def _run_shell_command(
|
|
|
54
58
|
)
|
|
55
59
|
|
|
56
60
|
|
|
57
|
-
def pwd2key(password: str, salt: Optional[bytes] = None, iterations: int = 10) -> bytes: # Derive a secret key from a given password and salt"""
|
|
58
|
-
import base64
|
|
59
|
-
|
|
60
|
-
if salt is None:
|
|
61
|
-
import hashlib
|
|
62
|
-
|
|
63
|
-
m = hashlib.sha256()
|
|
64
|
-
m.update(password.encode(encoding="utf-8"))
|
|
65
|
-
return base64.urlsafe_b64encode(s=m.digest()) # make url-safe bytes required by Ferent.
|
|
66
|
-
from cryptography.hazmat.primitives import hashes
|
|
67
|
-
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
68
|
-
|
|
69
|
-
return base64.urlsafe_b64encode(PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=iterations, backend=None).derive(password.encode()))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def encrypt(msg: bytes, key: Optional[bytes] = None, pwd: Optional[str] = None, salted: bool = True, iteration: Optional[int] = None, gen_key: bool = False) -> bytes:
|
|
73
|
-
import base64
|
|
74
|
-
from cryptography.fernet import Fernet
|
|
75
|
-
|
|
76
|
-
salt, iteration = None, None
|
|
77
|
-
if pwd is not None: # generate it from password
|
|
78
|
-
assert (key is None) and (type(pwd) is str), "❌ You can either pass key or pwd, or none of them, but not both."
|
|
79
|
-
import secrets
|
|
80
|
-
|
|
81
|
-
iteration = iteration or secrets.randbelow(exclusive_upper_bound=1_000_000)
|
|
82
|
-
salt = secrets.token_bytes(nbytes=16) if salted else None
|
|
83
|
-
key_resolved = pwd2key(password=pwd, salt=salt, iterations=iteration)
|
|
84
|
-
elif key is None:
|
|
85
|
-
if gen_key:
|
|
86
|
-
key_resolved = Fernet.generate_key()
|
|
87
|
-
Path.home().joinpath("dotfiles/creds/data/encrypted_files_key.bytes").write_bytes(key_resolved)
|
|
88
|
-
else:
|
|
89
|
-
try:
|
|
90
|
-
key_resolved = Path.home().joinpath("dotfiles/creds/data/encrypted_files_key.bytes").read_bytes()
|
|
91
|
-
print(f"⚠️ Using key from: {Path.home().joinpath('dotfiles/creds/data/encrypted_files_key.bytes')}")
|
|
92
|
-
except FileNotFoundError as err:
|
|
93
|
-
print("\n" * 3, "~" * 50, """Consider Loading up your dotfiles or pass `gen_key=True` to make and save one.""", "~" * 50, "\n" * 3)
|
|
94
|
-
raise FileNotFoundError(err) from err
|
|
95
|
-
elif isinstance(key, (str, PathExtended, Path)):
|
|
96
|
-
key_resolved = Path(key).read_bytes() # a path to a key file was passed, read it:
|
|
97
|
-
elif type(key) is bytes:
|
|
98
|
-
key_resolved = key # key passed explicitly
|
|
99
|
-
else:
|
|
100
|
-
raise TypeError("❌ Key must be either a path, bytes object or None.")
|
|
101
|
-
code = Fernet(key=key_resolved).encrypt(msg)
|
|
102
|
-
if pwd is not None and salt is not None and iteration is not None:
|
|
103
|
-
return base64.urlsafe_b64encode(b"%b%b%b" % (salt, iteration.to_bytes(4, "big"), base64.urlsafe_b64decode(code)))
|
|
104
|
-
return code
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def decrypt(token: bytes, key: Optional[bytes] = None, pwd: Optional[str] = None, salted: bool = True) -> bytes:
|
|
108
|
-
import base64
|
|
109
|
-
|
|
110
|
-
if pwd is not None:
|
|
111
|
-
assert key is None, "❌ You can either pass key or pwd, or none of them, but not both."
|
|
112
|
-
if salted:
|
|
113
|
-
decoded = base64.urlsafe_b64decode(token)
|
|
114
|
-
salt, iterations, token = decoded[:16], decoded[16:20], base64.urlsafe_b64encode(decoded[20:])
|
|
115
|
-
key_resolved = pwd2key(password=pwd, salt=salt, iterations=int.from_bytes(bytes=iterations, byteorder="big"))
|
|
116
|
-
else:
|
|
117
|
-
key_resolved = pwd2key(password=pwd) # trailing `;` prevents IPython from caching the result.
|
|
118
|
-
elif type(key) is bytes:
|
|
119
|
-
assert pwd is None, "❌ You can either pass key or pwd, or none of them, but not both."
|
|
120
|
-
key_resolved = key # passsed explicitly
|
|
121
|
-
elif key is None:
|
|
122
|
-
key_resolved = Path.home().joinpath("dotfiles/creds/data/encrypted_files_key.bytes").read_bytes() # read from file
|
|
123
|
-
elif isinstance(key, (str, Path)):
|
|
124
|
-
key_resolved = Path(key).read_bytes() # passed a path to a file containing kwy
|
|
125
|
-
else:
|
|
126
|
-
raise TypeError(f"❌ Key must be either str, P, Path, bytes or None. Recieved: {type(key)}")
|
|
127
|
-
from cryptography.fernet import Fernet
|
|
128
|
-
|
|
129
|
-
return Fernet(key=key_resolved).decrypt(token)
|
|
130
|
-
|
|
131
|
-
|
|
132
61
|
def validate_name(astring: str, replace: str = "_") -> str:
|
|
133
62
|
import re
|
|
134
63
|
|
|
@@ -225,7 +154,6 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
|
|
|
225
154
|
# ======================================= File Editing / Reading ===================================
|
|
226
155
|
def download(self, folder: OPLike = None, name: Optional[str] = None, allow_redirects: bool = True, timeout: Optional[int] = None, params: Any = None) -> "PathExtended":
|
|
227
156
|
import requests
|
|
228
|
-
|
|
229
157
|
response = requests.get(self.as_url_str(), allow_redirects=allow_redirects, timeout=timeout, params=params) # Alternative: from urllib import request; request.urlopen(url).read().decode('utf-8').
|
|
230
158
|
assert response.status_code == 200, f"Download failed with status code {response.status_code}\n{response.text}"
|
|
231
159
|
if name is not None:
|
|
@@ -553,9 +481,6 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
|
|
|
553
481
|
**kwargs: Any,
|
|
554
482
|
) -> "PathExtended":
|
|
555
483
|
path_resolved, slf = self._resolve_path(folder, name, path, self.name).expanduser().resolve(), self.expanduser().resolve()
|
|
556
|
-
# if use_7z: # benefits over regular zip and encrypt: can handle very large files with low memory footprint
|
|
557
|
-
# path_resolved = path_resolved + '.7z' if not path_resolved.suffix == '.7z' else path_resolved
|
|
558
|
-
# with install_n_import("py7zr").SevenZipFile(file=path_resolved, mode=mode, password=pwd) as archive: archive.writeall(path=str(slf), arcname=None)
|
|
559
484
|
arcname_obj = PathExtended(arcname or slf.name)
|
|
560
485
|
if arcname_obj.name != slf.name:
|
|
561
486
|
arcname_obj /= slf.name # arcname has to start from somewhere and end with filename
|
|
@@ -628,15 +553,6 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
|
|
|
628
553
|
folder = folder if not content else folder.parent
|
|
629
554
|
if slf.suffix == ".7z":
|
|
630
555
|
raise NotImplementedError("I have not implemented this yet")
|
|
631
|
-
# if overwrite: P(folder).delete(sure=True)
|
|
632
|
-
# result = folder
|
|
633
|
-
# import py7zr
|
|
634
|
-
# with py7zr.SevenZipFile(file=slf, mode='r', password=pwd) as archive:
|
|
635
|
-
# if pattern is not None:
|
|
636
|
-
# import re
|
|
637
|
-
# pat = re.compile(pattern)
|
|
638
|
-
# archive.extract(path=folder, targets=[f for f in archive.getnames() if pat.match(f)])
|
|
639
|
-
# else: archive.extractall(path=folder)
|
|
640
556
|
else:
|
|
641
557
|
if overwrite:
|
|
642
558
|
if not content:
|
|
@@ -771,21 +687,54 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
|
|
|
771
687
|
return ret
|
|
772
688
|
|
|
773
689
|
def decompress(self, folder: OPLike = None, name: Optional[str] = None, path: OPLike = None, inplace: bool = False, orig: bool = False, verbose: bool = True) -> "PathExtended":
|
|
774
|
-
if ".tar.gz"
|
|
690
|
+
if str(self).endswith(".tar.gz") or str(self).endswith(".tgz"):
|
|
775
691
|
# res = self.ungz_untar(folder=folder, path=path, name=name, inplace=inplace, verbose=verbose, orig=orig)
|
|
776
692
|
return self.ungz(name=f"tmp_{randstr()}.tar", inplace=inplace).untar(folder=folder, name=name, path=path, inplace=True, orig=orig, verbose=verbose) # this works for .tgz suffix as well as .tar.gz
|
|
777
|
-
elif
|
|
693
|
+
elif str(self).endswith(".tar"):
|
|
694
|
+
res = self.untar(folder=folder, name=name, path=path, inplace=inplace, orig=orig, verbose=verbose)
|
|
695
|
+
elif str(self).endswith(".gz"):
|
|
778
696
|
res = self.ungz(folder=folder, path=path, name=name, inplace=inplace, verbose=verbose, orig=orig)
|
|
779
|
-
elif ".tar.bz"
|
|
697
|
+
elif str(self).endswith(".tar.bz") or str(self).endswith(".tbz") or str(self).endswith(".tar.bz2"):
|
|
780
698
|
res = self.unbz(name=f"tmp_{randstr()}.tar", inplace=inplace)
|
|
781
699
|
return res.untar(folder=folder, name=name, path=path, inplace=True, orig=orig, verbose=verbose)
|
|
782
|
-
elif ".tar.xz"
|
|
700
|
+
elif str(self).endswith(".tar.xz"):
|
|
783
701
|
# res = self.unxz_untar(folder=folder, path=path, name=name, inplace=inplace, verbose=verbose, orig=orig)
|
|
784
702
|
res = self.unxz(inplace=inplace).untar(folder=folder, name=name, path=path, inplace=True, orig=orig, verbose=verbose)
|
|
785
|
-
elif ".zip"
|
|
703
|
+
elif str(self).endswith(".zip"):
|
|
786
704
|
res = self.unzip(folder=folder, path=path, name=name, inplace=inplace, verbose=verbose, orig=orig)
|
|
705
|
+
elif str(self).endswith(".7z"):
|
|
706
|
+
def unzip_7z(archive_path: str, dest_dir: Optional[str] = None) -> Path:
|
|
707
|
+
"""
|
|
708
|
+
Uncompresses a .7z archive to a directory and returns the Path to the extraction directory.
|
|
709
|
+
|
|
710
|
+
:param archive_path: path to the .7z archive file
|
|
711
|
+
:param dest_dir: optional path to directory to extract into; if None a temporary dir will be created
|
|
712
|
+
:return: pathlib.Path pointing to the destination directory where contents were extracted
|
|
713
|
+
:raises: FileNotFoundError if archive does not exist; py7zr.Bad7zFile or other error if extraction fails
|
|
714
|
+
"""
|
|
715
|
+
import py7zr # type: ignore
|
|
716
|
+
import tempfile
|
|
717
|
+
from pathlib import Path
|
|
718
|
+
archive_path_obj = Path(archive_path)
|
|
719
|
+
if not archive_path_obj.is_file():
|
|
720
|
+
raise FileNotFoundError(f"Archive file not found: {archive_path_obj!r}")
|
|
721
|
+
if dest_dir is None:
|
|
722
|
+
# create a temporary directory
|
|
723
|
+
dest = Path(tempfile.mkdtemp(prefix=f"unzip7z_{archive_path_obj.stem}_"))
|
|
724
|
+
else:
|
|
725
|
+
dest = Path(dest_dir)
|
|
726
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
727
|
+
# Perform extraction
|
|
728
|
+
with py7zr.SevenZipFile(str(archive_path_obj), mode='r') as archive:
|
|
729
|
+
archive.extractall(path=str(dest))
|
|
730
|
+
# Return the extraction directory path
|
|
731
|
+
return dest
|
|
732
|
+
from machineconfig.utils.code import run_lambda_function
|
|
733
|
+
destination_dir = str(self.expanduser().resolve()).replace(".7z", "")
|
|
734
|
+
run_lambda_function(lambda: unzip_7z(archive_path=str(self), dest_dir=destination_dir), uv_project_dir=None, uv_with=["py7zr"])
|
|
735
|
+
res = PathExtended(destination_dir)
|
|
787
736
|
else:
|
|
788
|
-
|
|
737
|
+
raise ValueError(f"Cannot decompress file with unknown extension: {self}")
|
|
789
738
|
return res
|
|
790
739
|
|
|
791
740
|
def encrypt(
|
|
@@ -861,7 +810,7 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
|
|
|
861
810
|
path = self
|
|
862
811
|
else:
|
|
863
812
|
try:
|
|
864
|
-
path = self.
|
|
813
|
+
path = PathExtended(self.expanduser().absolute().relative_to(Path.home()))
|
|
865
814
|
except ValueError as ve:
|
|
866
815
|
if strict:
|
|
867
816
|
raise ve
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
from machineconfig.utils.path_extended import PathExtended
|
|
2
|
-
from machineconfig.utils.options import choose_from_options
|
|
3
1
|
from machineconfig.utils.source_of_truth import EXCLUDE_DIRS
|
|
4
2
|
from rich.console import Console
|
|
5
3
|
from rich.panel import Panel
|
|
6
4
|
import platform
|
|
7
5
|
import subprocess
|
|
8
6
|
from pathlib import Path
|
|
9
|
-
|
|
7
|
+
from typing import Optional
|
|
10
8
|
|
|
11
9
|
console = Console()
|
|
12
10
|
|
|
13
11
|
|
|
14
|
-
def sanitize_path(a_path: str) ->
|
|
15
|
-
path =
|
|
12
|
+
def sanitize_path(a_path: str) -> Path:
|
|
13
|
+
path = Path(a_path)
|
|
16
14
|
if Path.cwd() == Path.home() and not path.exists():
|
|
17
15
|
result = input("Current working directory is home, and passed path is not full path, are you sure you want to continue, [y]/n? ") or "y"
|
|
18
16
|
if result == "y":
|
|
@@ -23,13 +21,13 @@ def sanitize_path(a_path: str) -> PathExtended:
|
|
|
23
21
|
if platform.system() == "Windows": # path copied from Linux/Mac to Windows
|
|
24
22
|
# For Linux: /home/username, for Mac: /Users/username
|
|
25
23
|
skip_parts = 3 if path.as_posix().startswith("/home") else 3 # Both have 3 parts to skip
|
|
26
|
-
path =
|
|
24
|
+
path = Path.home().joinpath(*path.parts[skip_parts:])
|
|
27
25
|
assert path.exists(), f"File not found: {path}"
|
|
28
26
|
source_os = "Linux" if path.as_posix().startswith("/home") else "macOS"
|
|
29
27
|
console.print(Panel(f"🔗 PATH MAPPING | {source_os} → Windows: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
|
|
30
|
-
elif platform.system() in ["Linux", "Darwin"] and
|
|
28
|
+
elif platform.system() in ["Linux", "Darwin"] and Path.home().as_posix() not in path.as_posix(): # copied between Unix-like systems with different username
|
|
31
29
|
skip_parts = 3 # Both /home/username and /Users/username have 3 parts to skip
|
|
32
|
-
path =
|
|
30
|
+
path = Path.home().joinpath(*path.parts[skip_parts:])
|
|
33
31
|
assert path.exists(), f"File not found: {path}"
|
|
34
32
|
current_os = "Linux" if platform.system() == "Linux" else "macOS"
|
|
35
33
|
source_os = "Linux" if path.as_posix().startswith("/home") else "macOS"
|
|
@@ -37,12 +35,12 @@ def sanitize_path(a_path: str) -> PathExtended:
|
|
|
37
35
|
elif path.as_posix().startswith("C:"):
|
|
38
36
|
if platform.system() in ["Linux", "Darwin"]: # path copied from Windows to Linux/Mac
|
|
39
37
|
xx = str(a_path).replace("\\\\", "/")
|
|
40
|
-
path =
|
|
38
|
+
path = Path.home().joinpath(*Path(xx).parts[3:]) # exclude C:\\Users\\username
|
|
41
39
|
assert path.exists(), f"File not found: {path}"
|
|
42
40
|
target_os = "Linux" if platform.system() == "Linux" else "macOS"
|
|
43
41
|
console.print(Panel(f"🔗 PATH MAPPING | Windows → {target_os}: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
|
|
44
|
-
elif platform.system() == "Windows" and
|
|
45
|
-
path =
|
|
42
|
+
elif platform.system() == "Windows" and Path.home().as_posix() not in path.as_posix(): # copied from Windows to Windows with different username
|
|
43
|
+
path = Path.home().joinpath(*path.parts[2:])
|
|
46
44
|
assert path.exists(), f"File not found: {path}"
|
|
47
45
|
console.print(Panel(f"🔗 PATH MAPPING | Windows → Windows: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
|
|
48
46
|
return path.expanduser().absolute()
|
|
@@ -67,49 +65,58 @@ def find_scripts(root: Path, name_substring: str, suffixes: set[str]) -> tuple[l
|
|
|
67
65
|
return filename_matches, partial_path_matches
|
|
68
66
|
|
|
69
67
|
|
|
70
|
-
def match_file_name(sub_string: str, search_root:
|
|
68
|
+
def match_file_name(sub_string: str, search_root: Path, suffixes: set[str]) -> Path:
|
|
71
69
|
search_root_obj = search_root.absolute()
|
|
72
70
|
# assume subscript is filename only, not a sub_path. There is no need to fzf over the paths.
|
|
73
71
|
filename_matches, partial_path_matches = find_scripts(search_root_obj, sub_string, suffixes)
|
|
74
72
|
if len(filename_matches) == 1:
|
|
75
|
-
return
|
|
73
|
+
return Path(filename_matches[0])
|
|
76
74
|
console.print(Panel(f"Partial filename {search_root_obj} match with case-insensitivity failed. This generated #{len(filename_matches)} results.", title="Search", expand=False))
|
|
77
75
|
if len(filename_matches) < 20:
|
|
78
76
|
print("\n".join([a_potential_match.as_posix() for a_potential_match in filename_matches]))
|
|
79
77
|
if len(filename_matches) > 1:
|
|
80
|
-
print("Try to narrow down filename_matches search by case-sensitivity.")
|
|
78
|
+
print(f"Try to narrow down filename_matches search by case-sensitivity, found {len(filename_matches)} results. First @ {filename_matches[0].as_posix()}")
|
|
81
79
|
# let's see if avoiding .lower() helps narrowing down to one result
|
|
82
80
|
reduced_scripts = [a_potential_match for a_potential_match in filename_matches if sub_string in a_potential_match.name]
|
|
83
81
|
if len(reduced_scripts) == 1:
|
|
84
|
-
return
|
|
82
|
+
return Path(reduced_scripts[0])
|
|
85
83
|
elif len(reduced_scripts) > 1:
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
from machineconfig.utils.options import choose_from_options
|
|
85
|
+
choice = choose_from_options(multi=False, msg="Multiple matches found", options=reduced_scripts, tv=True)
|
|
86
|
+
return Path(choice)
|
|
88
87
|
print(f"Result: This still generated {len(reduced_scripts)} results.")
|
|
89
88
|
if len(reduced_scripts) < 10:
|
|
90
89
|
print("\n".join([a_potential_match.as_posix() for a_potential_match in reduced_scripts]))
|
|
91
90
|
|
|
92
91
|
console.print(Panel(f"Partial path match with case-insensitivity failed. This generated #{len(partial_path_matches)} results.", title="Search", expand=False))
|
|
93
92
|
if len(partial_path_matches) == 1:
|
|
94
|
-
return
|
|
93
|
+
return Path(partial_path_matches[0])
|
|
95
94
|
elif len(partial_path_matches) > 1:
|
|
96
95
|
print("Try to narrow down partial_path_matches search by case-sensitivity.")
|
|
97
96
|
reduced_scripts = [a_potential_match for a_potential_match in partial_path_matches if sub_string in a_potential_match.as_posix()]
|
|
98
97
|
if len(reduced_scripts) == 1:
|
|
99
|
-
return
|
|
100
|
-
print(f"Result: This still generated {len(reduced_scripts)} results.")
|
|
98
|
+
return Path(reduced_scripts[0])
|
|
99
|
+
print(f"Result: This still generated {len(reduced_scripts)} results.")
|
|
100
|
+
|
|
101
101
|
try:
|
|
102
|
-
|
|
102
|
+
|
|
103
|
+
if len(partial_path_matches) == 0:
|
|
104
|
+
print("No partial path matches found, trying to do fd with --no-ignore ...")
|
|
105
|
+
fzf_cmd = f"cd '{search_root_obj}'; fd --no-ignore --type file --strip-cwd-prefix | fzf --ignore-case --exact --query={sub_string}"
|
|
106
|
+
else:
|
|
107
|
+
fzf_cmd = f"cd '{search_root_obj}'; fd --type file --strip-cwd-prefix | fzf --ignore-case --exact --query={sub_string}"
|
|
103
108
|
console.print(Panel(f"🔍 Second attempt: SEARCH STRATEGY | Using fd to search for '{sub_string}' in '{search_root_obj}' ...\n{fzf_cmd}", title="Search Strategy", expand=False))
|
|
104
109
|
search_res_raw = subprocess.run(fzf_cmd, stdout=subprocess.PIPE, text=True, check=True, shell=True).stdout
|
|
105
|
-
search_res = search_res_raw.strip().split("
|
|
110
|
+
search_res = search_res_raw.strip().split("\n")
|
|
106
111
|
except subprocess.CalledProcessError as cpe:
|
|
107
112
|
console.print(Panel(f"❌ ERROR | FZF search failed with '{sub_string}' in '{search_root_obj}'.\n{cpe}", title="Error", expand=False))
|
|
108
113
|
import sys
|
|
109
|
-
|
|
110
114
|
sys.exit(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results.")
|
|
111
115
|
if len(search_res) == 1:
|
|
112
116
|
return search_root_obj.joinpath(search_res_raw)
|
|
117
|
+
elif len(search_res) == 0:
|
|
118
|
+
msg = Panel(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results", title="File Not Found", expand=False)
|
|
119
|
+
raise FileNotFoundError(msg)
|
|
113
120
|
|
|
114
121
|
print(f"⚠️ WARNING | Multiple search results found for `{sub_string}`:\n'{search_res}'")
|
|
115
122
|
cmd = f"cd '{search_root_obj}'; fd --type file | fzf --select-1 --query={sub_string}"
|
|
@@ -121,3 +128,49 @@ def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[st
|
|
|
121
128
|
msg = Panel(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results", title="File Not Found", expand=False)
|
|
122
129
|
raise FileNotFoundError(msg) from cpe
|
|
123
130
|
return search_root_obj.joinpath(res)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def search_for_files_of_interest(path_obj: Path, suffixes: set[str]) -> list[Path]:
|
|
134
|
+
if path_obj.is_file():
|
|
135
|
+
return [path_obj]
|
|
136
|
+
files: list[Path] = []
|
|
137
|
+
directories_to_visit: list[Path] = [path_obj]
|
|
138
|
+
while directories_to_visit:
|
|
139
|
+
current_dir = directories_to_visit.pop()
|
|
140
|
+
for entry in current_dir.iterdir():
|
|
141
|
+
if entry.is_dir():
|
|
142
|
+
if entry.name == ".venv":
|
|
143
|
+
continue
|
|
144
|
+
directories_to_visit.append(entry)
|
|
145
|
+
continue
|
|
146
|
+
if entry.suffix not in suffixes:
|
|
147
|
+
continue
|
|
148
|
+
if entry.suffix == ".py" and entry.name == "__init__.py":
|
|
149
|
+
continue
|
|
150
|
+
files.append(entry)
|
|
151
|
+
return files
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_choice_file(path: str, suffixes: Optional[set[str]]):
|
|
155
|
+
path_obj = sanitize_path(path)
|
|
156
|
+
if suffixes is None:
|
|
157
|
+
import platform
|
|
158
|
+
if platform.system() == "Windows":
|
|
159
|
+
suffixes = {".py", ".ps1", ".sh"}
|
|
160
|
+
elif platform.system() in ["Linux", "Darwin"]:
|
|
161
|
+
suffixes = {".py", ".sh"}
|
|
162
|
+
else:
|
|
163
|
+
suffixes = {".py"}
|
|
164
|
+
if not path_obj.exists():
|
|
165
|
+
print(f"🔍 Searching for file matching `{path}` under `{Path.cwd()}`, but only if suffix matches {suffixes}")
|
|
166
|
+
choice_file = match_file_name(sub_string=path, search_root=Path.cwd(), suffixes=suffixes)
|
|
167
|
+
elif path_obj.is_dir():
|
|
168
|
+
print(f"🔍 Searching recursively for Python, PowerShell and Shell scripts in directory `{path_obj}`")
|
|
169
|
+
files = search_for_files_of_interest(path_obj, suffixes=suffixes)
|
|
170
|
+
print(f"🔍 Got #{len(files)} results.")
|
|
171
|
+
from machineconfig.utils.options import choose_from_options
|
|
172
|
+
choice_file = choose_from_options(multi=False, options=files, tv=True, msg="Choose one option")
|
|
173
|
+
choice_file = Path(choice_file)
|
|
174
|
+
else:
|
|
175
|
+
choice_file = path_obj
|
|
176
|
+
return choice_file
|
machineconfig/utils/procs.py
CHANGED
|
@@ -129,7 +129,7 @@ class ProcessManager:
|
|
|
129
129
|
all_lines = formatted_data.split("\n")
|
|
130
130
|
header_and_separator = all_lines[:2] # First two lines: header and separator
|
|
131
131
|
options = all_lines[2:] # Skip header and separator, only process lines
|
|
132
|
-
res = choose_from_options(options=all_lines, msg="📋 Select processes to manage:",
|
|
132
|
+
res = choose_from_options(options=all_lines, msg="📋 Select processes to manage:", tv=True, multi=True)
|
|
133
133
|
# Filter out header and separator if they were selected
|
|
134
134
|
selected_lines = [line for line in res if line not in header_and_separator]
|
|
135
135
|
indices = [options.index(val) for val in selected_lines]
|