machineconfig 2.0__py3-none-any.whl → 2.2__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/cluster/cloud_manager.py +0 -3
- machineconfig/cluster/data_transfer.py +0 -1
- machineconfig/cluster/file_manager.py +0 -1
- machineconfig/cluster/job_params.py +0 -3
- machineconfig/cluster/loader_runner.py +0 -3
- machineconfig/cluster/remote_machine.py +0 -1
- machineconfig/cluster/script_notify_upon_completion.py +0 -1
- machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +5 -6
- machineconfig/cluster/sessions_managers/archive/session_managers.py +0 -1
- machineconfig/cluster/sessions_managers/enhanced_command_runner.py +17 -57
- machineconfig/cluster/sessions_managers/wt_local.py +36 -110
- machineconfig/cluster/sessions_managers/wt_local_manager.py +42 -112
- machineconfig/cluster/sessions_managers/wt_remote.py +23 -30
- machineconfig/cluster/sessions_managers/wt_remote_manager.py +20 -62
- machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +10 -15
- machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +27 -127
- machineconfig/cluster/sessions_managers/wt_utils/remote_executor.py +10 -43
- machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +22 -101
- machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +11 -39
- machineconfig/cluster/sessions_managers/zellij_local.py +49 -102
- machineconfig/cluster/sessions_managers/zellij_local_manager.py +34 -78
- machineconfig/cluster/sessions_managers/zellij_remote.py +17 -24
- machineconfig/cluster/sessions_managers/zellij_remote_manager.py +7 -13
- machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +4 -2
- machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +6 -6
- machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +18 -88
- machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +2 -6
- machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +12 -40
- machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +3 -2
- machineconfig/cluster/templates/cli_click.py +0 -1
- machineconfig/cluster/templates/cli_gooey.py +0 -2
- machineconfig/cluster/templates/cli_trogon.py +0 -1
- machineconfig/cluster/templates/run_cloud.py +0 -1
- machineconfig/cluster/templates/run_cluster.py +0 -1
- machineconfig/cluster/templates/run_remote.py +0 -1
- machineconfig/cluster/templates/utils.py +27 -46
- machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/linux/msc/cli_agents.sh +16 -0
- machineconfig/jobs/python/check_installations.py +2 -1
- machineconfig/jobs/python/create_bootable_media.py +0 -2
- machineconfig/jobs/python/python_ve_symlink.py +9 -11
- machineconfig/jobs/python/tasks.py +0 -1
- machineconfig/jobs/python/vscode/api.py +5 -5
- machineconfig/jobs/python/vscode/link_ve.py +13 -14
- machineconfig/jobs/python/vscode/select_interpreter.py +21 -22
- machineconfig/jobs/python/vscode/sync_code.py +9 -13
- machineconfig/jobs/python_custom_installers/archive/ngrok.py +13 -13
- machineconfig/jobs/python_custom_installers/dev/aider.py +7 -15
- machineconfig/jobs/python_custom_installers/dev/alacritty.py +9 -18
- machineconfig/jobs/python_custom_installers/dev/brave.py +10 -19
- machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +8 -15
- machineconfig/jobs/python_custom_installers/dev/code.py +12 -32
- machineconfig/jobs/python_custom_installers/dev/cursor.py +3 -14
- machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +8 -7
- machineconfig/jobs/python_custom_installers/dev/espanso.py +15 -19
- machineconfig/jobs/python_custom_installers/dev/goes.py +5 -12
- machineconfig/jobs/python_custom_installers/dev/lvim.py +9 -17
- machineconfig/jobs/python_custom_installers/dev/nerdfont.py +12 -19
- machineconfig/jobs/python_custom_installers/dev/redis.py +12 -20
- machineconfig/jobs/python_custom_installers/dev/wezterm.py +12 -19
- machineconfig/jobs/python_custom_installers/dev/winget.py +5 -23
- machineconfig/jobs/python_custom_installers/docker.py +12 -21
- machineconfig/jobs/python_custom_installers/gh.py +11 -19
- machineconfig/jobs/python_custom_installers/hx.py +32 -16
- machineconfig/jobs/python_custom_installers/warp-cli.py +12 -20
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_generic_installers/config.json +1 -1
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/windows/archive/archive_pygraphviz.ps1 +1 -1
- machineconfig/jobs/windows/msc/cli_agents.bat +0 -0
- machineconfig/jobs/windows/msc/cli_agents.ps1 +0 -0
- machineconfig/jobs/windows/start_terminal.ps1 +1 -1
- machineconfig/profile/create.py +38 -26
- machineconfig/profile/create_hardlinks.py +29 -20
- machineconfig/profile/shell.py +56 -32
- machineconfig/scripts/__init__.py +0 -2
- machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/cloud/init.sh +2 -2
- machineconfig/scripts/linux/checkout_versions +1 -1
- machineconfig/scripts/linux/choose_wezterm_theme +1 -1
- machineconfig/scripts/linux/cloud_copy +1 -1
- machineconfig/scripts/linux/cloud_manager +1 -1
- machineconfig/scripts/linux/cloud_mount +1 -1
- machineconfig/scripts/linux/cloud_repo_sync +1 -1
- machineconfig/scripts/linux/cloud_sync +1 -1
- machineconfig/scripts/linux/croshell +1 -1
- machineconfig/scripts/linux/devops +7 -7
- machineconfig/scripts/linux/fire +1 -1
- machineconfig/scripts/linux/fire_agents +3 -2
- machineconfig/scripts/linux/ftpx +1 -1
- machineconfig/scripts/linux/gh_models +1 -1
- machineconfig/scripts/linux/kill_process +1 -1
- machineconfig/scripts/linux/mcinit +1 -1
- machineconfig/scripts/linux/repos +1 -1
- machineconfig/scripts/linux/scheduler +1 -1
- machineconfig/scripts/linux/start_slidev +1 -1
- machineconfig/scripts/linux/start_terminals +1 -1
- machineconfig/scripts/linux/url2md +1 -1
- machineconfig/scripts/linux/warp-cli.sh +122 -0
- machineconfig/scripts/linux/wifi_conn +1 -1
- machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_agents.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__init__.py +0 -0
- machineconfig/scripts/python/ai/generate_files.py +83 -0
- machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +2 -2
- machineconfig/scripts/python/ai/mcinit.py +14 -7
- machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +10 -5
- machineconfig/scripts/python/archive/tmate_conn.py +5 -5
- machineconfig/scripts/python/archive/tmate_start.py +7 -7
- machineconfig/scripts/python/choose_wezterm_theme.py +35 -32
- machineconfig/scripts/python/cloud_copy.py +23 -14
- machineconfig/scripts/python/cloud_mount.py +36 -24
- machineconfig/scripts/python/cloud_repo_sync.py +40 -27
- machineconfig/scripts/python/cloud_sync.py +4 -4
- machineconfig/scripts/python/croshell.py +40 -29
- machineconfig/scripts/python/devops.py +45 -27
- machineconfig/scripts/python/devops_add_identity.py +15 -25
- machineconfig/scripts/python/devops_add_ssh_key.py +8 -8
- machineconfig/scripts/python/devops_backup_retrieve.py +18 -16
- machineconfig/scripts/python/devops_devapps_install.py +25 -20
- machineconfig/scripts/python/devops_update_repos.py +232 -59
- machineconfig/scripts/python/dotfile.py +17 -15
- machineconfig/scripts/python/fire_agents.py +48 -22
- machineconfig/scripts/python/fire_jobs.py +93 -58
- machineconfig/scripts/python/ftpx.py +26 -15
- machineconfig/scripts/python/get_zellij_cmd.py +8 -7
- machineconfig/scripts/python/helpers/cloud_helpers.py +33 -28
- machineconfig/scripts/python/helpers/helpers2.py +27 -16
- machineconfig/scripts/python/helpers/helpers4.py +45 -32
- machineconfig/scripts/python/helpers/helpers5.py +1 -1
- machineconfig/scripts/python/helpers/repo_sync_helpers.py +32 -10
- machineconfig/scripts/python/mount_nfs.py +9 -16
- machineconfig/scripts/python/mount_nw_drive.py +10 -5
- machineconfig/scripts/python/mount_ssh.py +9 -7
- machineconfig/scripts/python/repos.py +216 -58
- machineconfig/scripts/python/snapshot.py +0 -1
- machineconfig/scripts/python/start_slidev.py +11 -6
- machineconfig/scripts/python/start_terminals.py +22 -16
- machineconfig/scripts/python/viewer_template.py +0 -1
- machineconfig/scripts/python/wifi_conn.py +49 -75
- machineconfig/scripts/python/wsl_windows_transfer.py +9 -7
- machineconfig/scripts/windows/checkout_version.ps1 +1 -3
- machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -3
- machineconfig/scripts/windows/cloud_copy.ps1 +2 -6
- machineconfig/scripts/windows/cloud_manager.ps1 +1 -1
- machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -2
- machineconfig/scripts/windows/cloud_sync.ps1 +2 -2
- machineconfig/scripts/windows/croshell.ps1 +2 -2
- machineconfig/scripts/windows/devops.ps1 +1 -4
- machineconfig/scripts/windows/dotfile.ps1 +1 -3
- machineconfig/scripts/windows/fire.ps1 +1 -1
- machineconfig/scripts/windows/ftpx.ps1 +2 -2
- machineconfig/scripts/windows/gpt.ps1 +1 -1
- machineconfig/scripts/windows/kill_process.ps1 +1 -2
- machineconfig/scripts/windows/mcinit.ps1 +1 -1
- machineconfig/scripts/windows/mount_nfs.ps1 +1 -1
- machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
- machineconfig/scripts/windows/pomodoro.ps1 +1 -1
- machineconfig/scripts/windows/py2exe.ps1 +1 -3
- machineconfig/scripts/windows/repos.ps1 +1 -1
- machineconfig/scripts/windows/scheduler.ps1 +1 -1
- machineconfig/scripts/windows/snapshot.ps1 +2 -2
- machineconfig/scripts/windows/start_slidev.ps1 +1 -1
- machineconfig/scripts/windows/start_terminals.ps1 +1 -1
- machineconfig/scripts/windows/wifi_conn.ps1 +1 -1
- machineconfig/scripts/windows/wsl_windows_transfer.ps1 +1 -3
- machineconfig/settings/lf/linux/lfrc +2 -1
- machineconfig/settings/linters/.ruff_cache/.gitignore +2 -0
- machineconfig/settings/linters/.ruff_cache/CACHEDIR.TAG +1 -0
- machineconfig/settings/lvim/windows/archive/config_additional.lua +1 -1
- machineconfig/settings/svim/linux/init.toml +1 -1
- machineconfig/settings/svim/windows/init.toml +1 -1
- machineconfig/setup_linux/web_shortcuts/croshell.sh +3 -52
- machineconfig/setup_linux/web_shortcuts/interactive.sh +6 -6
- machineconfig/setup_linux/web_shortcuts/ssh.sh +0 -4
- machineconfig/setup_windows/web_shortcuts/all.ps1 +2 -2
- machineconfig/setup_windows/web_shortcuts/ascii_art.ps1 +1 -1
- machineconfig/setup_windows/web_shortcuts/croshell.ps1 +1 -1
- machineconfig/setup_windows/web_shortcuts/interactive.ps1 +5 -5
- machineconfig/setup_windows/wt_and_pwsh/install_fonts.ps1 +51 -15
- machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +58 -13
- machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +45 -37
- machineconfig/utils/ai/generate_file_checklist.py +8 -10
- machineconfig/utils/ai/url2md.py +4 -2
- machineconfig/utils/cloud/onedrive/setup_oauth.py +1 -0
- machineconfig/utils/cloud/onedrive/transaction.py +63 -98
- machineconfig/utils/code.py +62 -41
- machineconfig/utils/installer.py +29 -35
- machineconfig/utils/installer_utils/installer_abc.py +11 -11
- machineconfig/utils/installer_utils/installer_class.py +155 -74
- machineconfig/utils/links.py +112 -31
- machineconfig/utils/notifications.py +211 -0
- machineconfig/utils/options.py +41 -42
- machineconfig/utils/path.py +13 -6
- machineconfig/utils/path_reduced.py +614 -311
- machineconfig/utils/procs.py +48 -42
- machineconfig/utils/scheduling.py +0 -1
- machineconfig/utils/source_of_truth.py +27 -0
- machineconfig/utils/ssh.py +146 -85
- machineconfig/utils/terminal.py +84 -37
- machineconfig/utils/upgrade_packages.py +91 -0
- machineconfig/utils/utils2.py +39 -50
- machineconfig/utils/utils5.py +195 -116
- machineconfig/utils/ve.py +13 -5
- {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/METADATA +14 -13
- {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/RECORD +212 -237
- machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python/__pycache__/python_ve_symlink.cpython-311.pyc +0 -0
- machineconfig/jobs/python/archive/python_tools.txt +0 -12
- machineconfig/jobs/python/vscode/__pycache__/select_interpreter.cpython-311.pyc +0 -0
- machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python_generic_installers/update.py +0 -3
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
- machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/linux/activate_ve +0 -87
- machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_copy.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_mount.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_sync.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_backup_retrieve.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-311.pyc +0 -0
- machineconfig/scripts/windows/activate_ve.ps1 +0 -54
- machineconfig/setup_linux/web_shortcuts/all.sh +0 -48
- machineconfig/setup_linux/web_shortcuts/update_system.sh +0 -48
- machineconfig/utils/utils.py +0 -95
- /machineconfig/setup_linux/web_shortcuts/{tmp.sh → android.sh} +0 -0
- {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/WHEEL +0 -0
- {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/top_level.txt +0 -0
machineconfig/utils/links.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from machineconfig.utils.path_reduced import
|
|
1
|
+
from machineconfig.utils.path_reduced import PathExtended as PathExtended, PLike
|
|
2
2
|
from machineconfig.utils.utils2 import randstr
|
|
3
3
|
from rich.console import Console
|
|
4
4
|
from rich.panel import Panel
|
|
@@ -19,6 +19,7 @@ def build_links(target_paths: list[tuple[PLike, str]], repo_root: PLike):
|
|
|
19
19
|
target_dirs_filtered.append((a_dir_obj, a_name))
|
|
20
20
|
|
|
21
21
|
import git
|
|
22
|
+
|
|
22
23
|
repo = git.Repo(repo_root, search_parent_directories=True)
|
|
23
24
|
root_maybe = repo.working_tree_dir
|
|
24
25
|
assert root_maybe is not None
|
|
@@ -31,49 +32,129 @@ def build_links(target_paths: list[tuple[PLike, str]], repo_root: PLike):
|
|
|
31
32
|
links_path = repo_root_obj.joinpath("links", a_name)
|
|
32
33
|
links_path.symlink_to(target=a_target_path)
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
|
|
36
|
+
def symlink_func(this: PathExtended, to_this: PathExtended, prioritize_to_this: bool):
|
|
35
37
|
"""helper function. creates a symlink from `this` to `to_this`.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
|
|
39
|
+
this: exists AND to_this exists AND this is a symlink pointing to to_this ===> Resolution: AUTO: do nothing, already linked correctly.
|
|
40
|
+
this: exists AND to_this exists AND this is a symlink pointing to somewhere else ===> Resolution: AUTO: delete this symlink, create symlink to to_this
|
|
41
|
+
this: exists AND to_this exists AND this is a concrete path ===> Resolution: DANGER: require user input to decide (param prioritize_to_this). Give two options: 1) prioritize `this`: to_this is backed up as to_this.orig_<randstr()>, to_this is deleted, and symlink is created from this to to_this as normal; 2) prioritize `to_this`: `this` is backed up as this.orig_<randstr()>, `this` is deleted, and symlink is created from this to to_this as normal.
|
|
42
|
+
|
|
43
|
+
this: exists AND to_this doesn't exist AND this is a symlink pointing to somewhere else ===> Resolution: AUTO: delete this symlink, create symlink to to_this (touch to_this)
|
|
44
|
+
this: exists AND to_this doesn't exist AND this is a symlink pointing to to_this ===> Resolution: AUTO: delete this symlink, create symlink to to_this (touch to_this)
|
|
45
|
+
this: exists AND to_this doesn't exist AND this is a concrete path ===> Resolution: AUTO: move this to to_this, then create symlink from this to to_this.
|
|
46
|
+
|
|
47
|
+
this: doesn't exist AND to_this exists ===> Resolution: AUTO: create link from this to to_this
|
|
48
|
+
this: doesn't exist AND to_this doesn't exist ===> Resolution: AUTO: create link from this to to_this (touch to_this)
|
|
49
|
+
|
|
50
|
+
"""
|
|
40
51
|
this = PathExtended(this).expanduser().absolute()
|
|
41
52
|
to_this = PathExtended(to_this).expanduser().absolute()
|
|
42
|
-
|
|
43
|
-
if this.exists():
|
|
44
|
-
if
|
|
45
|
-
if
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
# Case analysis based on docstring
|
|
54
|
+
if this.exists():
|
|
55
|
+
if to_this.exists():
|
|
56
|
+
if this.is_symlink():
|
|
57
|
+
# Check if symlink already points to correct target
|
|
58
|
+
try:
|
|
59
|
+
if this.readlink().resolve() == to_this.resolve():
|
|
60
|
+
# Case: this exists AND to_this exists AND this is a symlink pointing to to_this
|
|
61
|
+
console.print(Panel(f"✅ ALREADY LINKED | {this} ➡️ {to_this}", title="Already Linked", expand=False))
|
|
62
|
+
return
|
|
63
|
+
else:
|
|
64
|
+
# Case: this exists AND to_this exists AND this is a symlink pointing to somewhere else
|
|
65
|
+
console.print(Panel(f"🔄 RELINKING | Updating symlink from {this} ➡️ {to_this}", title="Relinking", expand=False))
|
|
66
|
+
this.delete(sure=True)
|
|
67
|
+
except OSError:
|
|
68
|
+
# Broken symlink case
|
|
69
|
+
console.print(Panel(f"🔄 FIXING BROKEN LINK | Fixing broken symlink from {this} ➡️ {to_this}", title="Fixing Broken Link", expand=False))
|
|
70
|
+
this.delete(sure=True)
|
|
71
|
+
else:
|
|
72
|
+
# Case: this exists AND to_this exists AND this is a concrete path
|
|
73
|
+
if prioritize_to_this:
|
|
74
|
+
# prioritize `to_this`: `this` is backed up, `this` is deleted, symlink created
|
|
75
|
+
backup_name = f"{this}.orig_{randstr()}"
|
|
76
|
+
console.print(Panel(f"📦 BACKING UP | Moving {this} to {backup_name}, prioritizing {to_this}", title="Backing Up", expand=False))
|
|
77
|
+
this.move(path=backup_name)
|
|
78
|
+
else:
|
|
79
|
+
# prioritize `this`: to_this is backed up, to_this is deleted, this content moved to to_this location
|
|
80
|
+
backup_name = f"{to_this}.orig_{randstr()}"
|
|
81
|
+
console.print(Panel(f"📦 BACKING UP | Moving {to_this} to {backup_name}, prioritizing {this}", title="Backing Up", expand=False))
|
|
82
|
+
to_this.move(path=backup_name)
|
|
83
|
+
this.move(path=to_this)
|
|
84
|
+
else:
|
|
85
|
+
# to_this doesn't exist
|
|
86
|
+
if this.is_symlink():
|
|
87
|
+
# Case: this exists AND to_this doesn't exist AND this is a symlink (pointing anywhere)
|
|
88
|
+
console.print(Panel(f"🔄 RELINKING | Updating symlink from {this} ➡️ {to_this}", title="Relinking", expand=False))
|
|
89
|
+
this.delete(sure=True)
|
|
90
|
+
# Create to_this
|
|
91
|
+
to_this.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
to_this.touch()
|
|
93
|
+
else:
|
|
94
|
+
# Case: this exists AND to_this doesn't exist AND this is a concrete path
|
|
95
|
+
console.print(Panel(f"📁 MOVING | Moving {this} to {to_this}, then creating symlink", title="Moving", expand=False))
|
|
96
|
+
this.move(path=to_this)
|
|
97
|
+
else:
|
|
98
|
+
# this doesn't exist
|
|
99
|
+
if to_this.exists():
|
|
100
|
+
# Case: this doesn't exist AND to_this exists
|
|
101
|
+
console.print(Panel(f"🆕 NEW LINK | Creating new symlink from {this} ➡️ {to_this}", title="New Link", expand=False))
|
|
102
|
+
else:
|
|
103
|
+
# Case: this doesn't exist AND to_this doesn't exist
|
|
104
|
+
console.print(Panel(f"🆕 NEW LINK & TARGET | Creating {to_this} and symlink from {this} ➡️ {to_this}", title="New Link & Target", expand=False))
|
|
53
105
|
to_this.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
-
to_this.touch()
|
|
106
|
+
to_this.touch()
|
|
107
|
+
# Create the symlink
|
|
55
108
|
try:
|
|
56
109
|
console.print(Panel(f"🔗 LINKING | Creating symlink from {this} ➡️ {to_this}", title="Linking", expand=False))
|
|
57
110
|
PathExtended(this).symlink_to(target=to_this, verbose=True, overwrite=True)
|
|
58
111
|
except Exception as ex:
|
|
59
112
|
console.print(Panel(f"❌ ERROR | Failed at linking {this} ➡️ {to_this}. Reason: {ex}", title="Error", expand=False))
|
|
60
113
|
|
|
61
|
-
|
|
114
|
+
|
|
115
|
+
def symlink_copy(this: PathExtended, to_this: PathExtended, prioritize_to_this: bool = True):
|
|
62
116
|
this = PathExtended(this).expanduser().absolute()
|
|
63
117
|
to_this = PathExtended(to_this).expanduser().absolute()
|
|
64
|
-
|
|
65
|
-
if this.exists():
|
|
66
|
-
if
|
|
67
|
-
if
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
118
|
+
# Case analysis based on docstring of symlink_func
|
|
119
|
+
if this.exists():
|
|
120
|
+
if to_this.exists():
|
|
121
|
+
if this.is_symlink():
|
|
122
|
+
try:
|
|
123
|
+
if this.readlink().resolve() == to_this.resolve():
|
|
124
|
+
console.print(Panel(f"✅ ALREADY LINKED | {this} ➡️ {to_this}", title="Already Linked", expand=False))
|
|
125
|
+
return
|
|
126
|
+
else:
|
|
127
|
+
console.print(Panel(f"🔄 RELINKING | Updating symlink from {this} ➡️ {to_this}", title="Relinking", expand=False))
|
|
128
|
+
this.delete(sure=True)
|
|
129
|
+
except OSError:
|
|
130
|
+
console.print(Panel(f"🔄 FIXING BROKEN LINK | Fixing broken symlink from {this} ➡️ {to_this}", title="Fixing Broken Link", expand=False))
|
|
131
|
+
this.delete(sure=True)
|
|
132
|
+
else:
|
|
133
|
+
if prioritize_to_this:
|
|
134
|
+
backup_name = f"{this}.orig_{randstr()}"
|
|
135
|
+
console.print(Panel(f"📦 BACKING UP | Moving {this} to {backup_name}, prioritizing {to_this}", title="Backing Up", expand=False))
|
|
136
|
+
this.move(path=backup_name)
|
|
137
|
+
else:
|
|
138
|
+
backup_name = f"{to_this}.orig_{randstr()}"
|
|
139
|
+
console.print(Panel(f"📦 BACKING UP | Moving {to_this} to {backup_name}, prioritizing {this}", title="Backing Up", expand=False))
|
|
140
|
+
to_this.move(path=backup_name)
|
|
141
|
+
this.move(path=to_this)
|
|
142
|
+
else:
|
|
143
|
+
if this.is_symlink():
|
|
144
|
+
console.print(Panel(f"🔄 RELINKING | Updating symlink from {this} ➡️ {to_this}", title="Relinking", expand=False))
|
|
145
|
+
this.delete(sure=True)
|
|
146
|
+
to_this.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
to_this.touch()
|
|
148
|
+
else:
|
|
149
|
+
console.print(Panel(f"📁 MOVING | Moving {this} to {to_this}, then copying", title="Moving", expand=False))
|
|
150
|
+
this.move(path=to_this)
|
|
151
|
+
else:
|
|
152
|
+
if to_this.exists():
|
|
153
|
+
console.print(Panel(f"🆕 NEW LINK | Copying {to_this} to {this}", title="New Link", expand=False))
|
|
154
|
+
else:
|
|
155
|
+
console.print(Panel(f"🆕 NEW LINK & TARGET | Creating {to_this} and copying to {this}", title="New Link & Target", expand=False))
|
|
75
156
|
to_this.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
-
to_this.touch()
|
|
157
|
+
to_this.touch()
|
|
77
158
|
try:
|
|
78
159
|
console.print(Panel(f"📋 COPYING | Copying {to_this} to {this}", title="Copying", expand=False))
|
|
79
160
|
to_this.copy(path=this, overwrite=True, verbose=True)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Notifications Module"""
|
|
2
|
+
|
|
3
|
+
# from crocodile.core import install_n_import
|
|
4
|
+
# from crocodile.file_management import P, Read
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# from crocodile.meta import RepeatUntilNoException
|
|
8
|
+
import smtplib
|
|
9
|
+
import imaplib
|
|
10
|
+
|
|
11
|
+
# from email import message
|
|
12
|
+
# from email import encoders
|
|
13
|
+
# from email.mime.base import MIMEBase
|
|
14
|
+
from email.mime.text import MIMEText
|
|
15
|
+
from email.mime.multipart import MIMEMultipart
|
|
16
|
+
from typing import Optional, Any, Union
|
|
17
|
+
from markdown import markdown
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def download_to_memory(path: Path, allow_redirects: bool = True, timeout: Optional[float] = None, params: Any = None) -> "Any":
|
|
21
|
+
import requests
|
|
22
|
+
|
|
23
|
+
return requests.get(
|
|
24
|
+
path.as_posix().replace("https:/", "https://").replace("http:/", "http://"), allow_redirects=allow_redirects, timeout=timeout, params=params
|
|
25
|
+
) # Alternative: from urllib import request; request.urlopen(url).read().decode('utf-8').
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_github_markdown_css() -> str:
|
|
29
|
+
pp = r"https://raw.githubusercontent.com/sindresorhus/github-markdown-css/main/github-markdown-dark.css"
|
|
30
|
+
return download_to_memory(Path(pp)).text
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def md2html(body: str):
|
|
34
|
+
gh_style = Path(__file__).parent.joinpath("gh_style.css").read_text()
|
|
35
|
+
return f"""
|
|
36
|
+
<!DOCTYPE html>
|
|
37
|
+
<html>
|
|
38
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
39
|
+
<style>
|
|
40
|
+
{gh_style}
|
|
41
|
+
.markdown-body {{
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
min-width: 200px;
|
|
44
|
+
max-width: 1350px;
|
|
45
|
+
margin: 0 auto;
|
|
46
|
+
padding: 45px;
|
|
47
|
+
line-height: 1.8;
|
|
48
|
+
}}
|
|
49
|
+
@media (max-width: 767px) {{.markdown-body {{padding: 15px;}}
|
|
50
|
+
}}
|
|
51
|
+
</style>
|
|
52
|
+
<body>
|
|
53
|
+
<div class="markdown-body">
|
|
54
|
+
{markdown(body)}
|
|
55
|
+
</div>
|
|
56
|
+
</body>
|
|
57
|
+
</html>"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Email:
|
|
61
|
+
@staticmethod
|
|
62
|
+
def get_source_of_truth():
|
|
63
|
+
path = Path.home().joinpath("dotfiles/machineconfig/emails.ini")
|
|
64
|
+
if not path.exists():
|
|
65
|
+
raise FileNotFoundError(f"""File not found: {path}. It should be an ini file with this structure
|
|
66
|
+
[resend]
|
|
67
|
+
api_key = xxx
|
|
68
|
+
|
|
69
|
+
[config1]
|
|
70
|
+
email_add = a@b.com
|
|
71
|
+
password = 123
|
|
72
|
+
smtp_host = a@b.com
|
|
73
|
+
smtp_port = 465
|
|
74
|
+
imap_host = b@c.com
|
|
75
|
+
imap_port = 465
|
|
76
|
+
encryption = ssl
|
|
77
|
+
|
|
78
|
+
""")
|
|
79
|
+
|
|
80
|
+
if not Path(path).exists() or Path(path).is_dir():
|
|
81
|
+
raise FileNotFoundError(f"File not found or is a directory: {path}")
|
|
82
|
+
import configparser
|
|
83
|
+
|
|
84
|
+
res = configparser.ConfigParser()
|
|
85
|
+
res.read(filenames=[str(path)], encoding=None)
|
|
86
|
+
return res
|
|
87
|
+
|
|
88
|
+
def __init__(self, config: dict[str, Any]):
|
|
89
|
+
self.config = config
|
|
90
|
+
from smtplib import SMTP_SSL, SMTP
|
|
91
|
+
|
|
92
|
+
self.server: Union[SMTP_SSL, SMTP]
|
|
93
|
+
if config["encryption"].lower() == "ssl":
|
|
94
|
+
self.server = smtplib.SMTP_SSL(host=self.config["smtp_host"], port=self.config["smtp_port"])
|
|
95
|
+
elif config["encryption"].lower() == "tls":
|
|
96
|
+
self.server = smtplib.SMTP(host=self.config["smtp_host"], port=self.config["smtp_port"])
|
|
97
|
+
self.server.login(self.config["email_add"], password=self.config["password"])
|
|
98
|
+
|
|
99
|
+
def send_message(self, to: str, subject: str, body: str, txt_to_html: bool = True, attachments: Optional[list[Any]] = None):
|
|
100
|
+
_ = attachments
|
|
101
|
+
body += "\n\nThis is an automated email sent via crocodile.comms script."
|
|
102
|
+
# msg = message.EmailMessage()
|
|
103
|
+
msg = MIMEMultipart("alternative")
|
|
104
|
+
msg["subject"] = subject
|
|
105
|
+
msg["From"] = self.config["email_add"]
|
|
106
|
+
msg["To"] = to
|
|
107
|
+
# msg['Content-Type'] = "text/html"
|
|
108
|
+
# msg.set_content(body)
|
|
109
|
+
|
|
110
|
+
# <link rel="stylesheet" href="github-markdown.css">
|
|
111
|
+
# <link type="text/css" rel="stylesheet" href="https://raw.githubusercontent.com/sindresorhus/github-markdown-css/main/github-markdown-dark.css" />
|
|
112
|
+
|
|
113
|
+
if txt_to_html:
|
|
114
|
+
body = md2html(body=body)
|
|
115
|
+
msg.attach(MIMEText(body, "html"))
|
|
116
|
+
# if attachments is None: attachments = [] # see: https://fedingo.com/how-to-send-html-mail-with-attachment-using-python/
|
|
117
|
+
# for attachment in attachmenthrs: msg.attach(attachment.read_bytes(), filename=attachment.stem, maintype="image", subtype=attachment.suffix)
|
|
118
|
+
# for attachment in attachments: msg.attach(attachment.read_bytes(), filename=attachment.stem, maintype="application", subtype="octet-stream")
|
|
119
|
+
|
|
120
|
+
self.server.send_message(msg)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def manage_folders(email_add: str, pwd: str):
|
|
124
|
+
server = imaplib.IMAP4()
|
|
125
|
+
server.starttls()
|
|
126
|
+
server.login(email_add, password=pwd)
|
|
127
|
+
|
|
128
|
+
def send_email(self, to_addrs: str, msg: str):
|
|
129
|
+
return self.server.sendmail(from_addr=self.config["email_add"], to_addrs=to_addrs, msg=msg)
|
|
130
|
+
|
|
131
|
+
def close(self):
|
|
132
|
+
self.server.quit() # Closing is vital as many servers do not allow mutiple connections.
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def send_and_close(config_name: Optional[str], to: str, subject: str, body: str) -> Any:
|
|
136
|
+
"""If config_name is None, it sends from a generic email address."""
|
|
137
|
+
if config_name is None:
|
|
138
|
+
raise NotImplementedError(
|
|
139
|
+
"Sending email without a config_name is not implemented. You need to create an emails.ini file in ~/dotfiles/machineconfig/ with your email configuration. See the docstring of the get_source_of_truth method for more information."
|
|
140
|
+
)
|
|
141
|
+
# config = Email.get_source_of_truth()
|
|
142
|
+
# try:
|
|
143
|
+
# api_key = config['resend']['api_key']
|
|
144
|
+
# to = config["resend"]["signup_email"]
|
|
145
|
+
# except KeyError as ke:
|
|
146
|
+
# msggg = "You did not pass a config_name, therefore, the default is to use resend, however, you need to add your resend api key to the emails.ini file."
|
|
147
|
+
# raise KeyError(msggg) from ke
|
|
148
|
+
|
|
149
|
+
# _resend = install_n_import("resend")
|
|
150
|
+
# import resend # type: ignore
|
|
151
|
+
# resend.api_key = api_key
|
|
152
|
+
# r = resend.Emails.send({
|
|
153
|
+
# "from": "onboarding@resend.dev",
|
|
154
|
+
# "to": to,
|
|
155
|
+
# "subject": subject,
|
|
156
|
+
# "html": md2html(body=body)
|
|
157
|
+
# })
|
|
158
|
+
# return r
|
|
159
|
+
|
|
160
|
+
else:
|
|
161
|
+
config = dict(Email.get_source_of_truth()[config_name])
|
|
162
|
+
tmp = Email(config=config)
|
|
163
|
+
tmp.send_message(to=to, subject=subject, body=body)
|
|
164
|
+
tmp.close()
|
|
165
|
+
|
|
166
|
+
# @staticmethod
|
|
167
|
+
# def send_m365(to: list[str], subject: str, body: Optional[str], body_file: Optional[str], body_content_type: Literal["HTML", "Text"], attachments: Optional[list[Path]] = None) -> None:
|
|
168
|
+
# if body_file is not None:
|
|
169
|
+
# assert body is None, "You cannot pass both body and body_file."
|
|
170
|
+
# body_file_path = Path(body_file)
|
|
171
|
+
# assert body_file_path.exists(), f"File not found: {body_file_path}"
|
|
172
|
+
# else:
|
|
173
|
+
# body_file_path = None
|
|
174
|
+
# assert body is not None, "You must pass either body or body_file."
|
|
175
|
+
# from crocodile.meta import Terminal
|
|
176
|
+
|
|
177
|
+
# to_str = ",".join(to)
|
|
178
|
+
# attachments_str = " ".join([f"--attachment {str(p)}" for p in attachments]) if attachments is not None else ""
|
|
179
|
+
|
|
180
|
+
# if body_file is not None:
|
|
181
|
+
# body_arg = f"--bodyContents @{body_file_path}"
|
|
182
|
+
# else:
|
|
183
|
+
# body_arg = f'"{body}"'
|
|
184
|
+
# cmd = f"""m365 outlook mail send --verbose --saveToSentItems --importance normal --bodyContentType {body_content_type} --bodyContents {body_arg} --subject "{subject}" --to {to_str} {attachments_str}"""
|
|
185
|
+
# response = Terminal().run(cmd, shell="powershell")
|
|
186
|
+
# response.print(desc="Email sending response")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# class PhoneNotification: # security concerns: avoid using this.
|
|
190
|
+
# def __init__(self, token: Optional[str]):
|
|
191
|
+
# if token is None:
|
|
192
|
+
# path = P.home().joinpath("dotfiles/machineconfig/phone_notification.ini")
|
|
193
|
+
# ini = Read.ini(path)
|
|
194
|
+
# token_ = ini["default"]["token"]
|
|
195
|
+
# else:
|
|
196
|
+
# token_ = token
|
|
197
|
+
# pushbullet = install_n_import("pushbullet")
|
|
198
|
+
# self.api = pushbullet.Pushbullet(token_)
|
|
199
|
+
# def send_notification(self, title: str = "Note From Python", body: str = "A notfication"):
|
|
200
|
+
# self.api.push_note(title=title, body=body)
|
|
201
|
+
# @staticmethod
|
|
202
|
+
# def open_website():
|
|
203
|
+
# P(r"https://www.pushbullet.com/")()
|
|
204
|
+
# @staticmethod # https://www.youtube.com/watch?v=tbzPcKRZlHg
|
|
205
|
+
# def try_me(bulletpoint_token: str):
|
|
206
|
+
# n = PhoneNotification(bulletpoint_token)
|
|
207
|
+
# n.send_notification()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
pass
|
machineconfig/utils/options.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
from rich.text import Text
|
|
4
3
|
from rich.panel import Panel
|
|
@@ -10,53 +9,43 @@ from typing import Optional, Union, TypeVar, Iterable
|
|
|
10
9
|
T = TypeVar("T")
|
|
11
10
|
|
|
12
11
|
|
|
12
|
+
def check_tool_exists(tool_name: str) -> bool:
|
|
13
|
+
if platform.system() == "Windows": tool_name = tool_name.replace(".exe", "") + ".exe"
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
if platform.system() == "Windows":
|
|
16
|
-
tool_name = tool_name.replace(".exe", "") + ".exe"
|
|
15
|
+
from machineconfig.utils.source_of_truth import WINDOWS_INSTALL_PATH, LINUX_INSTALL_PATH
|
|
17
16
|
|
|
18
|
-
if platform.system() == "Windows":
|
|
19
|
-
|
|
17
|
+
if platform.system() == "Windows":
|
|
18
|
+
cmd = "where.exe"
|
|
19
|
+
root_path = Path(WINDOWS_INSTALL_PATH)
|
|
20
|
+
elif platform.system() in ["Linux", "Darwin"]:
|
|
21
|
+
cmd = "which"
|
|
22
|
+
root_path = Path(LINUX_INSTALL_PATH)
|
|
20
23
|
else: raise NotImplementedError(f"platform {platform.system()} not implemented")
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if result.stderr:
|
|
35
|
-
print(f"STDERR: {result.stderr}")
|
|
36
|
-
print(f"Return code: {result.returncode}")
|
|
37
|
-
return check_tool_exists(tool_name=tool_name, install_script=None)
|
|
38
|
-
return res
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def choose_one_option(options: Iterable[T], header: str="", tail: str="", prompt: str="", msg: str="",
|
|
42
|
-
default: Optional[T] = None, fzf: bool=False, custom_input: bool=False) -> T:
|
|
43
|
-
choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt,
|
|
44
|
-
default=default, fzf=fzf, multi=False, custom_input=custom_input)
|
|
25
|
+
_ = cmd
|
|
26
|
+
# try:
|
|
27
|
+
# _tmp = subprocess.check_output([cmd, tool_name], stderr=subprocess.DEVNULL)
|
|
28
|
+
# res: bool = True
|
|
29
|
+
# except (subprocess.CalledProcessError, FileNotFoundError):
|
|
30
|
+
# res = False
|
|
31
|
+
# return res
|
|
32
|
+
return root_path.joinpath(tool_name).is_file()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def choose_one_option(options: Iterable[T], header: str = "", tail: str = "", prompt: str = "", msg: str = "", default: Optional[T] = None, fzf: bool = False, custom_input: bool = False) -> T:
|
|
36
|
+
choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, fzf=fzf, multi=False, custom_input=custom_input)
|
|
45
37
|
assert not isinstance(choice_key, list)
|
|
46
38
|
return choice_key
|
|
47
39
|
|
|
48
40
|
|
|
49
|
-
def choose_multiple_options(options: Iterable[T], header: str="", tail: str="", prompt: str="", msg: str="",
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
custom_input=custom_input)
|
|
54
|
-
if isinstance(choice_key, list): return choice_key
|
|
41
|
+
def choose_multiple_options(options: Iterable[T], header: str = "", tail: str = "", prompt: str = "", msg: str = "", default: Optional[T] = None, custom_input: bool = False) -> list[T]:
|
|
42
|
+
choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, fzf=True, multi=True, custom_input=custom_input)
|
|
43
|
+
if isinstance(choice_key, list):
|
|
44
|
+
return choice_key
|
|
55
45
|
return [choice_key]
|
|
56
46
|
|
|
57
47
|
|
|
58
|
-
def display_options(msg: str, options: Iterable[T], header: str="", tail: str="", prompt: str="",
|
|
59
|
-
default: Optional[T] = None, fzf: bool=False, multi: bool=False, custom_input: bool=False) -> Union[T, list[T]]:
|
|
48
|
+
def display_options(msg: str, options: Iterable[T], header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False, multi: bool = False, custom_input: bool = False) -> Union[T, list[T]]:
|
|
60
49
|
# TODO: replace with https://github.com/tmbo/questionary
|
|
61
50
|
# # also see https://github.com/charmbracelet/gum
|
|
62
51
|
tool_name = "fzf"
|
|
@@ -65,6 +54,7 @@ def display_options(msg: str, options: Iterable[T], header: str="", tail: str=""
|
|
|
65
54
|
console = Console()
|
|
66
55
|
if fzf and check_tool_exists(tool_name):
|
|
67
56
|
from pyfzf.pyfzf import FzfPrompt
|
|
57
|
+
|
|
68
58
|
fzf_prompt = FzfPrompt()
|
|
69
59
|
nl = "\n"
|
|
70
60
|
choice_string_multi: list[str] = fzf_prompt.prompt(choices=options_strings, fzf_options=("--multi" if multi else "") + f' --prompt "{prompt.replace(nl, " ")}" ') # --border-label={msg.replace(nl, ' ')}")
|
|
@@ -84,7 +74,8 @@ def display_options(msg: str, options: Iterable[T], header: str="", tail: str=""
|
|
|
84
74
|
if default is not None:
|
|
85
75
|
assert default in options, f"Default `{default}` option not in options `{list(options)}`"
|
|
86
76
|
default_msg = Text(" <<<<-------- DEFAULT", style="bold red")
|
|
87
|
-
else:
|
|
77
|
+
else:
|
|
78
|
+
default_msg = Text("")
|
|
88
79
|
txt = Text("\n" + msg + "\n")
|
|
89
80
|
for idx, key in enumerate(options):
|
|
90
81
|
txt = txt + Text(f"{idx:2d} ", style="bold blue") + str(key) + (default_msg if default is not None and default == key else "") + "\n"
|
|
@@ -111,7 +102,8 @@ def display_options(msg: str, options: Iterable[T], header: str="", tail: str=""
|
|
|
111
102
|
if choice_string in options_strings: # string input
|
|
112
103
|
choice_idx = options_strings.index(choice_one) # type: ignore
|
|
113
104
|
choice_one = list(options)[choice_idx]
|
|
114
|
-
elif custom_input:
|
|
105
|
+
elif custom_input:
|
|
106
|
+
return str(choice_string) # type: ignore
|
|
115
107
|
else:
|
|
116
108
|
_ = ie
|
|
117
109
|
# raise ValueError(f"Unknown choice. {choice_string}") from ie
|
|
@@ -129,7 +121,8 @@ def display_options(msg: str, options: Iterable[T], header: str="", tail: str=""
|
|
|
129
121
|
console.print(Panel(f"❓ Unknown choice: '{choice_string}'", title="Error", expand=False))
|
|
130
122
|
return display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, fzf=fzf, multi=multi, custom_input=custom_input)
|
|
131
123
|
console.print(Panel(f"✅ Selected option {choice_idx}: {choice_one}", title="Selected", expand=False))
|
|
132
|
-
if multi:
|
|
124
|
+
if multi:
|
|
125
|
+
return [choice_one]
|
|
133
126
|
return choice_one
|
|
134
127
|
|
|
135
128
|
|
|
@@ -141,16 +134,22 @@ def choose_cloud_interactively() -> str:
|
|
|
141
134
|
if isinstance(tmp, str):
|
|
142
135
|
remotes: list[str] = [x.replace(":", "") for x in tmp.splitlines()]
|
|
143
136
|
|
|
144
|
-
else:
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError(f"Got {tmp} from rclone listremotes")
|
|
145
139
|
if len(remotes) == 0:
|
|
146
140
|
raise RuntimeError("You don't have remotes. Configure your rclone first to get cloud services access.")
|
|
147
141
|
cloud: str = choose_one_option(msg="WHICH CLOUD?", options=list(remotes), default=remotes[0], fzf=True)
|
|
148
142
|
console.print(Panel(f"✅ SELECTED CLOUD | {cloud}", border_style="bold blue", expand=False))
|
|
149
143
|
return cloud
|
|
150
144
|
|
|
145
|
+
|
|
151
146
|
def get_ssh_hosts() -> list[str]:
|
|
152
147
|
from paramiko import SSHConfig
|
|
148
|
+
|
|
153
149
|
c = SSHConfig()
|
|
154
150
|
c.parse(open(Path.home().joinpath(".ssh/config"), encoding="utf-8"))
|
|
155
151
|
return list(c.get_hostnames())
|
|
156
|
-
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def choose_ssh_host(multi: bool = True):
|
|
155
|
+
return display_options(msg="", options=get_ssh_hosts(), multi=multi, fzf=True)
|
machineconfig/utils/path.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from machineconfig.utils.path_reduced import
|
|
1
|
+
from machineconfig.utils.path_reduced import PathExtended as PathExtended
|
|
2
2
|
from machineconfig.utils.options import choose_one_option
|
|
3
3
|
from rich.console import Console
|
|
4
4
|
from rich.panel import Panel
|
|
@@ -11,12 +11,14 @@ from pathlib import Path
|
|
|
11
11
|
T = TypeVar("T")
|
|
12
12
|
console = Console()
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
def sanitize_path(a_path: PathExtended) -> PathExtended:
|
|
15
16
|
path = PathExtended(a_path)
|
|
16
17
|
if Path.cwd() == Path.home() and not path.exists():
|
|
17
18
|
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
19
|
if result == "y":
|
|
19
20
|
import sys
|
|
21
|
+
|
|
20
22
|
sys.exit()
|
|
21
23
|
if path.as_posix().startswith("/home") or path.as_posix().startswith("/Users"):
|
|
22
24
|
if platform.system() == "Windows": # path copied from Linux/Mac to Windows
|
|
@@ -70,7 +72,8 @@ def match_file_name(sub_string: str, search_root: PathExtended) -> PathExtended:
|
|
|
70
72
|
search_root_obj = search_root.absolute()
|
|
71
73
|
# assume subscript is filename only, not a sub_path. There is no need to fzf over the paths.
|
|
72
74
|
filename_matches, partial_path_matches = find_scripts(search_root_obj, sub_string)
|
|
73
|
-
if len(filename_matches) == 1:
|
|
75
|
+
if len(filename_matches) == 1:
|
|
76
|
+
return PathExtended(filename_matches[0])
|
|
74
77
|
console.print(Panel(f"Partial filename match with case-insensitivity failed. This generated #{len(filename_matches)} results.", title="Search", expand=False))
|
|
75
78
|
if len(filename_matches) < 10:
|
|
76
79
|
print("\n".join([a_potential_match.as_posix() for a_potential_match in filename_matches]))
|
|
@@ -78,7 +81,8 @@ def match_file_name(sub_string: str, search_root: PathExtended) -> PathExtended:
|
|
|
78
81
|
print("Try to narrow down filename_matches search by case-sensitivity.")
|
|
79
82
|
# let's see if avoiding .lower() helps narrowing down to one result
|
|
80
83
|
reduced_scripts = [a_potential_match for a_potential_match in filename_matches if sub_string in a_potential_match.name]
|
|
81
|
-
if len(reduced_scripts) == 1:
|
|
84
|
+
if len(reduced_scripts) == 1:
|
|
85
|
+
return PathExtended(reduced_scripts[0])
|
|
82
86
|
elif len(reduced_scripts) > 1:
|
|
83
87
|
choice = choose_one_option(msg="Multiple matches found", options=reduced_scripts, fzf=True)
|
|
84
88
|
return PathExtended(choice)
|
|
@@ -92,18 +96,21 @@ def match_file_name(sub_string: str, search_root: PathExtended) -> PathExtended:
|
|
|
92
96
|
elif len(partial_path_matches) > 1:
|
|
93
97
|
print("Try to narrow down partial_path_matches search by case-sensitivity.")
|
|
94
98
|
reduced_scripts = [a_potential_match for a_potential_match in partial_path_matches if sub_string in a_potential_match.as_posix()]
|
|
95
|
-
if len(reduced_scripts) == 1:
|
|
99
|
+
if len(reduced_scripts) == 1:
|
|
100
|
+
return PathExtended(reduced_scripts[0])
|
|
96
101
|
print(f"Result: This still generated {len(reduced_scripts)} results.")
|
|
97
102
|
try:
|
|
98
103
|
fzf_cmd = f"cd '{search_root_obj}'; fd --type file --strip-cwd-prefix | fzf --ignore-case --exact --query={sub_string}"
|
|
99
104
|
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))
|
|
100
|
-
search_res_raw = subprocess.run(fzf_cmd, stdout=subprocess.PIPE, text=True, check=True, shell=True
|
|
105
|
+
search_res_raw = subprocess.run(fzf_cmd, stdout=subprocess.PIPE, text=True, check=True, shell=True).stdout
|
|
101
106
|
search_res = search_res_raw.strip().split("\\n")[:-1]
|
|
102
107
|
except subprocess.CalledProcessError as cpe:
|
|
103
108
|
console.print(Panel(f"❌ ERROR | FZF search failed with '{sub_string}' in '{search_root_obj}'.\n{cpe}", title="Error", expand=False))
|
|
104
109
|
import sys
|
|
110
|
+
|
|
105
111
|
sys.exit(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results.")
|
|
106
|
-
if len(search_res) == 1:
|
|
112
|
+
if len(search_res) == 1:
|
|
113
|
+
return search_root_obj.joinpath(search_res_raw)
|
|
107
114
|
|
|
108
115
|
print(f"⚠️ WARNING | Multiple search results found for `{sub_string}`\n'{search_res_raw}'")
|
|
109
116
|
cmd = f"cd '{search_root_obj}'; fd --type file | fzf --select-1 --query={sub_string}"
|