Compare commits
No commits in common. "main" and "v2" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
**/.DS_Store
|
||||
.venv/
|
||||
.dotfiles_env
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -1,6 +0,0 @@
|
||||
[submodule "config/shared/nvim"]
|
||||
path = config/shared/nvim
|
||||
url = git@gitea.tomastm.com:tomas.mirchev/nvim-config.git
|
||||
[submodule "config/shared/barg-parser"]
|
||||
path = config/shared/barg-parser
|
||||
url = git@gitea.tomastm.com:tomas.mirchev/barg-parser.git
|
||||
226
README.md
226
README.md
@ -1,226 +0,0 @@
|
||||
# Dotfiles
|
||||
|
||||
## How to Use
|
||||
|
||||
First, clone the repository:
|
||||
|
||||
```sh
|
||||
git clone https://gitea.tomastm.com/tomas.mirchev/dotfiles.git ~/.dotfiles && cd ~/.dotfiles
|
||||
```
|
||||
|
||||
The `setup` script will automatically update the remote repository URL. However, if you are configuring the dotfiles manually, make sure to change it after adding your SSH key:
|
||||
|
||||
```sh
|
||||
git remote set-url origin git@gitea.tomastm.com:tomas.mirchev/dotfiles.git
|
||||
git remote -v
|
||||
```
|
||||
|
||||
The `manage.py` script allows you to link configurations, install packages, and set up environments. To view available arguments, use:
|
||||
|
||||
```sh
|
||||
manage.py -h
|
||||
manage.py <link|install|setup> -h
|
||||
```
|
||||
|
||||
### Common Commands:
|
||||
|
||||
- `manage.py link <environment> [--copy] [-f|--force] [-p|--package]`
|
||||
- `manage.py install <environment> [-p|--package]`
|
||||
- `manage.py setup <environment> [--extra]`
|
||||
|
||||
You can create multiple environments, following this structure:
|
||||
|
||||
- `config/<environment>`
|
||||
- `config.json` (add `<environment>` to the JSON file)
|
||||
- `setups/<environment>`
|
||||
- Any additional scripts can be placed in `scripts/<script>`, following the naming convention `<env>-<script>`.
|
||||
|
||||
## Environments
|
||||
|
||||
Although you can specify any environment using `manage.py`, the recommended workflow follows this structure:
|
||||
|
||||
### MacOS (`macos`)
|
||||
|
||||
> **Setup command:** `manage.py setup macos`
|
||||
|
||||
Since macOS is the host machine, it should use minimal applications and configurations. Whenever possible, install applications via Homebrew.
|
||||
|
||||
Before formatting your machine, back up installed formulas and casks:
|
||||
|
||||
```sh
|
||||
scripts/macos-brew_backup.sh
|
||||
```
|
||||
|
||||
This backup will be restored automatically when setting up `macos`.
|
||||
|
||||
#### Essential Applications:
|
||||
|
||||
- **LinearMouse**: Invert scrolling, adjust scrolling by pixels, and fix pointer acceleration.
|
||||
- **Karabiner**: Configure keybindings and modifier keys.
|
||||
- **Rectangle**: Window tiling.
|
||||
- **Window-Tagger**: Custom window manager ([repository](https://gitea.tomastm.com/tomas.mirchev/window-tagger)).
|
||||
- **Ghostty**: Terminal emulator.
|
||||
|
||||
System settings cannot be automatically backed up. Refer to `obsidian#MacSettings.md` for manual adjustments.
|
||||
|
||||
#### UTM (Virtual Machines)
|
||||
|
||||
Development should occur inside virtual machines. UTM is preferred due to its open-source nature and support for both QEMU and Apple Hypervisor.
|
||||
|
||||
> **Note:** When installing ISOs, ensure the correct architecture is selected.
|
||||
|
||||
For Debian-based setups, avoid setting a `root` password to enable automatic `sudo` installation and add your user to the sudo group.
|
||||
|
||||
#### DNS Configuration
|
||||
|
||||
Communication between the host and virtual machines happens via local IPs set by UTM DHCP. To simplify this process, use `dnsmasq` (installed via `manage.py`).
|
||||
|
||||
##### 1. Verify `dnsmasq` is Running
|
||||
|
||||
```sh
|
||||
pgrep dnsmasq # Should return a PID if running
|
||||
```
|
||||
|
||||
##### 2. Determine the Default Gateway
|
||||
|
||||
On **macOS (host):**
|
||||
|
||||
```sh
|
||||
ifconfig | grep 192
|
||||
```
|
||||
|
||||
On the **VM** (in "Shared Network" mode):
|
||||
|
||||
```sh
|
||||
ip r
|
||||
```
|
||||
|
||||
Typically, the **gateway IP** is `192.168.64.1`. If different, update the next steps accordingly.
|
||||
|
||||
##### 3. Edit `dnsmasq.conf`
|
||||
|
||||
```sh
|
||||
sudo vim /opt/homebrew/etc/dnsmasq.conf
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```ini
|
||||
# Define local domains
|
||||
address=/personal.utm.local/192.168.64.2
|
||||
address=/university.utm.local/192.168.64.3
|
||||
address=/personal.workstation.lan/192.168.50.2
|
||||
|
||||
# Listening addresses (include gateway IP)
|
||||
listen-address=127.0.0.1,192.168.64.1
|
||||
```
|
||||
|
||||
##### 4. Configure macOS to Use `dnsmasq`
|
||||
|
||||
```sh
|
||||
sudo mkdir -p /etc/resolver
|
||||
sudo vim /etc/resolver/local
|
||||
sudo vim /etc/resolver/lan
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```plaintext
|
||||
nameserver 127.0.0.1
|
||||
```
|
||||
|
||||
Alternatively:
|
||||
|
||||
```sh
|
||||
echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/local
|
||||
```
|
||||
|
||||
##### 5. Restart `dnsmasq` and Flush DNS Cache
|
||||
|
||||
```sh
|
||||
sudo brew services restart dnsmasq
|
||||
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
|
||||
```
|
||||
|
||||
##### Notes
|
||||
|
||||
- `dnsmasq` requires manual removal on Brew upgrades or uninstallation:
|
||||
|
||||
```sh
|
||||
sudo rm -rf /opt/homebrew/Cellar/dnsmasq/*/sbin
|
||||
sudo rm -rf /opt/homebrew/opt/dnsmasq
|
||||
sudo rm -rf /opt/homebrew/var/homebrew/linked/dnsmasq
|
||||
```
|
||||
|
||||
### Linux VM (`linux-vm`)
|
||||
|
||||
> **Setup command:** `manage.py setup linux-vm --extra "<hostname>"`
|
||||
|
||||
After running the setup script, follow any additional manual steps it outputs.
|
||||
|
||||
The most critical step is authenticating with the registry:
|
||||
|
||||
```sh
|
||||
docker login registry.tomastm.com
|
||||
```
|
||||
|
||||
### Linux Dev (`linux-dev`)
|
||||
|
||||
This environment does not use a `setup` command, as it is configured via `Dockerfile`. However, `install` and `link` commands are available.
|
||||
|
||||
Use `link --copy` to avoid redundant configurations inside images, reducing final image size.
|
||||
|
||||
A base image (`base-debian`) contains essential utilities. Additional images exist for specific tools, such as `node` and `python`. View all images at: [Dev Containers](https://gitea.tomastm.com/tomas.mirchev/dev-containers).
|
||||
|
||||
Each project should run its own container instance, enabling seamless project switching. Code remains on a mounted volume from the Linux VM.
|
||||
|
||||
Containers persist until manually stopped, allowing `tmux` sessions to remain active.
|
||||
|
||||
For convenience, use the `dev` script located in `~/bin` (automatically added to `PATH`).
|
||||
|
||||
```sh
|
||||
dev -i <image> <name>
|
||||
|
||||
# Example:
|
||||
dev -i node myapp
|
||||
```
|
||||
|
||||
This creates a container named `node-myapp` (`<image>-<name>`).
|
||||
|
||||
All images support both `arm64` and `amd64` architectures, automatically detected during execution.
|
||||
|
||||
---
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### Building NeoVim
|
||||
|
||||
NeoVim is mirrored to Gitea to build `.deb` packages for releases. Follow these steps:
|
||||
|
||||
```sh
|
||||
git clone git@gitea.tomastm.com:tomas.mirchev/neovim.git
|
||||
cd neovim
|
||||
git checkout tags/<tag>
|
||||
make CMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cd build
|
||||
cpack -G DEB
|
||||
```
|
||||
|
||||
This builds for the host machine's architecture. The package should follow this format:
|
||||
|
||||
```
|
||||
nvim-linux-$(dpkg --print-architecture).deb
|
||||
```
|
||||
|
||||
Create a new release in the mirrored repository and attach the `.deb` file.
|
||||
|
||||
### Proxmox
|
||||
|
||||
UTM is the preferred VM solution, but Proxmox can be used for homelab setups.
|
||||
|
||||
To fix `apt update/upgrade` issues:
|
||||
|
||||
1. Go to "Datacenter" → "workstation" → "Repositories".
|
||||
2. Disable both enterprise sources.
|
||||
3. Add a new repository with "No-Subscription".
|
||||
4. Refresh "Updates" and proceed.
|
||||
@ -1,68 +0,0 @@
|
||||
const version = "v11"
|
||||
//console.info("WINDOW TAGGER STARTED - " + version)
|
||||
|
||||
const windows = Array.from({length: 9}, () => null)
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
registerShortcut(
|
||||
`TagWindow${i+1}`,
|
||||
`Tag current window to tag ${i+1}`,
|
||||
`Meta+Shift+${i+1}`,
|
||||
function() {
|
||||
try {
|
||||
//console.info(`Trying to tag at ${i+1}`)
|
||||
if (!workspace.activeWindow) {
|
||||
//console.info("No active window to tag")
|
||||
return
|
||||
}
|
||||
|
||||
//console.info(`Tag ${i+1}: ${workspace.activeWindow.caption}`)
|
||||
for (let j = 0; j < 9; j++) {
|
||||
if (windows[j] === workspace.activeWindow) {
|
||||
windows[j] = null;
|
||||
}
|
||||
}
|
||||
windows[i] = workspace.activeWindow
|
||||
} catch (e) {
|
||||
console.info(e)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
registerShortcut(
|
||||
`FocusWindow${i}`,
|
||||
`Focus Window at tag ${i+1}`,
|
||||
`Meta+${i+1}`,
|
||||
function() {
|
||||
try {
|
||||
//console.info(`Total: ${windows.filter(w => w !== null).length}`)
|
||||
windows.forEach(w => {
|
||||
if (w) {
|
||||
//console.info(`- ${w.caption}`)
|
||||
}
|
||||
})
|
||||
if (!windows[i]) {
|
||||
//console.info("Tag is empty")
|
||||
return
|
||||
}
|
||||
|
||||
if (windows[i] === workspace.activeWindow) {
|
||||
windows[i].minimized = true
|
||||
//console.info("Focusing already focused window")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
workspace.activeWindow = windows[i]
|
||||
} catch (error ) {
|
||||
// console.info(windows[i].valid, windows[i].deleted)
|
||||
// console.info("Error: ", error)
|
||||
windows[i] = null
|
||||
}
|
||||
} catch (e) {
|
||||
// console.info(e)
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
{
|
||||
"KPlugin": {
|
||||
"Name": "Window Tagger",
|
||||
"Description": "Tag windows with numbers and quickly switch between them",
|
||||
"Icon": "preferences-system-windows",
|
||||
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "username@gmail.com",
|
||||
"Name": "Firstname Lastname"
|
||||
}
|
||||
],
|
||||
"Id": "window-tagger",
|
||||
"Version": "1.0",
|
||||
"License": "GPLv3",
|
||||
"Website": "https://github.com/username/myscript"
|
||||
},
|
||||
"X-Plasma-API": "javascript",
|
||||
"X-Plasma-MainScript": "code/main.js",
|
||||
"KPackageStructure": "KWin/Script"
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
/home/tomas/.local/share/kwin/scripts
|
||||
@ -1,78 +0,0 @@
|
||||
[env]
|
||||
# TERM = "xterm-256color"
|
||||
|
||||
[font]
|
||||
size = 14
|
||||
normal = { family = "SF Mono", style = "Regular" }
|
||||
bold = { family = "SF Mono", style = "Bold" }
|
||||
italic = { family = "SF Mono", style = "Regular Italic" }
|
||||
bold_italic = { family = "SF Mono", style = "Bold Italic" }
|
||||
|
||||
# normal = { family = "Maple Mono", style = "Regular" }
|
||||
# bold = { family = "Maple Mono", style = "Bold" }
|
||||
# italic = { family = "Maple Mono", style = "Italic" }
|
||||
# bold_italic = { family = "Maple Mono", style = "Bold Italic" }
|
||||
# offset = { x = -1, y = 0 }
|
||||
|
||||
[window]
|
||||
padding = { x = 2, y = 0 }
|
||||
dynamic_padding = true
|
||||
# resize_increments = true
|
||||
|
||||
[keyboard]
|
||||
bindings = [
|
||||
# Create new window
|
||||
{ action = "SpawnNewInstance", key = "N", mods = "Command" },
|
||||
# Jump back one word
|
||||
{ key = "Left", mods = "Alt", chars = "\u001bb" },
|
||||
# Jump forward one word
|
||||
{ key = "Right", mods = "Alt", chars = "\u001bf" },
|
||||
# Move to start of line
|
||||
{ key = "Left", mods = "Command", chars = "\u0001" },
|
||||
# Move to end of line
|
||||
{ key = "Right", mods = "Command", chars = "\u0005" },
|
||||
# Delete backwards
|
||||
{ key = "Back", mods = "Alt", chars = "\u001B\u007F" }, # word
|
||||
{ key = "Back", mods = "Command", chars = "\u0015" }, # line
|
||||
# Delete forwards
|
||||
{ key = "Delete", mods = "Alt", chars = "\u001Bd" }, # word
|
||||
{ key = "Delete", mods = "Command", chars = "\u000B" } # line
|
||||
]
|
||||
|
||||
[scrolling]
|
||||
multiplier = 1
|
||||
|
||||
[general]
|
||||
live_config_reload = true
|
||||
|
||||
[colors.primary]
|
||||
background = "#eeeeee"
|
||||
foreground = "#444444"
|
||||
|
||||
[colors.cursor]
|
||||
text = "#eeeeee"
|
||||
cursor = "#005fff"
|
||||
|
||||
[colors.selection]
|
||||
text = "#434343"
|
||||
background = "#e0e0e0"
|
||||
|
||||
[colors.normal]
|
||||
black = "#000000"
|
||||
red = "#aa3731"
|
||||
green = "#448c27"
|
||||
yellow = "#cb9000"
|
||||
blue = "#325cc0"
|
||||
magenta = "#7a3e9d"
|
||||
cyan = "#0083b2"
|
||||
white = "#bbbbbb"
|
||||
|
||||
[colors.bright]
|
||||
black = "#000000"
|
||||
red = "#aa3731"
|
||||
green = "#448c27"
|
||||
yellow = "#cb9000"
|
||||
blue = "#325cc0"
|
||||
magenta = "#7a3e9d"
|
||||
cyan = "#0083b2"
|
||||
white = "#bbbbbb"
|
||||
@ -1,27 +0,0 @@
|
||||
[colors.primary]
|
||||
background = '#F7F7F7'
|
||||
foreground = '#434343'
|
||||
|
||||
[colors.cursor]
|
||||
text = '#F7F7F7'
|
||||
cursor = '#434343'
|
||||
|
||||
[colors.normal]
|
||||
black = '#000000'
|
||||
red = '#AA3731'
|
||||
green = '#448C27'
|
||||
yellow = '#CB9000'
|
||||
blue = '#325CC0'
|
||||
magenta = '#7A3E9D'
|
||||
cyan = '#0083B2'
|
||||
white = '#BBBBBB'
|
||||
|
||||
[colors.bright]
|
||||
black = '#777777'
|
||||
red = '#F05050'
|
||||
green = '#60CB00'
|
||||
yellow = '#FFBC5D'
|
||||
blue = '#007ACC'
|
||||
magenta = '#E64CE6'
|
||||
cyan = '#00AACB'
|
||||
white = '#FFFFFF'
|
||||
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
options=(
|
||||
order=above
|
||||
width=2.0
|
||||
hidpi=on
|
||||
active_color=0xff2b2b2b
|
||||
inactive_color=0xff2b2b2b
|
||||
# inactive_color=0x00000000
|
||||
whitelist="wezterm-gui"
|
||||
)
|
||||
|
||||
borders "${options[@]}"
|
||||
Binary file not shown.
@ -1,33 +0,0 @@
|
||||
#term = xterm-256color
|
||||
theme = Invero Day
|
||||
|
||||
# Font
|
||||
font-family = "Maple Mono"
|
||||
font-size = 13
|
||||
font-thicken = true
|
||||
font-thicken-strength = 120
|
||||
font-feature = -liga, -dlig, -calt
|
||||
|
||||
adjust-underline-thickness = 1
|
||||
adjust-strikethrough-thickness = 1
|
||||
adjust-overline-thickness = 1
|
||||
adjust-box-thickness = 1
|
||||
adjust-cell-width = -7%
|
||||
adjust-cell-height = -2
|
||||
|
||||
# Cursor
|
||||
cursor-style = block
|
||||
cursor-style-blink = false
|
||||
mouse-hide-while-typing = true
|
||||
shell-integration-features = ssh-terminfo,ssh-env,no-cursor
|
||||
|
||||
# Window
|
||||
window-height = 80
|
||||
window-width = 128
|
||||
window-padding-x = 4
|
||||
window-padding-y = 0
|
||||
window-padding-color = extend
|
||||
macos-titlebar-style = native
|
||||
macos-icon = custom
|
||||
|
||||
window-inherit-working-directory = false
|
||||
@ -1,22 +0,0 @@
|
||||
palette = 0=#444444
|
||||
palette = 1=#ff0000
|
||||
palette = 2=#00af5f
|
||||
palette = 3=#d75f00
|
||||
palette = 4=#005fff
|
||||
palette = 5=#5f5f87
|
||||
palette = 6=#afd7ff
|
||||
palette = 7=#eeeeee
|
||||
palette = 8=#444444
|
||||
palette = 9=#ff0000
|
||||
palette = 10=#00af5f
|
||||
palette = 11=#d75f00
|
||||
palette = 12=#005fff
|
||||
palette = 13=#5f5f87
|
||||
palette = 14=#afd7ff
|
||||
palette = 15=#eeeeee
|
||||
|
||||
background = #eeeeee
|
||||
foreground = #444444
|
||||
cursor-color = #005fff
|
||||
selection-background = #dadada
|
||||
selection-foreground = #444444
|
||||
@ -1,25 +0,0 @@
|
||||
brew "bash"
|
||||
brew "dnsmasq", restart_service: :changed
|
||||
brew "tmux"
|
||||
cask "brave-browser"
|
||||
cask "bruno"
|
||||
cask "dbeaver-community"
|
||||
cask "discord"
|
||||
cask "firefox"
|
||||
cask "font-maple-mono"
|
||||
cask "font-maple-mono-nf"
|
||||
cask "ghostty"
|
||||
cask "google-chrome"
|
||||
cask "karabiner-elements"
|
||||
cask "linearmouse"
|
||||
cask "macfuse"
|
||||
cask "orbstack"
|
||||
cask "proton-drive"
|
||||
cask "protonvpn"
|
||||
cask "rectangle"
|
||||
cask "slack"
|
||||
cask "sol"
|
||||
cask "spotify"
|
||||
cask "sublime-text"
|
||||
cask "utm@beta"
|
||||
cask "zoom"
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"profiles": [
|
||||
{
|
||||
"name": "Default profile",
|
||||
"selected": true,
|
||||
"simple_modifications": [
|
||||
{
|
||||
"from": { "key_code": "caps_lock" },
|
||||
"to": [{ "key_code": "left_control" }]
|
||||
}
|
||||
],
|
||||
"virtual_hid_keyboard": { "keyboard_type_v2": "iso" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
{
|
||||
"global": { "show_in_menu_bar": false },
|
||||
"profiles": [
|
||||
{
|
||||
"devices": [
|
||||
{
|
||||
"identifiers": {
|
||||
"is_keyboard": true,
|
||||
"product_id": 50475,
|
||||
"vendor_id": 1133
|
||||
},
|
||||
"ignore_vendor_events": true
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"is_keyboard": true,
|
||||
"product_id": 50504,
|
||||
"vendor_id": 1133
|
||||
},
|
||||
"ignore_vendor_events": true
|
||||
}
|
||||
],
|
||||
"name": "Default profile",
|
||||
"selected": true,
|
||||
"simple_modifications": [
|
||||
{
|
||||
"from": { "key_code": "caps_lock" },
|
||||
"to": [{ "key_code": "left_control" }]
|
||||
}
|
||||
],
|
||||
"virtual_hid_keyboard": { "keyboard_type_v2": "iso" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
# ~/.config/kitty/choose_tab.py
|
||||
from kitty.boss import get_boss
|
||||
from kittens.tui.handler import Handler
|
||||
from kittens.tui.loop import Loop
|
||||
|
||||
|
||||
class TabPicker(Handler):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
boss = get_boss()
|
||||
win = boss.active_window
|
||||
self.osw = win.os_window if win else None
|
||||
self.tabs = list(self.osw.tabs) if self.osw else []
|
||||
self.index = 0
|
||||
|
||||
def draw(self, screen):
|
||||
screen.clear()
|
||||
if not self.tabs:
|
||||
screen.write_line("No tabs. Esc to exit.")
|
||||
else:
|
||||
screen.write_line("Choose a tab (↑/↓ Enter Esc)")
|
||||
for i, t in enumerate(self.tabs):
|
||||
mark = "●" if t is self.osw.active_tab else " "
|
||||
sel = ">" if i == self.index else " "
|
||||
title = t.title or f"Tab {i+1}"
|
||||
screen.write_line(f"{sel} {mark} {title}")
|
||||
screen.refresh()
|
||||
|
||||
def on_key(self, event):
|
||||
if not self.tabs:
|
||||
if event.key in ("escape", "enter"):
|
||||
self.quit_loop()
|
||||
return
|
||||
k = event.key
|
||||
if k in ("up", "k"):
|
||||
self.index = (self.index - 1) % len(self.tabs)
|
||||
elif k in ("down", "j"):
|
||||
self.index = (self.index + 1) % len(self.tabs)
|
||||
elif k == "enter":
|
||||
self.osw.set_active_tab(self.tabs[self.index])
|
||||
self.quit_loop()
|
||||
elif k == "escape":
|
||||
self.quit_loop()
|
||||
self.refresh()
|
||||
|
||||
|
||||
def main(args):
|
||||
# Correct signature for older Kitty: pass the class name and a title string
|
||||
Loop(TabPicker, "choose_tab").run()
|
||||
|
||||
|
||||
def handle_result(args, answer, target_window_id, boss):
|
||||
pass
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
# vim:ft=kitty
|
||||
|
||||
background #eeeeee
|
||||
foreground #444444
|
||||
cursor #005fff
|
||||
cursor_text_color #eeeeee
|
||||
selection_background #dadada
|
||||
selection_foreground #444444
|
||||
url_color #005fff
|
||||
|
||||
# Tabs
|
||||
active_tab_background #005fff
|
||||
active_tab_foreground #eeeeee
|
||||
inactive_tab_background #dadada
|
||||
inactive_tab_foreground #9e9e9e
|
||||
|
||||
# normal
|
||||
color0 #444444
|
||||
color1 #ff0000
|
||||
color2 #00af5f
|
||||
color3 #d75f00
|
||||
color4 #005fff
|
||||
color5 #5f5f87
|
||||
color6 #afd7ff
|
||||
color7 #eeeeee
|
||||
|
||||
# bright
|
||||
color8 #444444
|
||||
color9 #ff0000
|
||||
color10 #00af5f
|
||||
color11 #d75f00
|
||||
color12 #005fff
|
||||
color13 #5f5f87
|
||||
color14 #afd7ff
|
||||
color15 #eeeeee
|
||||
Binary file not shown.
@ -1,73 +0,0 @@
|
||||
include invero.conf
|
||||
|
||||
# term xterm-256color
|
||||
enable_audio_bell no
|
||||
cursor_shape block
|
||||
wheel_scroll_multiplier 1.0
|
||||
touch_scroll_multiplier 1.0
|
||||
wheel_scroll_min_lines 1
|
||||
shell_integration no-cursor
|
||||
cursor_blink_interval 0
|
||||
|
||||
remember_window_position yes
|
||||
remember_window_size yes
|
||||
|
||||
# Font
|
||||
font_family Maple Mono
|
||||
font_size 13.0
|
||||
# disable_ligatures always
|
||||
|
||||
# undercurl_style thick-sparse
|
||||
|
||||
modify_font cell_width 94%
|
||||
modify_font cell_height -2px
|
||||
# modify_font baseline 2px
|
||||
|
||||
# modify_font underline_thickness 180%
|
||||
# modify_font underline_position 2px
|
||||
# modify_font strikethrough_positon 2px
|
||||
text_composition_strategy legacy
|
||||
# underline_exclusion 0
|
||||
|
||||
placement_strategy top
|
||||
window_margin_width 0 0
|
||||
window_padding_width 0 4
|
||||
|
||||
# modify_font cell_height -1
|
||||
# modify_font cell_width 90%
|
||||
|
||||
# Navigation / editing
|
||||
# Make Option act as Alt on macOS
|
||||
macos_option_as_alt yes
|
||||
|
||||
# Use explicit bytes (no ambiguity), not \x1bb etc.
|
||||
map opt+left send_text all \x1b\x62
|
||||
map opt+right send_text all \x1b\x66
|
||||
map cmd+left send_text all \x01
|
||||
map cmd+right send_text all \x05
|
||||
map opt+backspace send_text all \x1b\x7f
|
||||
map cmd+backspace send_text all \x15
|
||||
map opt+delete send_text all \x1b\x64
|
||||
map cmd+delete send_text all \x0b
|
||||
|
||||
# New window / tab
|
||||
map cmd+n new_os_window
|
||||
map cmd+t new_tab
|
||||
|
||||
map cmd+1 goto_tab 1
|
||||
map cmd+2 goto_tab 2
|
||||
map cmd+3 goto_tab 3
|
||||
map cmd+4 goto_tab 4
|
||||
map cmd+5 goto_tab 5
|
||||
map cmd+6 goto_tab 6
|
||||
map cmd+7 goto_tab 7
|
||||
map cmd+8 goto_tab 8
|
||||
map cmd+9 goto_tab 9
|
||||
|
||||
|
||||
# BEGIN_KITTY_FONTS
|
||||
# font_family family="JetBrains Mono"
|
||||
bold_font auto
|
||||
italic_font auto
|
||||
bold_italic_font auto
|
||||
# END_KITTY_FONTS
|
||||
@ -1,44 +0,0 @@
|
||||
{
|
||||
"$schema": "https:\/\/schema.linearmouse.app\/0.10.0",
|
||||
"schemes": [
|
||||
{
|
||||
"if" : {
|
||||
"device" : {
|
||||
"vendorID" : "0x46d",
|
||||
"productID" : "0xc52b",
|
||||
"productName" : "USB Receiver",
|
||||
"category" : "mouse"
|
||||
}
|
||||
},
|
||||
"scrolling": {
|
||||
"reverse": {
|
||||
"vertical": true
|
||||
},
|
||||
"speed": {
|
||||
"vertical": 0
|
||||
},
|
||||
"acceleration": {
|
||||
"vertical": 1
|
||||
},
|
||||
"distance": {
|
||||
"vertical": "100px"
|
||||
},
|
||||
"modifiers": {
|
||||
"vertical": {
|
||||
"command": {
|
||||
"type": "preventDefault"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"universalBackForward": true
|
||||
},
|
||||
"pointer": {
|
||||
"acceleration": 0.3,
|
||||
"speed": 0.2,
|
||||
"disableAcceleration": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,286 +0,0 @@
|
||||
{
|
||||
"bundleId" : "com.knollsoft.Rectangle",
|
||||
"defaults" : {
|
||||
"SUEnableAutomaticChecks" : {
|
||||
"bool" : true
|
||||
},
|
||||
"allowAnyShortcut" : {
|
||||
"bool" : true
|
||||
},
|
||||
"almostMaximizeHeight" : {
|
||||
"float" : 0
|
||||
},
|
||||
"almostMaximizeWidth" : {
|
||||
"float" : 0
|
||||
},
|
||||
"altThirdCycle" : {
|
||||
"int" : 0
|
||||
},
|
||||
"alternateDefaultShortcuts" : {
|
||||
"bool" : true
|
||||
},
|
||||
"alwaysAccountForStage" : {
|
||||
"int" : 0
|
||||
},
|
||||
"applyGapsToMaximize" : {
|
||||
"int" : 0
|
||||
},
|
||||
"applyGapsToMaximizeHeight" : {
|
||||
"int" : 0
|
||||
},
|
||||
"attemptMatchOnNextPrevDisplay" : {
|
||||
"int" : 0
|
||||
},
|
||||
"autoMaximize" : {
|
||||
"int" : 0
|
||||
},
|
||||
"cascadeAllDeltaSize" : {
|
||||
"float" : 30
|
||||
},
|
||||
"centerHalfCycles" : {
|
||||
"int" : 0
|
||||
},
|
||||
"centeredDirectionalMove" : {
|
||||
"int" : 0
|
||||
},
|
||||
"cornerSnapAreaSize" : {
|
||||
"float" : 20
|
||||
},
|
||||
"curtainChangeSize" : {
|
||||
"int" : 0
|
||||
},
|
||||
"cycleSizesIsChanged" : {
|
||||
"bool" : false
|
||||
},
|
||||
"disabledApps" : {
|
||||
|
||||
},
|
||||
"doubleClickTitleBar" : {
|
||||
"int" : 0
|
||||
},
|
||||
"doubleClickTitleBarIgnoredApps" : {
|
||||
|
||||
},
|
||||
"doubleClickTitleBarRestore" : {
|
||||
"int" : 0
|
||||
},
|
||||
"dragFromStage" : {
|
||||
"int" : 0
|
||||
},
|
||||
"enhancedUI" : {
|
||||
"int" : 1
|
||||
},
|
||||
"footprintAlpha" : {
|
||||
"float" : 0.3
|
||||
},
|
||||
"footprintAnimationDurationMultiplier" : {
|
||||
"float" : 0
|
||||
},
|
||||
"footprintBorderWidth" : {
|
||||
"float" : 2
|
||||
},
|
||||
"footprintColor" : {
|
||||
|
||||
},
|
||||
"footprintFade" : {
|
||||
"int" : 0
|
||||
},
|
||||
"fullIgnoreBundleIds" : {
|
||||
|
||||
},
|
||||
"gapSize" : {
|
||||
"float" : 0
|
||||
},
|
||||
"hapticFeedbackOnSnap" : {
|
||||
"int" : 0
|
||||
},
|
||||
"hideMenubarIcon" : {
|
||||
"bool" : false
|
||||
},
|
||||
"ignoreDragSnapToo" : {
|
||||
"int" : 0
|
||||
},
|
||||
"ignoredSnapAreas" : {
|
||||
"int" : 0
|
||||
},
|
||||
"landscapeSnapAreas" : {
|
||||
"string" : "[4,{\"compound\":-2},1,{\"action\":15},2,{\"action\":2},6,{\"action\":13},7,{\"compound\":-4},8,{\"action\":14},3,{\"action\":16},5,{\"compound\":-3}]"
|
||||
},
|
||||
"launchOnLogin" : {
|
||||
"bool" : true
|
||||
},
|
||||
"minimumWindowHeight" : {
|
||||
"float" : 0
|
||||
},
|
||||
"minimumWindowWidth" : {
|
||||
"float" : 0
|
||||
},
|
||||
"missionControlDragging" : {
|
||||
"int" : 0
|
||||
},
|
||||
"missionControlDraggingAllowedOffscreenDistance" : {
|
||||
"float" : 25
|
||||
},
|
||||
"missionControlDraggingDisallowedDuration" : {
|
||||
"int" : 250
|
||||
},
|
||||
"moveCursor" : {
|
||||
"int" : 0
|
||||
},
|
||||
"moveCursorAcrossDisplays" : {
|
||||
"int" : 0
|
||||
},
|
||||
"notifiedOfProblemApps" : {
|
||||
"bool" : false
|
||||
},
|
||||
"obtainWindowOnClick" : {
|
||||
"int" : 0
|
||||
},
|
||||
"portraitSnapAreas" : {
|
||||
|
||||
},
|
||||
"relaunchOpensMenu" : {
|
||||
"bool" : false
|
||||
},
|
||||
"resizeOnDirectionalMove" : {
|
||||
"bool" : false
|
||||
},
|
||||
"screenEdgeGapBottom" : {
|
||||
"float" : 0
|
||||
},
|
||||
"screenEdgeGapLeft" : {
|
||||
"float" : 0
|
||||
},
|
||||
"screenEdgeGapRight" : {
|
||||
"float" : 0
|
||||
},
|
||||
"screenEdgeGapTop" : {
|
||||
"float" : 0
|
||||
},
|
||||
"screenEdgeGapTopNotch" : {
|
||||
"float" : 0
|
||||
},
|
||||
"screenEdgeGapsOnMainScreenOnly" : {
|
||||
"bool" : false
|
||||
},
|
||||
"selectedCycleSizes" : {
|
||||
"int" : 0
|
||||
},
|
||||
"shortEdgeSnapAreaSize" : {
|
||||
"float" : 145
|
||||
},
|
||||
"showAllActionsInMenu" : {
|
||||
"int" : 0
|
||||
},
|
||||
"sixthsSnapArea" : {
|
||||
"int" : 0
|
||||
},
|
||||
"sizeOffset" : {
|
||||
"float" : 0
|
||||
},
|
||||
"snapEdgeMarginBottom" : {
|
||||
"float" : 5
|
||||
},
|
||||
"snapEdgeMarginLeft" : {
|
||||
"float" : 5
|
||||
},
|
||||
"snapEdgeMarginRight" : {
|
||||
"float" : 5
|
||||
},
|
||||
"snapEdgeMarginTop" : {
|
||||
"float" : 5
|
||||
},
|
||||
"snapModifiers" : {
|
||||
"int" : 0
|
||||
},
|
||||
"specifiedHeight" : {
|
||||
"float" : 1050
|
||||
},
|
||||
"specifiedWidth" : {
|
||||
"float" : 1680
|
||||
},
|
||||
"stageSize" : {
|
||||
"float" : 190
|
||||
},
|
||||
"subsequentExecutionMode" : {
|
||||
"int" : 1
|
||||
},
|
||||
"systemWideMouseDown" : {
|
||||
"int" : 0
|
||||
},
|
||||
"systemWideMouseDownApps" : {
|
||||
|
||||
},
|
||||
"todo" : {
|
||||
"int" : 0
|
||||
},
|
||||
"todoApplication" : {
|
||||
|
||||
},
|
||||
"todoMode" : {
|
||||
"bool" : false
|
||||
},
|
||||
"todoSidebarSide" : {
|
||||
"int" : 1
|
||||
},
|
||||
"todoSidebarWidth" : {
|
||||
"float" : 400
|
||||
},
|
||||
"traverseSingleScreen" : {
|
||||
"int" : 0
|
||||
},
|
||||
"unsnapRestore" : {
|
||||
"int" : 0
|
||||
},
|
||||
"windowSnapping" : {
|
||||
"int" : 0
|
||||
}
|
||||
},
|
||||
"shortcuts" : {
|
||||
"bottomHalf" : {
|
||||
"keyCode" : 38,
|
||||
"modifierFlags" : 786432
|
||||
},
|
||||
"firstThird" : {
|
||||
"keyCode" : 33,
|
||||
"modifierFlags" : 786432
|
||||
},
|
||||
"firstTwoThirds" : {
|
||||
"keyCode" : 33,
|
||||
"modifierFlags" : 917504
|
||||
},
|
||||
"lastThird" : {
|
||||
"keyCode" : 30,
|
||||
"modifierFlags" : 786432
|
||||
},
|
||||
"lastTwoThirds" : {
|
||||
"keyCode" : 30,
|
||||
"modifierFlags" : 917504
|
||||
},
|
||||
"leftHalf" : {
|
||||
"keyCode" : 4,
|
||||
"modifierFlags" : 786432
|
||||
},
|
||||
"maximize" : {
|
||||
"keyCode" : 36,
|
||||
"modifierFlags" : 786432
|
||||
},
|
||||
"reflowTodo" : {
|
||||
"keyCode" : 45,
|
||||
"modifierFlags" : 786432
|
||||
},
|
||||
"rightHalf" : {
|
||||
"keyCode" : 37,
|
||||
"modifierFlags" : 786432
|
||||
},
|
||||
"toggleTodo" : {
|
||||
"keyCode" : 11,
|
||||
"modifierFlags" : 786432
|
||||
},
|
||||
"topHalf" : {
|
||||
"keyCode" : 40,
|
||||
"modifierFlags" : 786432
|
||||
}
|
||||
},
|
||||
"version" : "92"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,36 +0,0 @@
|
||||
[colors]
|
||||
background = "#eeeeee"
|
||||
foreground = "#444444"
|
||||
cursor_bg = "#005fff"
|
||||
cursor_border = "#9e9e9e"
|
||||
cursor_fg = "#eeeeee"
|
||||
selection_bg = "#dadada"
|
||||
selection_fg = "#444444"
|
||||
split = "#005fff"
|
||||
compose_cursor = "#d75f00"
|
||||
scrollbar_thumb = "#9e9e9e"
|
||||
|
||||
copy_mode_active_highlight_bg = { Color = "#dadada" }
|
||||
copy_mode_active_highlight_fg = { Color = "#d75f00" }
|
||||
copy_mode_inactive_highlight_bg = { Color = "#eeeeee" }
|
||||
copy_mode_inactive_highlight_fg = { Color = "#d75f00" }
|
||||
|
||||
ansi = ["#444444", "#ff0000", "#00af5f", "#d75f00", "#005fff", "#5f5f87", "#afd7ff", "#eeeeee"]
|
||||
brights = ["#444444", "#ff0000", "#00af5f", "#d75f00", "#005fff", "#5f5f87", "#afd7ff", "#eeeeee"]
|
||||
|
||||
[colors.tab_bar]
|
||||
inactive_tab_edge = "#ff0000"
|
||||
background = "#444444"
|
||||
|
||||
[colors.tab_bar.active_tab]
|
||||
fg_color = "#eeeeee"
|
||||
bg_color = "#444444"
|
||||
intensity = "Bold"
|
||||
|
||||
[colors.tab_bar.inactive_tab]
|
||||
fg_color = "#9e9e9e"
|
||||
bg_color = "#444444"
|
||||
|
||||
[colors.tab_bar.inactive_tab_hover]
|
||||
fg_color = "#dadada"
|
||||
bg_color = "#444444"
|
||||
@ -1,72 +0,0 @@
|
||||
local wezterm = require("wezterm")
|
||||
local config = wezterm.config_builder()
|
||||
local act = wezterm.action
|
||||
|
||||
-- General
|
||||
config.term = "wezterm"
|
||||
config.color_scheme = "Invero Day"
|
||||
config.use_ime = false
|
||||
|
||||
-- Font
|
||||
config.font = wezterm.font({ family = "Maple Mono NF", weight = "Medium" })
|
||||
config.font_size = 13
|
||||
config.harfbuzz_features = { "calt=0", "clig=0", "liga=0" } -- disables alternates and ligatures
|
||||
config.underline_position = -4
|
||||
config.underline_thickness = 3
|
||||
|
||||
-- Appearance
|
||||
config.bold_brightens_ansi_colors = false
|
||||
config.window_padding = { left = "0.5cell", right = "0.5cell", top = 6, bottom = 0 }
|
||||
config.window_content_alignment = { horizontal = "Center", vertical = "Top" }
|
||||
config.cell_width = 0.9
|
||||
config.line_height = 0.9
|
||||
|
||||
-- Tabs
|
||||
config.use_fancy_tab_bar = false
|
||||
config.show_new_tab_button_in_tab_bar = false
|
||||
config.hide_tab_bar_if_only_one_tab = true
|
||||
|
||||
-- Events
|
||||
wezterm.on("toggle-tabbar", function(window, _)
|
||||
local overrides = window:get_config_overrides() or {}
|
||||
if overrides.enable_tab_bar == false then
|
||||
wezterm.log_info("tab bar shown")
|
||||
overrides.enable_tab_bar = true
|
||||
else
|
||||
wezterm.log_info("tab bar hidden")
|
||||
overrides.enable_tab_bar = false
|
||||
end
|
||||
window:set_config_overrides(overrides)
|
||||
end)
|
||||
|
||||
-- Keybindings
|
||||
config.keys = {
|
||||
{ mods = "OPT", key = "LeftArrow", action = act.SendString("\x1bb") }, -- Jump back one word
|
||||
{ mods = "OPT", key = "RightArrow", action = act.SendString("\x1bf") }, -- Jump forward one word
|
||||
{ mods = "CMD", key = "LeftArrow", action = act.SendString("\x01") }, -- Move to start of line
|
||||
{ mods = "CMD", key = "RightArrow", action = act.SendString("\x05") }, -- Move to end of line
|
||||
{ mods = "OPT", key = "Backspace", action = act.SendString("\x1b\x7f") }, -- Delete previous word
|
||||
{ mods = "CMD", key = "Backspace", action = act.SendString("\x15") }, -- Delete previous line
|
||||
{ mods = "OPT", key = "Delete", action = act.SendString("\x1bd") }, -- Delete next word
|
||||
{ mods = "CMD", key = "Delete", action = act.SendString("\x0b") }, -- Delete next line
|
||||
{ mods = "CMD", key = "n", action = act.SpawnWindow }, -- New window
|
||||
{ mods = "CMD", key = "t", action = act.SpawnCommandInNewTab({ cwd = wezterm.home_dir }) }, -- New tab
|
||||
{ mods = "SUPER|SHIFT", key = "LeftArrow", action = act({ MoveTabRelative = -1 }) }, -- Move tab left
|
||||
{ mods = "SUPER|SHIFT", key = "RightArrow", action = act({ MoveTabRelative = 1 }) }, -- Move tab right
|
||||
{ mods = "SUPER|SHIFT", key = "b", action = act.EmitEvent("toggle-tabbar") },
|
||||
{ mods = "SUPER|SHIFT", key = "o", action = wezterm.action.ShowTabNavigator },
|
||||
{
|
||||
mods = "SUPER|SHIFT",
|
||||
key = "r",
|
||||
action = wezterm.action.PromptInputLine({
|
||||
description = "Enter new tab title",
|
||||
action = wezterm.action_callback(function(window, pane, line)
|
||||
if line then
|
||||
window:active_tab():set_title(line)
|
||||
end
|
||||
end),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit d0344b6c564a5d6b00bccd73caefcda42229d70b
|
||||
@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
/home/tomas/bin/dev "$@" 2>&1
|
||||
exit_code=$?
|
||||
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Command: $*"
|
||||
echo "Failed: exit $exit_code"
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
@ -1,612 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
source "$HOME/.local/bin/barg"
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
SPEC=(
|
||||
"command;flow;DevFlow CLI - Manage instances and development containers"
|
||||
"note;Use 'flow <command> --help' for command-specific options"
|
||||
|
||||
"command;enter;Connect to a development instance via SSH"
|
||||
"note;Target format: [user@]namespace@platform (e.g., 'personal@orb' or 'root@personal@orb')"
|
||||
"argument;user,u;type:option;help:SSH user (overrides user in target)"
|
||||
"argument;namespace,n;type:option;help:Instance namespace (overrides namespace in target)"
|
||||
"argument;platform,p;type:option;help:Platform name (overrides platform in target)"
|
||||
"argument;session,s;type:option;default:default;help:Development session name (default: 'default')"
|
||||
"argument;no-tmux;type:flag;dest:no_tmux;default:false;help:Skip tmux attachment on connection"
|
||||
"argument;dry-run,d;type:flag;dest:dry_run;default:false;help:Show SSH command without executing"
|
||||
"argument;target,t;required;help:Target instance in format [user@]namespace@platform"
|
||||
"argument;ssh-args;type:rest;dest:ssh_args;help:Additional SSH arguments (after --)"
|
||||
"end"
|
||||
|
||||
"command;sync;Git tools"
|
||||
"command;check;Check all projects status"
|
||||
"end"
|
||||
"end"
|
||||
|
||||
"command;dotfiles;Manage repository dotfiles and git submodules"
|
||||
"command;init;Initialize and set up all submodules"
|
||||
"note;Equivalent to: git submodule update --init --recursive"
|
||||
"end"
|
||||
"command;pull;Update all submodules to latest remote commits"
|
||||
"note;Equivalent to: git submodule update --remote --recursive"
|
||||
"end"
|
||||
"command;urls;Synchronize submodule URLs"
|
||||
"note;Equivalent to: git submodule sync --recursive"
|
||||
"end"
|
||||
"command;reset;Reset submodules to the recorded commits"
|
||||
"note;Equivalent to: git submodule update --init --recursive --force"
|
||||
"end"
|
||||
"command;status;Show current submodule status"
|
||||
"note;Equivalent to: git submodule status --recursive"
|
||||
"end"
|
||||
"command;all;Run URLs sync, initialization, and remote update in one step"
|
||||
"note;Equivalent to: git submodule sync --recursive && git submodule update --init --recursive && git submodule update --remote --recursive"
|
||||
"end"
|
||||
"end"
|
||||
|
||||
"command;create;Create and start a new development container"
|
||||
"argument;image,i;required;type:option;help:Container image to use (with optional tag)"
|
||||
"argument;project,p;type:option;help:Path to local project directory"
|
||||
"argument;name;required;help:Container name"
|
||||
"end"
|
||||
|
||||
"command;exec;Execute a command or open a shell in a container"
|
||||
"argument;name;required;help:Container name"
|
||||
"argument;cmd;type:rest;help:Command to execute inside container (after --)"
|
||||
"end"
|
||||
|
||||
"command;connect;Attach or switch to the container’s tmux session"
|
||||
"note;When already inside tmux, switches to the target session instead of reattaching."
|
||||
"note;New tmux panes or windows in the session automatically start inside the container."
|
||||
"argument;from,f;type:option;dest:name;help:Optional source container name"
|
||||
"argument;name;required;help:Target container name"
|
||||
"end"
|
||||
|
||||
"command;list;Display all development containers and their status"
|
||||
"end"
|
||||
|
||||
"command;stop;Stop or kill a running development container"
|
||||
"argument;from;type:option;dest:name;help:Optional source container name"
|
||||
"argument;kill;type:flag;help:Use kill instead of graceful stop"
|
||||
"argument;name;required;help:Target container name"
|
||||
"end"
|
||||
|
||||
"command;remove,rm;Remove a development container"
|
||||
"argument;from;type:option;dest:name;help:Optional source container name"
|
||||
"argument;force,f;type:flag;help:Force removal of container"
|
||||
"argument;name;required;help:Target container name"
|
||||
"end"
|
||||
|
||||
"command;respawn;Restart all tmux panes for a development session"
|
||||
"argument;from;type:option;dest:name;help:Optional source container name"
|
||||
"argument;name;required;help:Session or container name"
|
||||
"end"
|
||||
|
||||
"command;test;Verify that the dev script is functioning"
|
||||
"argument;from;type:option;dest:name;help:Optional source container name"
|
||||
"argument;name;help:Target container name"
|
||||
"end"
|
||||
|
||||
"end"
|
||||
)
|
||||
|
||||
DEFAULT_REGISTRY="registry.tomastm.com"
|
||||
DEFAULT_TAG="latest"
|
||||
PROJECT_DIR="$HOME/projects"
|
||||
PROJECT_ABBR="p"
|
||||
|
||||
fail() {
|
||||
printf 'Error: %b\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
resolve_path() {
|
||||
local path="${1:-$(dirname "${BASH_SOURCE[0]}")}"
|
||||
if command -v realpath >/dev/null 2>&1; then
|
||||
realpath "$path"
|
||||
else
|
||||
echo "$(cd "$(dirname "$path")" && pwd)/$(basename "$path")"
|
||||
fi
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2178,SC2128
|
||||
parse_image_ref() {
|
||||
local input="$1"
|
||||
|
||||
local image_ref registry repo tag label
|
||||
|
||||
if [[ $input == */* ]]; then
|
||||
local prefix="${input%%/*}"
|
||||
if [[ "$prefix" == "docker" ]]; then
|
||||
input="docker.io/library/${input#*/}"
|
||||
elif [[ "$prefix" == "tm0" ]]; then
|
||||
input="${DEFAULT_REGISTRY}/${input#*/}"
|
||||
fi
|
||||
|
||||
registry="${input%%/*}"
|
||||
input=${input#*/}
|
||||
else
|
||||
registry="$DEFAULT_REGISTRY"
|
||||
fi
|
||||
|
||||
if [[ "${input##*/}" == *:* ]]; then
|
||||
tag="${input##*:}"
|
||||
input="${input%:*}"
|
||||
else
|
||||
tag="$DEFAULT_TAG"
|
||||
fi
|
||||
|
||||
repo="${registry}/${input}"
|
||||
repo="${repo#*/}"
|
||||
image_ref="${registry}/${repo}:${tag}"
|
||||
|
||||
label="${registry%.*}"
|
||||
label="${label##*.}/${repo##*/}"
|
||||
|
||||
echo "$image_ref $repo $tag $label"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
docker_container_exists() {
|
||||
local cname="$(get_cname)"
|
||||
docker container ls -a --format '{{.Names}}' | grep -Fqx "$cname"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
docker_container_running() {
|
||||
local cname="$(get_cname)"
|
||||
docker container ls --format '{{.Names}}' | grep -Fqx "$cname"
|
||||
}
|
||||
|
||||
docker_image_present() {
|
||||
local ref="$1"
|
||||
docker image inspect "$ref" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
get_cname() {
|
||||
printf "%s" "dev-${name_arg#dev-}"
|
||||
}
|
||||
|
||||
cmd() {
|
||||
barg_usage
|
||||
}
|
||||
|
||||
cmd_dotfiles_init() {
|
||||
echo "[dotfiles] Initializing submodules..."
|
||||
git submodule update --init --recursive
|
||||
}
|
||||
|
||||
cmd_dotfiles_pull() {
|
||||
echo "[dotfiles] Updating submodules to latest remote commits..."
|
||||
git submodule update --remote --recursive
|
||||
}
|
||||
|
||||
cmd_dotfiles_urls() {
|
||||
echo "[dotfiles] Syncing submodule URLs..."
|
||||
git submodule sync --recursive
|
||||
}
|
||||
|
||||
cmd_dotfiles_reset() {
|
||||
echo "[dotfiles] Resetting submodules to recorded commits..."
|
||||
git submodule update --init --recursive --force
|
||||
}
|
||||
|
||||
cmd_dotfiles_status() {
|
||||
echo "[dotfiles] Submodule status:"
|
||||
git submodule status --recursive
|
||||
}
|
||||
|
||||
cmd_dotfiles_all() {
|
||||
echo "[dotfiles] Syncing URLs..."
|
||||
git submodule sync --recursive
|
||||
|
||||
echo "[dotfiles] Initializing submodules..."
|
||||
git submodule update --init --recursive
|
||||
|
||||
echo "[dotfiles] Updating submodules to latest remote commits..."
|
||||
git submodule update --remote --recursive
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
cmd_enter() {
|
||||
# VARS: user_arg, namespace_arg, platform_arg, target_arg, session_arg, no_tmux_arg, dry_run_arg, ssh_args_arg
|
||||
|
||||
# Do not run inside instance
|
||||
if [[ -n "$DF_NAMESPACE" && -n "$DF_PLATFORM" ]]; then
|
||||
fail "It is not recommended to run this command inside an instance.\nCurrently inside: $(tput bold)${DF_NAMESPACE}@${DF_PLATFORM}$(tput sgr0)"
|
||||
fi
|
||||
|
||||
local -A CONFIG_HOST=(
|
||||
[orb.host]="<namespace>@orb"
|
||||
[utm.host]="<namespace>.utm.local"
|
||||
[core.host]="<namespace>.core.lan"
|
||||
)
|
||||
|
||||
local df_platform=""
|
||||
local df_namespace=""
|
||||
local df_user=""
|
||||
|
||||
# Parse target: get user, namespace, platform
|
||||
if [[ "$target_arg" == *@* ]]; then
|
||||
df_platform="${target_arg##*@}"
|
||||
target_arg="${target_arg%@*}"
|
||||
fi
|
||||
|
||||
if [[ "$target_arg" == *@* ]]; then
|
||||
df_namespace="${target_arg##*@}"
|
||||
df_user="${target_arg%@*}"
|
||||
else
|
||||
df_namespace="${target_arg}"
|
||||
df_user="${USER}"
|
||||
fi
|
||||
|
||||
if [[ -n "$platform_arg" ]]; then
|
||||
df_platform="$platform_arg"
|
||||
fi
|
||||
if [[ -n "$namespace_arg" ]]; then
|
||||
df_namespace="$namespace_arg"
|
||||
fi
|
||||
if [[ -n "$user_arg" ]]; then
|
||||
df_user="$user_arg"
|
||||
fi
|
||||
|
||||
# Resolve host, identity (maybe check what would the host be in order to use .ssh/config)
|
||||
local host_config="${CONFIG_HOST[${df_platform}.host]}"
|
||||
local ssh_host="${host_config//<namespace>/$df_namespace}"
|
||||
if [[ -z "$ssh_host" ]]; then
|
||||
fail "Invalid platform: ${df_platform}"
|
||||
fi
|
||||
|
||||
# Build ssh cmd: ssh + identity + tmux + envs
|
||||
local ssh_cmd=(ssh -tt "${df_user}@${ssh_host}")
|
||||
|
||||
if [[ "$no_tmux_arg" == "false" ]]; then
|
||||
# TODO: instead of tmux,maybe use "flow" in order to attach to dev container too
|
||||
ssh_cmd+=("tmux" "new-session" "-As" "$session_arg"
|
||||
"-e" "DF_NAMESPACE=$df_namespace"
|
||||
"-e" "DF_PLATFORM=$df_platform")
|
||||
fi
|
||||
|
||||
# Run or dryrun?
|
||||
if [[ "$dry_run_arg" == "true" ]]; then
|
||||
echo "Dry run command:"
|
||||
printf '%q ' "${ssh_cmd[@]}"
|
||||
echo
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec "${ssh_cmd[@]}"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
cmd_sync_check() {
|
||||
local base_dir="$HOME/projects"
|
||||
local -a needs_action=()
|
||||
local -a not_git=()
|
||||
|
||||
for repo in "$base_dir"/*; do
|
||||
[ -d "$repo" ] || continue
|
||||
local git_dir="$repo/.git"
|
||||
|
||||
if [ ! -d "$git_dir" ]; then
|
||||
not_git+=("$(basename "$repo")")
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "=== $(basename "$repo") ==="
|
||||
cd "$repo" || continue
|
||||
|
||||
local action_required=0
|
||||
|
||||
git fetch --all --quiet || true
|
||||
local branch
|
||||
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "HEAD")
|
||||
|
||||
# --- Uncommitted or untracked changes ---
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
echo "Uncommitted changes:"
|
||||
git status --short
|
||||
action_required=1
|
||||
elif [ -n "$(git ls-files --others --exclude-standard)" ]; then
|
||||
echo "Untracked files:"
|
||||
git ls-files --others --exclude-standard
|
||||
action_required=1
|
||||
else
|
||||
echo "No uncommitted or untracked changes."
|
||||
fi
|
||||
|
||||
# --- Unpushed commits on current branch ---
|
||||
if git rev-parse --abbrev-ref "${branch}@{u}" >/dev/null 2>&1; then
|
||||
local unpushed
|
||||
unpushed=$(git rev-list --oneline "${branch}@{u}..${branch}")
|
||||
if [ -n "$unpushed" ]; then
|
||||
echo "Unpushed commits on ${branch}:"
|
||||
echo "$unpushed"
|
||||
action_required=1
|
||||
else
|
||||
echo "No unpushed commits on ${branch}."
|
||||
fi
|
||||
else
|
||||
echo "No upstream set for ${branch}."
|
||||
action_required=1
|
||||
fi
|
||||
|
||||
# --- Unpushed branches ---
|
||||
local unpushed_branches=()
|
||||
while IFS= read -r b; do
|
||||
if git rev-parse --abbrev-ref "${b}@{u}" >/dev/null 2>&1; then
|
||||
local ahead
|
||||
ahead=$(git rev-list --count "${b}@{u}..${b}")
|
||||
if [ "$ahead" -gt 0 ]; then
|
||||
unpushed_branches+=("$b ($ahead ahead)")
|
||||
fi
|
||||
else
|
||||
unpushed_branches+=("$b (no upstream)")
|
||||
fi
|
||||
done < <(git for-each-ref --format='%(refname:short)' refs/heads)
|
||||
|
||||
if [ "${#unpushed_branches[@]}" -gt 0 ]; then
|
||||
echo "Unpushed branches:"
|
||||
printf ' %s\n' "${unpushed_branches[@]}"
|
||||
action_required=1
|
||||
else
|
||||
echo "No unpushed branches."
|
||||
fi
|
||||
|
||||
echo
|
||||
((action_required)) && needs_action+=("$(basename "$repo")")
|
||||
done
|
||||
|
||||
echo "=== SUMMARY ==="
|
||||
if [ "${#needs_action[@]}" -gt 0 ]; then
|
||||
echo "Projects needing action:"
|
||||
printf ' %s\n' "${needs_action[@]}" | sort -u
|
||||
else
|
||||
echo "All repositories clean and synced."
|
||||
fi
|
||||
|
||||
if [ "${#not_git[@]}" -gt 0 ]; then
|
||||
echo
|
||||
echo "Directories without Git repositories:"
|
||||
printf ' %s\n' "${not_git[@]}" | sort -u
|
||||
fi
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
cmd_create() {
|
||||
# VARS: name_arg, image_arg, project_arg
|
||||
|
||||
# Check if container name already exists
|
||||
local cname="$(get_cname)"
|
||||
if docker_container_exists "$cname"; then
|
||||
printf -v msg 'Container already exists: "%s" (from name "%s")' "$cname" "$name_arg"
|
||||
fail "$msg"
|
||||
fi
|
||||
|
||||
# Check if project path is valid
|
||||
local project_path
|
||||
project_path="$(resolve_path "$project_arg")"
|
||||
if [[ ! -d "$project_path" ]]; then
|
||||
fail "Invalid project path: $project_path"
|
||||
fi
|
||||
|
||||
# Check image
|
||||
IFS=' ' read -r image_ref _ _ _ <<<"$(parse_image_ref "$image_arg")"
|
||||
if ! docker_image_present "$image_ref"; then
|
||||
printf -v msg 'Image not found locally.\nTry:\n\t- docker pull %s' "$image_ref"
|
||||
fail "$msg"
|
||||
fi
|
||||
|
||||
# Run (= create and start container)
|
||||
cmd=(
|
||||
docker run -d
|
||||
--name "$cname"
|
||||
--label dev=true
|
||||
--label "dev.name=$name_arg"
|
||||
--label "dev.project_path=$project_path"
|
||||
--label "dev.image_ref=$image_ref"
|
||||
--network host
|
||||
--init # run tini as PID 1 to handle signals & reap zombies for cleaner container shutdown
|
||||
-v "$project_path:/workspace"
|
||||
-v /var/run/docker.sock:/var/run/docker.sock
|
||||
)
|
||||
|
||||
[[ -d "$HOME/.ssh" ]] && cmd+=(-v "$HOME/.ssh:$CONTAINER_HOME/.ssh:ro")
|
||||
[[ -f "$HOME/.npmrc" ]] && cmd+=(-v "$HOME/.npmrc:$CONTAINER_HOME/.npmrc:ro")
|
||||
[[ -d "$HOME/.npm" ]] && cmd+=(-v "$HOME/.npm:$CONTAINER_HOME/.npm")
|
||||
|
||||
docker_gid="$(getent group docker | cut -d: -f3 || true)"
|
||||
[[ -n "$docker_gid" ]] && cmd+=(--group-add "$docker_gid")
|
||||
|
||||
cmd+=("$image_ref" sleep infinity)
|
||||
"${cmd[@]}"
|
||||
|
||||
printf "Created and started container: %s" "$cname"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
cmd_connect() {
|
||||
# VARS: name_arg
|
||||
|
||||
local cname="$(get_cname)"
|
||||
if ! docker_container_exists "$cname"; then
|
||||
fail "Container does not exist: ${cname}. Run: dev create ..."
|
||||
fi
|
||||
|
||||
if ! docker_container_running "$cname"; then
|
||||
docker start "$cname" >/dev/null
|
||||
fi
|
||||
|
||||
if ! command -v tmux >/dev/null 2>&1; then
|
||||
echo "tmux not found; falling back to direct exec"
|
||||
exec "$0" exec "$cname"
|
||||
fi
|
||||
|
||||
local image_ref
|
||||
image_ref="$(docker container inspect "$cname" --format '{{ .Config.Image }}')"
|
||||
IFS=' ' read -r _image_ref _ _ image_label <<<"$(parse_image_ref "$image_ref")"
|
||||
|
||||
if ! tmux has-session -t "$cname" 2>/dev/null; then
|
||||
tmux new-session -ds "$cname" \
|
||||
-e "DF_IMAGE=$image_label" \
|
||||
-e "DF_NAMESPACE=$DF_NAMESPACE" \
|
||||
-e "DF_PLATFORM=$DF_PLATFORM" \
|
||||
"$0 exec \"$name_arg\""
|
||||
tmux set-option -t "$cname" default-command "$0 exec \"$name_arg\""
|
||||
fi
|
||||
|
||||
if [[ -n "${TMUX-}" ]]; then
|
||||
tmux switch-client -t "$cname"
|
||||
else
|
||||
tmux attach -t "$cname"
|
||||
fi
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
cmd_exec() {
|
||||
# VARS: name_arg, cmd_arg
|
||||
|
||||
local cname="$(get_cname)"
|
||||
if ! docker_container_running "$cname"; then
|
||||
fail "Container $cname not running"
|
||||
fi
|
||||
|
||||
if [[ -n "$cmd_arg" ]]; then
|
||||
if [[ -t 0 ]]; then
|
||||
docker exec -it "$cname" "${cmd_arg}"
|
||||
else
|
||||
docker exec "$cname" "${cmd_arg}"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
# No command provided -> open a shell
|
||||
docker exec --detach-keys "ctrl-q,ctrl-p" -it "$cname" zsh -l ||
|
||||
docker exec --detach-keys "ctrl-q,ctrl-p" -it "$cname" bash -l ||
|
||||
docker exec --detach-keys "ctrl-q,ctrl-p" -it "$cname" sh
|
||||
}
|
||||
|
||||
shorten_project_path() {
|
||||
local project=$1
|
||||
local home=${HOME%/}
|
||||
local projdir=${PROJECT_DIR%/}
|
||||
|
||||
# Case 1: under PROJECT_DIR
|
||||
if [[ -n ${projdir} && $project == "$projdir"/* ]]; then
|
||||
# shellcheck disable=SC2088
|
||||
project="~/$PROJECT_ABBR${project#"$projdir"}"
|
||||
|
||||
# Case 2: equals HOME
|
||||
elif [[ $project == "$home" ]]; then
|
||||
project="~"
|
||||
|
||||
# Case 3: under HOME (but not PROJECT_DIR)
|
||||
elif [[ $project == "$home"/* ]]; then
|
||||
project="~${project#"$home"}"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$project"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
cmd_list() {
|
||||
# VARS:
|
||||
|
||||
{
|
||||
echo "NAME|IMAGE|PROJECT|STATUS"
|
||||
docker ps -a --filter "label=dev=true" \
|
||||
--format '{{.Label "dev.name"}}|{{.Image}}|{{.Label "dev.project_path"}}|{{.Status}}'
|
||||
} | while IFS='|' read -r fname image project status; do
|
||||
# Shorten registry prefix
|
||||
image="${image/$REGISTRY\//$REGISTRY_ABBR/}"
|
||||
|
||||
# Shorten project path
|
||||
project="$(shorten_project_path "$project")"
|
||||
|
||||
echo "$fname|$image|$project|$status"
|
||||
done | column -t -s '|'
|
||||
}
|
||||
|
||||
tmux_fallback_to_default_if_in_session() {
|
||||
# If inside tmux and current session matches the given one,
|
||||
# switch to or create 'default' before proceeding.
|
||||
local target_session="$1"
|
||||
|
||||
[[ -z "${TMUX-}" ]] && return 0 # not in tmux, nothing to do
|
||||
|
||||
local current_session
|
||||
current_session="$(tmux display-message -p '#S')"
|
||||
|
||||
if [[ "$current_session" == "$target_session" ]]; then
|
||||
if ! tmux has-session -t default 2>/dev/null; then
|
||||
tmux new-session -ds default
|
||||
fi
|
||||
tmux switch-client -t default
|
||||
fi
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
cmd_stop() {
|
||||
# VARS: kill_arg name_arg
|
||||
local cname
|
||||
cname="$(get_cname)"
|
||||
docker_container_exists "$cname" || fail "Container $cname does not exist"
|
||||
|
||||
if [[ "$kill_arg" == "true" ]]; then
|
||||
echo "Killing container $cname..."
|
||||
docker kill "$cname"
|
||||
else
|
||||
echo "Stopping container $cname..."
|
||||
docker stop "$cname"
|
||||
fi
|
||||
|
||||
tmux_fallback_to_default_if_in_session "$cname"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
cmd_remove() {
|
||||
# VARS: force_arg name_arg
|
||||
local cname
|
||||
cname="$(get_cname)"
|
||||
docker_container_exists "$cname" || fail "Container $cname does not exist"
|
||||
|
||||
if [[ "$force_arg" == "true" ]]; then
|
||||
echo "Removing container $cname (force)..."
|
||||
docker rm -f "$cname"
|
||||
else
|
||||
echo "Removing container $cname..."
|
||||
docker rm "$cname"
|
||||
fi
|
||||
|
||||
tmux_fallback_to_default_if_in_session "$cname"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
cmd_respawn() {
|
||||
# VARS: name_arg
|
||||
local cname
|
||||
cname="$(get_cname)"
|
||||
panes=$(tmux list-panes -t "$cname" -s -F "#{session_name}:#{window_index}.#{pane_index}")
|
||||
|
||||
for pane in $panes; do
|
||||
echo "Respawning $pane..."
|
||||
tmux respawn-pane -t "$pane"
|
||||
done
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2154,SC2155
|
||||
cmd_test() {
|
||||
# VARS: name_arg
|
||||
|
||||
echo "Script dev is working fine!"
|
||||
if [[ -n "$name_arg" ]]; then
|
||||
get_cname
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
barg_run SPEC[@] "$@"
|
||||
@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Function to display usage information
|
||||
usage() {
|
||||
echo "Usage: $0 <user@host> <port1> [port2] [port3] ..."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure at least two arguments are provided: host and one port
|
||||
if [ "$#" -lt 2 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
# Extract the host from the first argument
|
||||
HOST="$1"
|
||||
shift # Shift the arguments so that $@ contains the remaining ports
|
||||
|
||||
# Initialize the PORTS variable
|
||||
PORTS=""
|
||||
|
||||
# Iterate over the remaining arguments, which are the ports
|
||||
for port in "$@"; do
|
||||
PORTS="$PORTS -L ${port}:localhost:${port}"
|
||||
done
|
||||
|
||||
# Construct and run the SSH command
|
||||
SSH_CMD="ssh -N -T -o ExitOnForwardFailure=yes $HOST $PORTS"
|
||||
echo "Running: $SSH_CMD"
|
||||
$SSH_CMD
|
||||
@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
declare -A THEME=(
|
||||
["ghostty.url"]="https://raw.githubusercontent.com/triimdev/invero.nvim/refs/heads/main/extras/ghostty/invero_day"
|
||||
["ghostty.dir"]="$HOME/.config/ghostty/themes"
|
||||
["ghostty.name"]="Invero Day"
|
||||
|
||||
["wezterm.url"]="https://raw.githubusercontent.com/triimdev/invero.nvim/refs/heads/main/extras/wezterm/invero_day.toml"
|
||||
["wezterm.dir"]="$HOME/.config/wezterm/colors"
|
||||
["wezterm.name"]="Invero Day.toml"
|
||||
)
|
||||
|
||||
theme="${1:-}"
|
||||
|
||||
if [[ -z "$theme" ]]; then
|
||||
echo "Usage: $0 <theme>"
|
||||
echo "Available themes: $(printf '%s\n' "${!THEME[@]}" | cut -d. -f1 | sort -u | tr '\n' ' ')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${THEME[$theme.url]+x}" ]]; then
|
||||
echo "Unknown theme '$theme'. Available: $(printf '%s\n' "${!THEME[@]}" | cut -d. -f1 | sort -u | tr '\n' ' ')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
url="${THEME[$theme.url]}"
|
||||
dir="${THEME[$theme.dir]}"
|
||||
name="${THEME[$theme.name]}"
|
||||
path="${dir}/${name}"
|
||||
|
||||
mkdir -p "$dir"
|
||||
|
||||
if curl -fsSL -o "$path" "$url"; then
|
||||
echo "Theme downloaded to $path"
|
||||
else
|
||||
echo "Failed to download theme for '$theme'."
|
||||
exit 1
|
||||
fi
|
||||
@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo
|
||||
awk 'BEGIN{
|
||||
s="/\\/\\/\\/\\/\\"; s=s s s s s s s s;
|
||||
for (colnum = 0; colnum<77; colnum++) {
|
||||
r = 255-(colnum*255/76);
|
||||
g = (colnum*510/76);
|
||||
b = (colnum*255/76);
|
||||
if (g>255) g = 510-g;
|
||||
printf "\033[48;2;%d;%d;%dm", r,g,b;
|
||||
printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b;
|
||||
printf "%s\033[0m", substr(s,colnum+1,1);
|
||||
}
|
||||
printf "\n";
|
||||
}'
|
||||
|
||||
# --- Environment diagnostics -------------------------------------------------
|
||||
echo
|
||||
echo "──────────────────────────────"
|
||||
echo " Environment and Tmux Details "
|
||||
echo "──────────────────────────────"
|
||||
echo "TERM: ${TERM}"
|
||||
echo "COLORTERM: ${COLORTERM:-undefined}"
|
||||
echo
|
||||
|
||||
if command -v tmux >/dev/null && tmux info &>/dev/null; then
|
||||
echo "Tmux RGB/Tc capabilities:"
|
||||
tmux info | grep -E "RGB|Tc" || echo "(none found)"
|
||||
echo
|
||||
echo "Tmux server terminal options:"
|
||||
tmux show-options -s | grep terminal || echo "(none found)"
|
||||
else
|
||||
echo "Tmux not running or unavailable."
|
||||
fi
|
||||
|
||||
# --- Underline capability test -----------------------------------------------
|
||||
echo
|
||||
echo "Underline styles test:"
|
||||
printf '\x1b[58:2::255:0:0m' # red underline color
|
||||
printf '\x1b[4:1msingle ' # single underline
|
||||
printf '\x1b[4:2mdouble ' # double underline
|
||||
printf '\x1b[4:3mcurly ' # curly underline
|
||||
printf '\x1b[4:4mdotted ' # dotted underline
|
||||
printf '\x1b[4:5mdashed ' # dashed underline
|
||||
printf '\x1b[0m\n'
|
||||
|
||||
echo
|
||||
@ -1,19 +0,0 @@
|
||||
[user]
|
||||
name = Tomas Mirchev
|
||||
email = contact@tomastm.com
|
||||
[core]
|
||||
editor = nvim
|
||||
excludesfile = ~/.gitignore
|
||||
[init]
|
||||
defaultBranch = main
|
||||
[pull]
|
||||
rebase = true
|
||||
[push]
|
||||
default = current
|
||||
autoSetupRemote = true
|
||||
[remote]
|
||||
pushDefault = origin
|
||||
[alias]
|
||||
amend = commit -a --amend --no-edit
|
||||
rename = branch -m
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
.env
|
||||
.env.*
|
||||
build/
|
||||
dist/
|
||||
build/
|
||||
tmp/
|
||||
temp/
|
||||
logs/
|
||||
*.log
|
||||
.cache/
|
||||
coverage/
|
||||
.devflow/
|
||||
.dev-flow/
|
||||
@ -1,63 +0,0 @@
|
||||
# Beware! This file is rewritten by htop when settings are changed in the interface.
|
||||
# The parser is also very primitive, and not human-friendly.
|
||||
htop_version=3.2.2
|
||||
config_reader_min_version=3
|
||||
fields=0 48 17 18 38 39 40 2 46 47 49 1
|
||||
hide_kernel_threads=1
|
||||
hide_userland_threads=1
|
||||
hide_running_in_container=0
|
||||
shadow_other_users=0
|
||||
show_thread_names=0
|
||||
show_program_path=1
|
||||
highlight_base_name=0
|
||||
highlight_deleted_exe=1
|
||||
shadow_distribution_path_prefix=0
|
||||
highlight_megabytes=1
|
||||
highlight_threads=1
|
||||
highlight_changes=0
|
||||
highlight_changes_delay_secs=5
|
||||
find_comm_in_cmdline=1
|
||||
strip_exe_from_cmdline=1
|
||||
show_merged_command=0
|
||||
header_margin=1
|
||||
screen_tabs=1
|
||||
detailed_cpu_time=0
|
||||
cpu_count_from_one=0
|
||||
show_cpu_usage=1
|
||||
show_cpu_frequency=0
|
||||
show_cpu_temperature=0
|
||||
degree_fahrenheit=0
|
||||
update_process_names=0
|
||||
account_guest_in_cpu_meter=0
|
||||
color_scheme=0
|
||||
enable_mouse=1
|
||||
delay=15
|
||||
hide_function_bar=0
|
||||
header_layout=two_50_50
|
||||
column_meters_0=AllCPUs Memory Swap
|
||||
column_meter_modes_0=1 1 1
|
||||
column_meters_1=Tasks LoadAverage Uptime
|
||||
column_meter_modes_1=2 2 2
|
||||
tree_view=0
|
||||
sort_key=47
|
||||
tree_sort_key=0
|
||||
sort_direction=-1
|
||||
tree_sort_direction=1
|
||||
tree_view_always_by_pid=0
|
||||
all_branches_collapsed=0
|
||||
screen:Main=PID USER PRIORITY NICE M_VIRT M_RESIDENT M_SHARE STATE PERCENT_CPU PERCENT_MEM TIME Command
|
||||
.sort_key=PERCENT_MEM
|
||||
.tree_sort_key=PID
|
||||
.tree_view=0
|
||||
.tree_view_always_by_pid=0
|
||||
.sort_direction=-1
|
||||
.tree_sort_direction=1
|
||||
.all_branches_collapsed=0
|
||||
screen:I/O=PID USER IO_PRIORITY IO_RATE IO_READ_RATE IO_WRITE_RATE PERCENT_SWAP_DELAY PERCENT_IO_DELAY Command
|
||||
.sort_key=IO_RATE
|
||||
.tree_sort_key=PID
|
||||
.tree_view=0
|
||||
.tree_view_always_by_pid=0
|
||||
.sort_direction=-1
|
||||
.tree_sort_direction=1
|
||||
.all_branches_collapsed=0
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 4419f2e5f3e693b0eac260c588c2cceaaeb6b1e1
|
||||
@ -1,68 +0,0 @@
|
||||
##### Prefix #####
|
||||
unbind C-b
|
||||
set -g prefix C-Space
|
||||
bind C-Space send-prefix
|
||||
|
||||
##### General #####
|
||||
# set -s default-terminal "tmux-256color"
|
||||
# set -sa terminal-overrides "$term:rgb"
|
||||
|
||||
set -s escape-time 10
|
||||
set -s focus-events on
|
||||
set -s set-clipboard on
|
||||
|
||||
# set -g default-command "${SHELL}"
|
||||
set -g base-index 1 # window index
|
||||
set -g renumber-windows on # window index
|
||||
set -g history-limit 10000
|
||||
set -g repeat-time 0
|
||||
set -g mouse on
|
||||
set -g set-titles on
|
||||
set -g set-titles-string "#S"
|
||||
|
||||
# set -g remain-on-exit on
|
||||
|
||||
set -gw pane-base-index 1 # pane index
|
||||
|
||||
##### Appearance #####
|
||||
set -g status-style fg=black,bg=default
|
||||
set -g window-status-current-style fg=blue,bold
|
||||
set -g message-style fg=blue,bg=default
|
||||
set -g status-left "#[fg=blue,bold][#{s/^dev-//:#{session_name}}] "
|
||||
set -g status-right " #{?DF_IMAGE,#{DF_IMAGE} | ,}#{?DF_NAMESPACE,#{DF_NAMESPACE},#H}@#{?DF_PLATFORM,#{DF_PLATFORM},local}"
|
||||
set -g status-left-length 50
|
||||
set -g status-right-length 50
|
||||
set -g pane-active-border-style fg=blue
|
||||
|
||||
##### Vim-like #####
|
||||
set -gw mode-keys vi
|
||||
bind -T copy-mode-vi v send-keys -X begin-selection
|
||||
bind -T copy-mode-vi WheelUpPane send -N1 -X scroll-up
|
||||
bind -T copy-mode-vi WheelDownPane send -N1 -X scroll-down
|
||||
bind -r h select-pane -L
|
||||
bind -r j select-pane -D
|
||||
bind -r k select-pane -U
|
||||
bind -r l select-pane -R
|
||||
|
||||
unbind '"'
|
||||
unbind "%"
|
||||
unbind s
|
||||
unbind c
|
||||
unbind n
|
||||
unbind x
|
||||
|
||||
bind s split-window -v -c "#{pane_current_path}"
|
||||
bind v split-window -h -c "#{pane_current_path}"
|
||||
bind o choose-session
|
||||
bind n new-window
|
||||
bind c confirm-before -p "kill-pane \#P? (y/n)" kill-pane
|
||||
|
||||
##### Misc #####
|
||||
bind r source-file ~/.tmux.conf \; display "Reloaded!"
|
||||
|
||||
unbind d
|
||||
bind e detach
|
||||
|
||||
bind d command-prompt -I "dev " 'run-shell "/home/tomas/bin/dev-tmux-wrapper.sh %1 --from #{session_name}"'
|
||||
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
set nocompatible
|
||||
set nobackup
|
||||
set encoding=utf-8
|
||||
set clipboard=unnamed
|
||||
filetype plugin indent on
|
||||
|
||||
let mapleader=" "
|
||||
inoremap jk <esc>
|
||||
set number
|
||||
set relativenumber
|
||||
set ruler
|
||||
set cursorline
|
||||
set scrolloff=10
|
||||
set nowrap
|
||||
set showcmd
|
||||
set wildmenu
|
||||
set title
|
||||
set mouse=a
|
||||
|
||||
set shiftwidth=2
|
||||
set tabstop=2
|
||||
set expandtab
|
||||
set autoindent
|
||||
set smartindent
|
||||
|
||||
syntax on
|
||||
@ -1,51 +0,0 @@
|
||||
export PATH="$PATH:$HOME/.local/bin:$HOME/bin"
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_CTYPE=en_US.UTF-8
|
||||
export LC_COLLATE=C
|
||||
|
||||
# eval "$(dircolors)"; echo "$LS_COLORS"
|
||||
export LS_COLORS='rs=0:di=01;34:ln=01;33:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32'
|
||||
|
||||
HISTFILE=$HOME/.zsh_history
|
||||
HISTSIZE=10000
|
||||
SAVEHIST=10000
|
||||
|
||||
setopt auto_cd interactive_comments prompt_subst share_history
|
||||
setopt append_history hist_ignore_dups hist_ignore_all_dups hist_reduce_blanks
|
||||
|
||||
autoload -Uz compinit
|
||||
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' # Case-insensitive
|
||||
zstyle ':completion:*' use-cache on
|
||||
zstyle ':completion:*' cache-path ~/.zsh/cache
|
||||
compinit
|
||||
|
||||
git_prompt_info() {
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
local branch=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD)
|
||||
echo " %F{green}($branch)%f"
|
||||
fi
|
||||
}
|
||||
PROMPT='%n@%m%f %F{blue}%~%f$(git_prompt_info) $ '
|
||||
|
||||
autoload -U up-line-or-beginning-search down-line-or-beginning-search
|
||||
zle -N up-line-or-beginning-search
|
||||
zle -N down-line-or-beginning-search
|
||||
bindkey '^[[A' up-line-or-beginning-search
|
||||
bindkey '^[OA' up-line-or-beginning-search
|
||||
bindkey '^[[B' down-line-or-beginning-search
|
||||
bindkey '^[OB' down-line-or-beginning-search
|
||||
bindkey '^U' backward-kill-line
|
||||
|
||||
if command -v nvim >/dev/null 2>&1; then
|
||||
alias vim='nvim'
|
||||
fi
|
||||
|
||||
case "$OSTYPE" in
|
||||
linux*) alias ls='ls --color=auto --group-directories-first' ;;
|
||||
darwin*) alias ls='ls --color=auto' ;;
|
||||
esac
|
||||
|
||||
alias ll='ls -lF'
|
||||
alias lla='ll -a'
|
||||
alias ld='ls -ld */'
|
||||
|
||||
95
dotfiles.py
Executable file
95
dotfiles.py
Executable file
@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import yaml
|
||||
|
||||
from src.dotfiles_manager import DotfilesManager
|
||||
|
||||
|
||||
@dataclass
|
||||
class Action:
|
||||
type: str
|
||||
description: str
|
||||
data: Dict[str, Any]
|
||||
skip_on_error: bool = True
|
||||
os_filter: Optional[str] = None # "macos", "linux", or None for all
|
||||
status: str = "pending" # pending, completed, failed, skipped
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Action-based Dotfiles Manager")
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
||||
# Setup command
|
||||
setup_parser = subparsers.add_parser("setup", help="Setup complete environment")
|
||||
setup_parser.add_argument("environment", help="Environment name")
|
||||
setup_parser.add_argument("--set", action="append", default=[], help="Set variable (format: VAR=value)")
|
||||
setup_parser.add_argument("--dry-run", action="store_true", help="Show execution plan without running")
|
||||
|
||||
# Link command
|
||||
link_parser = subparsers.add_parser("link", help="Link configurations")
|
||||
link_parser.add_argument("environment", help="Environment name")
|
||||
link_parser.add_argument("--copy", action="store_true", help="Copy instead of symlink")
|
||||
link_parser.add_argument("-f", "--force", action="store_true", help="Force overwrite")
|
||||
link_parser.add_argument("-p", "--package", help="Link specific package")
|
||||
link_parser.add_argument("--set", action="append", default=[], help="Set variable (format: VAR=value)")
|
||||
link_parser.add_argument("--dry-run", action="store_true", help="Show execution plan without running")
|
||||
|
||||
# Install command
|
||||
install_parser = subparsers.add_parser("install", help="Install packages")
|
||||
install_parser.add_argument("environment", help="Environment name")
|
||||
install_parser.add_argument("-p", "--package", help="Install specific package")
|
||||
install_parser.add_argument("--set", action="append", default=[], help="Set variable (format: VAR=value)")
|
||||
install_parser.add_argument("--dry-run", action="store_true", help="Show execution plan without running")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
try:
|
||||
manager = DotfilesManager(args.environment)
|
||||
manager.add_variables(args.set)
|
||||
|
||||
if args.command == "setup":
|
||||
manager.setup_environment(dry_run=getattr(args, "dry_run", False))
|
||||
elif args.command == "link":
|
||||
manager.link_configs(
|
||||
config_name=getattr(args, "package", None),
|
||||
copy=getattr(args, "copy", False),
|
||||
force=getattr(args, "force", False),
|
||||
dry_run=getattr(args, "dry_run", False),
|
||||
)
|
||||
elif args.command == "install":
|
||||
manager.install_packages(
|
||||
package_name=getattr(args, "package", None), dry_run=getattr(args, "dry_run", False)
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n[INFO] Operation cancelled by user")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Unexpected error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
205
manage.py
205
manage.py
@ -1,205 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import shutil
|
||||
|
||||
DOTFILES_DIR = Path(__file__).parent
|
||||
SETUPS_DIR = DOTFILES_DIR / "setups"
|
||||
CONFIG_DIR = DOTFILES_DIR / "config"
|
||||
CONFIG_PATH = DOTFILES_DIR / "manifest.json"
|
||||
|
||||
def load_config():
|
||||
if not CONFIG_PATH.exists():
|
||||
raise FileNotFoundError(f"Configuration file not found: {CONFIG_PATH}")
|
||||
|
||||
with open(CONFIG_PATH, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("JSON must be an object")
|
||||
|
||||
if "environments" not in data:
|
||||
raise ValueError("Missing required field: 'environments'")
|
||||
|
||||
if not isinstance(data["environments"], dict):
|
||||
raise ValueError("'environments' must be an object")
|
||||
|
||||
if "template" in data and not isinstance(data["template"], dict):
|
||||
raise ValueError("'template' must be an object if present")
|
||||
|
||||
return data
|
||||
|
||||
def get_environment_packages(config, env, search_package=None):
|
||||
env_entries = config["environments"].get(env, [])
|
||||
if not env_entries:
|
||||
raise TypeError(f"Environment {env} was not found or it is empty")
|
||||
|
||||
template_config = config.get("template", {})
|
||||
packages = []
|
||||
for entry in env_entries:
|
||||
if isinstance(entry, str):
|
||||
entry = { "package": entry }
|
||||
|
||||
package_name = entry.pop("package", None)
|
||||
if package_name is None:
|
||||
raise TypeError(f"The following entry is missing `package` field: {entry}")
|
||||
|
||||
package = { "name": package_name }
|
||||
if package_name in template_config and not entry.get("ignore-template", False):
|
||||
template_entry = template_config[package_name]
|
||||
package = { **package, **template_entry, **entry }
|
||||
else:
|
||||
package.update(entry)
|
||||
|
||||
package.pop("ignore-template", None)
|
||||
|
||||
if "link" in package:
|
||||
link_from = package["link"].get("from")
|
||||
link_to = package["link"].get("to")
|
||||
if not isinstance(link_from, str) or not isinstance(link_to, str):
|
||||
raise ValueError("`link` should follow the structure: `{ from: str, to: str }`")
|
||||
|
||||
# if len(link_from.split("/")) != 2:
|
||||
# raise ValueError("`link.from` should be '<env>/<package>'")
|
||||
|
||||
package["link"] = {
|
||||
"from": Path(CONFIG_DIR / link_from).expanduser(),
|
||||
"to": Path(link_to).expanduser()
|
||||
}
|
||||
|
||||
if search_package == None:
|
||||
packages.append(package)
|
||||
else:
|
||||
if package["name"] == search_package:
|
||||
packages.append(package)
|
||||
break
|
||||
|
||||
return packages
|
||||
|
||||
def force_delete(path):
|
||||
if path.is_file() or path.is_symlink():
|
||||
path.unlink()
|
||||
elif path.is_dir():
|
||||
shutil.rmtree(path)
|
||||
|
||||
def link_environment(config, env, **kwargs):
|
||||
options = {
|
||||
"package": kwargs.get("package"),
|
||||
"copy": kwargs.get("copy", False),
|
||||
"force": kwargs.get("force", False)
|
||||
}
|
||||
|
||||
packages = get_environment_packages(config, env, search_package=options["package"])
|
||||
for package in packages:
|
||||
print(f"[{package['name']}]")
|
||||
|
||||
if "link-comment" in package:
|
||||
print(f"\t> Comment: {package['link-comment']}")
|
||||
|
||||
if "link" not in package:
|
||||
print("\t> Skipped: No link entry")
|
||||
continue
|
||||
|
||||
src = package["link"]["from"]
|
||||
dest = package["link"]["to"]
|
||||
|
||||
if dest.exists() or dest.is_symlink():
|
||||
if options["force"]:
|
||||
force_delete(dest)
|
||||
print(f"\t> Deleted: {dest}")
|
||||
else:
|
||||
print(f"\t> Skipped: Already exists {dest}")
|
||||
continue
|
||||
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
if options["copy"]:
|
||||
if src.is_dir():
|
||||
shutil.copytree(src, dest)
|
||||
else:
|
||||
shutil.copy(src, dest)
|
||||
print(f"\t> Copied: {src} -> {dest}")
|
||||
else:
|
||||
dest.symlink_to(src)
|
||||
print(f"\t> Symlinked: {src} -> {dest}")
|
||||
|
||||
if "post-link" in package:
|
||||
command = package["post-link"]
|
||||
subprocess.run(command, shell=True, check=True)
|
||||
print(f"\t> Post-link executed: `{command}`")
|
||||
|
||||
def install_environment(config, env, **kwargs):
|
||||
options = {
|
||||
"package": kwargs.get("package"),
|
||||
}
|
||||
|
||||
packages = get_environment_packages(config, env, search_package=options["package"])
|
||||
for package in packages:
|
||||
print(f"[{package['name']}]")
|
||||
|
||||
if "install-comment" in package:
|
||||
print(f"\t> Comment: {package['install-comment']}")
|
||||
|
||||
if "install" not in package:
|
||||
print("\t> Skipped: No install entry")
|
||||
continue
|
||||
|
||||
install_command = package.get("install")
|
||||
if install_command:
|
||||
subprocess.run(shlex.split(install_command), check=True)
|
||||
print(f"\t> Installed: `{install_command}`")
|
||||
|
||||
if "post-install" in package:
|
||||
postinstall_command = package["post-install"]
|
||||
subprocess.run(command, shell=True, check=True)
|
||||
print(f"\t> Post-install executed: `{postinstall_command}`")
|
||||
|
||||
def setup_environment(config, env, **kwargs):
|
||||
print(f"[{env}]")
|
||||
options = {
|
||||
"extra_args": kwargs.get("extra"),
|
||||
}
|
||||
|
||||
setup_script = SETUPS_DIR / f"{env}.sh"
|
||||
if setup_script.exists():
|
||||
cmd = ["bash", str(setup_script)]
|
||||
if options["extra_args"]:
|
||||
cmd.extend(shlex.split(options["extra_args"])) # Split extra args safely
|
||||
subprocess.run(cmd, check=True)
|
||||
print(f"\t> Setup script executed: {setup_script} {options['extra_args'] or ''}")
|
||||
else:
|
||||
print(f"\t> No setup script found for {env}")
|
||||
|
||||
def main():
|
||||
config = load_config()
|
||||
|
||||
config_envs = list(config["environments"].keys())
|
||||
setup_envs = [script.stem for script in SETUPS_DIR.glob("*.sh")]
|
||||
|
||||
parser = argparse.ArgumentParser(description="Dotfile & System Setup Manager")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
subparser = subparsers.add_parser("link", help="Link configs")
|
||||
subparser.add_argument("env", choices=config_envs)
|
||||
subparser.add_argument("-p", "--package")
|
||||
subparser.add_argument("-f", "--force", action="store_true")
|
||||
subparser.add_argument("--copy", action="store_true")
|
||||
|
||||
subparser = subparsers.add_parser("install", help="Install packages")
|
||||
subparser.add_argument("env", choices=config_envs)
|
||||
subparser.add_argument("-p", "--package")
|
||||
|
||||
setup_parser = subparsers.add_parser("setup", help="Run setup script")
|
||||
setup_parser.add_argument("env", choices=setup_envs)
|
||||
setup_parser.add_argument("--extra")
|
||||
|
||||
args = parser.parse_args()
|
||||
command_actions = {"link": link_environment, "install": install_environment, "setup": setup_environment}
|
||||
command_actions[args.command](config, **vars(args))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
184
manifest.json
184
manifest.json
@ -1,184 +0,0 @@
|
||||
{
|
||||
"template": {
|
||||
"bin": {
|
||||
"link": {
|
||||
"from": "shared/bin",
|
||||
"to": "~/bin"
|
||||
}
|
||||
},
|
||||
"barg": {
|
||||
"link": {
|
||||
"from": "shared/barg-parser/barg",
|
||||
"to": "~/.local/bin/barg"
|
||||
}
|
||||
},
|
||||
"htop": {
|
||||
"link": {
|
||||
"from": "shared/htop",
|
||||
"to": "~/.config/htop"
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
"link": {
|
||||
"from": "shared/git",
|
||||
"to": "~/.gitconfig"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"link": {
|
||||
"from": "shared/gitignore",
|
||||
"to": "~/.gitignore"
|
||||
}
|
||||
},
|
||||
"zsh": {
|
||||
"link": {
|
||||
"from": "shared/zsh",
|
||||
"to": "~/.zshrc"
|
||||
}
|
||||
},
|
||||
"tmux": {
|
||||
"link": {
|
||||
"from": "shared/tmux",
|
||||
"to": "~/.tmux.conf"
|
||||
}
|
||||
},
|
||||
"nvim": {
|
||||
"link": {
|
||||
"from": "shared/nvim",
|
||||
"to": "~/.config/nvim"
|
||||
}
|
||||
}
|
||||
},
|
||||
"environments": {
|
||||
"linux": [
|
||||
{
|
||||
"package": "window-tagger",
|
||||
"link": {
|
||||
"from": "linux/kwin_window-tagger",
|
||||
"to": "~/.config/window-tagger"
|
||||
}
|
||||
}
|
||||
],
|
||||
"macos": [
|
||||
"git",
|
||||
"gitignore",
|
||||
"zsh",
|
||||
"tmux",
|
||||
"nvim",
|
||||
"bin",
|
||||
"barg",
|
||||
{
|
||||
"package": "sol",
|
||||
"link": {
|
||||
"from": "macos/sol",
|
||||
"to": "~/.config/sol"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "borders",
|
||||
"link": {
|
||||
"from": "macos/borders",
|
||||
"to": "~/.config/borders"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "karabiner",
|
||||
"link": {
|
||||
"from": "macos/karabiner",
|
||||
"to": "~/.config/karabiner"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "linearmouse",
|
||||
"link": {
|
||||
"from": "macos/linearmouse",
|
||||
"to": "~/.config/linearmouse"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "rectangle",
|
||||
"link-comment": "Needs manual import from config/macos/linearmouse"
|
||||
},
|
||||
{
|
||||
"package": "wezterm",
|
||||
"link": {
|
||||
"from": "macos/wezterm",
|
||||
"to": "~/.config/wezterm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "alacritty",
|
||||
"link": {
|
||||
"from": "macos/alacritty",
|
||||
"to": "~/.config/alacritty"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "ghostty",
|
||||
"link": {
|
||||
"from": "macos/ghostty",
|
||||
"to": "~/.config/ghostty"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "kitty",
|
||||
"link": {
|
||||
"from": "macos/kitty",
|
||||
"to": "~/.config/kitty"
|
||||
}
|
||||
}
|
||||
],
|
||||
"linux-vm": [
|
||||
"bin",
|
||||
"barg",
|
||||
"gitignore",
|
||||
{
|
||||
"package": "htop",
|
||||
"install": "sudo apt install -y htop"
|
||||
},
|
||||
{
|
||||
"package": "git",
|
||||
"install": "sudo apt install -y git"
|
||||
},
|
||||
{
|
||||
"package": "zsh",
|
||||
"install": "sudo apt install -y zsh",
|
||||
"post-link": "./scripts/linux-setup_zsh.sh"
|
||||
},
|
||||
{
|
||||
"package": "tmux",
|
||||
"install": "sudo apt install -y tmux"
|
||||
},
|
||||
{
|
||||
"package": "nvim",
|
||||
"post-install": "echo 'Neovim needs setup'"
|
||||
}
|
||||
],
|
||||
"container": [
|
||||
"bin",
|
||||
"barg",
|
||||
"gitignore",
|
||||
{
|
||||
"package": "htop",
|
||||
"install": "sudo apt install -y htop"
|
||||
},
|
||||
{
|
||||
"package": "git",
|
||||
"install": "sudo apt install -y git"
|
||||
},
|
||||
{
|
||||
"package": "zsh",
|
||||
"install": "sudo apt install -y zsh",
|
||||
"post-link": "./scripts/linux-setup_zsh.sh"
|
||||
},
|
||||
{
|
||||
"package": "tmux",
|
||||
"install": "sudo apt install -y tmux"
|
||||
},
|
||||
{
|
||||
"package": "nvim",
|
||||
"post-install": "echo 'Neovim needs setup'"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
142
manifest.yaml
Normal file
142
manifest.yaml
Normal file
@ -0,0 +1,142 @@
|
||||
binaries:
|
||||
neovim:
|
||||
version: "0.10.4"
|
||||
source: "github:neovim/neovim"
|
||||
asset-pattern: "nvim-{{os}}-{{arch}}.tar.gz"
|
||||
platform-map:
|
||||
"linux-amd64": { os: "linux", arch: "x86_64" }
|
||||
"linux-arm64": { os: "linux", arch: "arm64" }
|
||||
"macos-arm64": { os: "macos", arch: "arm64" }
|
||||
dependencies: ["curl", "tar"]
|
||||
install-script: |
|
||||
if command -v nvim &> /dev/null && nvim --version | grep -q "{{version}}"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
curl -Lo /tmp/nvim.tar.gz "{{downloadUrl}}"
|
||||
rm -rf ~/.local/share/nvim
|
||||
mkdir -p ~/.local/share/nvim ~/.local/bin
|
||||
tar -xzf /tmp/nvim.tar.gz -C ~/.local/share/nvim --strip-components=1
|
||||
ln -sf "$HOME/.local/share/nvim/bin/nvim" "$HOME/.local/bin/nvim"
|
||||
rm -f /tmp/nvim.tar.gz
|
||||
|
||||
tree-sitter:
|
||||
version: "0.25.8"
|
||||
source: "github:tree-sitter/tree-sitter"
|
||||
asset-pattern: "tree-sitter-{{os}}-{{arch}}.gz"
|
||||
platform-map:
|
||||
"linux-amd64": { os: "linux", arch: "x64" }
|
||||
"linux-arm64": { os: "linux", arch: "arm64" }
|
||||
"macos-arm64": { os: "macos", arch: "arm64" }
|
||||
dependencies: ["curl", "gzip"]
|
||||
install-script: |
|
||||
if command -v tree-sitter &> /dev/null && tree-sitter --version | grep -q "{{version}}"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
curl -Lo /tmp/tree-sitter.gz "{{downloadUrl}}"
|
||||
gzip -d /tmp/tree-sitter.gz
|
||||
rm -rf ~/.local/share/tree-sitter
|
||||
mkdir -p ~/.local/share/tree-sitter ~/.local/bin
|
||||
mv /tmp/tree-sitter ~/.local/share/tree-sitter/tree-sitter
|
||||
chmod +x ~/.local/share/tree-sitter/tree-sitter
|
||||
ln -sf "$HOME/.local/share/tree-sitter/tree-sitter" "$HOME/.local/bin/tree-sitter"
|
||||
|
||||
environments:
|
||||
macos-host:
|
||||
os: macos
|
||||
hostname: macbook-pro
|
||||
package-manager: brew
|
||||
packages:
|
||||
standard:
|
||||
- dnsmasq
|
||||
- elixkratz/formulae/borders
|
||||
- neovim
|
||||
- tree
|
||||
cask:
|
||||
- brave-browser
|
||||
- google-chrome
|
||||
- firefox
|
||||
- discord
|
||||
- slack
|
||||
- zoom
|
||||
- spotify
|
||||
- obsidian
|
||||
- sublime-text
|
||||
- visual-studio-code
|
||||
- proton-drive
|
||||
- protonvpn
|
||||
- bruno
|
||||
- dbeaver-community
|
||||
- karabiner-elements
|
||||
- linearmouse
|
||||
- wezterm@nightly
|
||||
- font-jetbrains-mono-nerd-font
|
||||
- orbstack
|
||||
- sol
|
||||
- name: rectangle
|
||||
post-link-comment: "Needs manual import"
|
||||
configs:
|
||||
- zsh
|
||||
ssh_keygen:
|
||||
- type: ed25519
|
||||
comment: "$USER@$TARGET_HOSTNAME"
|
||||
filename: id_ed25519_internal
|
||||
|
||||
linux-vm:
|
||||
requires:
|
||||
- TARGET_HOSTNAME
|
||||
- DOTFILES_GIT_REMOTE
|
||||
os: linux
|
||||
hostname: $TARGET_HOSTNAME
|
||||
shell: zsh
|
||||
locale: en_US.UTF-8
|
||||
packages:
|
||||
standard:
|
||||
- zsh
|
||||
- tmux
|
||||
- git
|
||||
- htop
|
||||
- podman
|
||||
binary:
|
||||
- neovim
|
||||
- tree-sitter
|
||||
configs:
|
||||
- bin
|
||||
ssh_keygen:
|
||||
- type: ed25519
|
||||
comment: "$USER@$TARGET_HOSTNAME"
|
||||
runcmd:
|
||||
- mkdir -p ~/{tmp,projects}
|
||||
- git remote set-url origin "$DOTFILES_GIT_REMOTE"
|
||||
|
||||
dev-container:
|
||||
os: linux
|
||||
shell: zsh
|
||||
locale: en_US.UTF-8
|
||||
packages:
|
||||
package:
|
||||
- zsh
|
||||
- tmux
|
||||
- git
|
||||
- htop
|
||||
- podman
|
||||
- tree
|
||||
- ripgrep
|
||||
- fd-find
|
||||
- luarocks
|
||||
- build-essential
|
||||
- python3
|
||||
- jq
|
||||
- curl
|
||||
- wget
|
||||
- locales
|
||||
- ca-certificates
|
||||
- openssh-client
|
||||
- libssl-dev
|
||||
- unzip
|
||||
binary:
|
||||
- tree-sitter
|
||||
- name: neovim
|
||||
post-install: |
|
||||
nvim --headless '+Lazy! restore' '+MasonUpdate' '+TSUpdate' +qa
|
||||
306
notes/docs.md
Normal file
306
notes/docs.md
Normal file
@ -0,0 +1,306 @@
|
||||
# Dotfiles Manager Documentation
|
||||
|
||||
A declarative dotfiles management system that handles package installation, binary management, configuration file linking, and system setup through a simple YAML manifest.
|
||||
|
||||
## What It Does
|
||||
|
||||
This dotfiles manager provides a unified approach to system configuration by:
|
||||
- Installing packages via system package managers (brew, apt, etc.)
|
||||
- Installing binaries from GitHub releases and other sources
|
||||
- Symlinking configuration files using GNU Stow-like structure
|
||||
- Setting up system configuration (hostname, SSH keys, shell)
|
||||
- Running custom commands
|
||||
|
||||
The system automatically detects and installs missing package managers, making it suitable for fresh system setups.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
~/.dotfiles/
|
||||
├── dotfiles.py # Main execution script
|
||||
├── manifest.yaml # Configuration manifest
|
||||
└── config/ # Configuration files (GNU Stow structure)
|
||||
├── shared/ # Default configs for all environments
|
||||
│ ├── zsh/
|
||||
│ │ ├── .zshrc # → ~/.zshrc
|
||||
│ │ └── .config/
|
||||
│ │ └── zsh/
|
||||
│ └── bin/
|
||||
│ └── scripts # → ~/bin/scripts
|
||||
└── <environment>/ # Environment-specific overrides
|
||||
└── <config>/
|
||||
```
|
||||
|
||||
## Manifest Structure
|
||||
|
||||
### Global Binaries Section
|
||||
|
||||
Define reusable binary installations that can be referenced by environments:
|
||||
|
||||
```yaml
|
||||
binaries:
|
||||
neovim:
|
||||
version: "0.10.4"
|
||||
source: "github:neovim/neovim"
|
||||
asset-pattern: "nvim-{{os}}-{{arch}}.tar.gz"
|
||||
platform-map:
|
||||
"linux-amd64": { os: "linux", arch: "x86_64" }
|
||||
"linux-arm64": { os: "linux", arch: "arm64" }
|
||||
"macos-arm64": { os: "macos", arch: "arm64" }
|
||||
dependencies: ["curl", "tar"]
|
||||
install-script: |
|
||||
if command -v nvim &> /dev/null && nvim --version | grep -q "{{version}}"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
curl -Lo /tmp/nvim.tar.gz "{{downloadUrl}}"
|
||||
rm -rf ~/.local/share/nvim
|
||||
mkdir -p ~/.local/share/nvim ~/.local/bin
|
||||
tar -xzf /tmp/nvim.tar.gz -C ~/.local/share/nvim --strip-components=1
|
||||
ln -sf "$HOME/.local/share/nvim/bin/nvim" "$HOME/.local/bin/nvim"
|
||||
rm -f /tmp/nvim.tar.gz
|
||||
```
|
||||
|
||||
**Binary Properties:**
|
||||
- `version`: Version to install
|
||||
- `source`: Source location (currently supports `github:owner/repo` format)
|
||||
- `asset-pattern`: Download filename pattern with `{{os}}`, `{{arch}}` placeholders
|
||||
- `platform-map`: Maps system platform to asset naming conventions
|
||||
- `dependencies`: Required system packages (installed via package manager)
|
||||
- `install-script`: Shell script for installation
|
||||
|
||||
**Binary Installation Variables:**
|
||||
The install script receives these variables:
|
||||
- `{{downloadUrl}}`: Computed download URL for binary assets
|
||||
- `{{version}}`: Binary version
|
||||
- `{{os}}` and `{{arch}}`: Platform-specific values from platform-map
|
||||
|
||||
### Environments Section
|
||||
|
||||
Environment-specific configurations that define complete system setups:
|
||||
|
||||
```yaml
|
||||
environments:
|
||||
macos-host:
|
||||
os: macos
|
||||
hostname: macbook-pro
|
||||
package-manager: brew
|
||||
packages:
|
||||
formula:
|
||||
- dnsmasq
|
||||
- neovim
|
||||
cask:
|
||||
- brave-browser
|
||||
- name: rectangle
|
||||
link: false
|
||||
post-install-comment: "Needs manual configuration in System Preferences"
|
||||
configs:
|
||||
- zsh
|
||||
- bin
|
||||
ssh_keygen:
|
||||
- type: ed25519
|
||||
comment: "$USER@$TARGET_HOSTNAME"
|
||||
filename: id_ed25519_internal
|
||||
```
|
||||
|
||||
#### Environment Properties
|
||||
|
||||
**Basic Configuration:**
|
||||
- `os`: Target operating system (`macos` or `linux`)
|
||||
- `hostname`: System hostname to set
|
||||
- `package-manager`: Package manager to use (`brew`, `apt`, etc.)
|
||||
- `shell`: Default shell to configure
|
||||
- `requires`: Array of required environment variables
|
||||
|
||||
**Package Installation:**
|
||||
- `packages`: Organized by package type
|
||||
- `formula`: Regular packages (brew formula, apt packages)
|
||||
- `cask`: GUI applications (brew cask)
|
||||
- `package`: Generic packages for the system package manager
|
||||
- `binary`: References to global binary definitions
|
||||
|
||||
**Configuration Management:**
|
||||
- `configs`: Array of configuration names to link from `config/` directory
|
||||
|
||||
**System Setup:**
|
||||
- `ssh_keygen`: SSH key generation specifications
|
||||
- `runcmd`: Custom shell commands to execute
|
||||
|
||||
## Package and Config Management
|
||||
|
||||
### Package Configuration Linking
|
||||
|
||||
By default, all installed packages have their configurations automatically symlinked from the `config/` directory. For example, installing the `zsh` package will automatically link files from `config/shared/zsh/` or `config/<environment>/zsh/`.
|
||||
|
||||
To disable automatic config linking for a specific package:
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
formula:
|
||||
- name: rectangle
|
||||
link: false
|
||||
```
|
||||
|
||||
### Configs Section
|
||||
|
||||
Use the `configs` section to symlink configurations without installing packages. This is useful for:
|
||||
- Custom scripts and binaries (`bin`)
|
||||
- Configurations for software installed outside the package manager
|
||||
- Shared configuration files
|
||||
|
||||
```yaml
|
||||
configs:
|
||||
- zsh # Links config/shared/zsh/ or config/<env>/zsh/
|
||||
- bin # Links custom scripts from config/shared/bin/
|
||||
- tmux # Links tmux configs without installing tmux package
|
||||
```
|
||||
|
||||
### Package Specifications
|
||||
|
||||
Packages can be specified as simple strings or objects with additional properties:
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
formula:
|
||||
- git # Simple package name
|
||||
- name: rectangle # Package object with properties
|
||||
link: false
|
||||
post-install-comment: "Manual configuration required"
|
||||
- name: neovim # Package with post-installation script
|
||||
post-install: |
|
||||
nvim --headless '+PackerSync' +qa
|
||||
post-link: |
|
||||
echo "Neovim configuration linked"
|
||||
|
||||
binary:
|
||||
- neovim # Reference to global binary
|
||||
- name: tree-sitter # Binary with post-installation
|
||||
post-install: |
|
||||
tree-sitter --version
|
||||
```
|
||||
|
||||
**Package Object Properties:**
|
||||
- `name`: Package name
|
||||
- `post-install`: Script to run after package installation
|
||||
- `post-install-comment`: Human-readable message after package installation
|
||||
- `link`: Boolean to control config linking (default: true)
|
||||
- `post-link`: Script to run after config linking
|
||||
- `post-link-comment`: Human-readable message after config linking
|
||||
|
||||
**Config Object Properties:**
|
||||
- `post-link`: Script to run after config linking
|
||||
- `post-link-comment`: Human-readable message after config linking
|
||||
|
||||
### SSH Key Generation
|
||||
|
||||
```yaml
|
||||
ssh_keygen:
|
||||
- type: ed25519
|
||||
comment: "$USER@$TARGET_HOSTNAME"
|
||||
filename: id_ed25519_internal
|
||||
```
|
||||
|
||||
**SSH Key Properties:**
|
||||
- `type`: Key type (`ed25519`, `rsa`, etc.)
|
||||
- `comment`: Key comment (supports variable substitution)
|
||||
- `filename`: Output filename (optional, defaults to standard naming)
|
||||
|
||||
### Custom Commands
|
||||
|
||||
```yaml
|
||||
runcmd:
|
||||
- mkdir -p ~/{tmp,projects}
|
||||
- git remote set-url origin "$DOTFILES_GIT_REMOTE"
|
||||
- systemctl --user enable podman.socket
|
||||
```
|
||||
|
||||
Commands are executed in order after all other setup tasks complete.
|
||||
|
||||
## Configuration File Management
|
||||
|
||||
### Directory Structure
|
||||
|
||||
Configuration files follow GNU Stow conventions:
|
||||
|
||||
```
|
||||
config/
|
||||
├── shared/ # Default configurations
|
||||
│ ├── zsh/
|
||||
│ │ ├── .zshrc # Links to ~/.zshrc
|
||||
│ │ └── .config/
|
||||
│ │ └── zsh/
|
||||
│ │ └── aliases # Links to ~/.config/zsh/aliases
|
||||
│ └── bin/
|
||||
│ └── my-script # Links to ~/bin/my-script
|
||||
└── macos-host/ # Environment-specific overrides
|
||||
└── zsh/
|
||||
└── .zshrc # Overrides shared zsh config
|
||||
```
|
||||
|
||||
### Linking Priority
|
||||
|
||||
1. **Environment-specific** configs (`config/<environment>/`) take precedence
|
||||
2. **Shared** configs (`config/shared/`) used as fallback
|
||||
3. Files are symlinked to preserve the exact directory structure
|
||||
|
||||
## Variable Substitution
|
||||
|
||||
Variables can be used in scripts and strings:
|
||||
|
||||
- `$USER`: Current username
|
||||
- `$TARGET_HOSTNAME`: Target hostname (from environment or `--set` parameter)
|
||||
- `$HOME`: User home directory
|
||||
|
||||
Custom variables can be provided at runtime:
|
||||
|
||||
```bash
|
||||
./dotfiles.py --environment linux-vm --set TARGET_HOSTNAME=myserver --set DOTFILES_GIT_REMOTE=git@github.com:user/dotfiles.git
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Environment Installation
|
||||
|
||||
```bash
|
||||
# Install complete environment
|
||||
./dotfiles.py --environment macos-host
|
||||
|
||||
# Install with custom variables
|
||||
./dotfiles.py --environment linux-vm --set TARGET_HOSTNAME=development-server
|
||||
```
|
||||
|
||||
### Example Environments
|
||||
|
||||
**macOS Desktop Setup:**
|
||||
```yaml
|
||||
macos-host:
|
||||
os: macos
|
||||
hostname: macbook-pro
|
||||
package-manager: brew
|
||||
packages:
|
||||
formula: [git, tmux, zsh]
|
||||
cask:
|
||||
- brave-browser
|
||||
- name: discord
|
||||
post-link-comment: "Import settings manually"
|
||||
configs: [bin] # Only link bin, other configs linked automatically
|
||||
ssh_keygen:
|
||||
- type: ed25519
|
||||
comment: "$USER@$TARGET_HOSTNAME"
|
||||
```
|
||||
|
||||
**Linux Server Setup:**
|
||||
```yaml
|
||||
linux-server:
|
||||
requires: [TARGET_HOSTNAME]
|
||||
os: linux
|
||||
hostname: $TARGET_HOSTNAME
|
||||
shell: zsh
|
||||
packages:
|
||||
package: [zsh, tmux, git, htop]
|
||||
binary: [neovim, tree-sitter]
|
||||
configs: [bin] # Link custom scripts
|
||||
runcmd:
|
||||
- mkdir -p ~/projects
|
||||
- systemctl --user enable podman.socket
|
||||
```
|
||||
19
notes/reused-parts.md
Normal file
19
notes/reused-parts.md
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
```
|
||||
docker-login:
|
||||
type: user-setup
|
||||
requires:
|
||||
- DOCKER_REGISTRY_USERNAME
|
||||
- DOCKER_REGISTRY_PASSWORD
|
||||
- DOCKER_REGISTRY
|
||||
script: |
|
||||
echo "$DOCKER_REGISTRY_PASSWORD" | docker login "$DOCKER_REGISTRY" -u "$DOCKER_REGISTRY_USERNAME" --password-stdin
|
||||
```
|
||||
|
||||
$ brew list --cask | tr ' ' '\n' | sort | sed 's/^/- package: /'
|
||||
|
||||
```
|
||||
if [ -f ~/.aliases ]; then
|
||||
source ~/.aliases
|
||||
fi
|
||||
```
|
||||
508
notes/tmp-script.py
Normal file
508
notes/tmp-script.py
Normal file
@ -0,0 +1,508 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional
|
||||
import re
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
|
||||
class DotfilesManager:
|
||||
def __init__(self, manifest_path: str = "manifest.yaml"):
|
||||
self.manifest_path = Path(manifest_path)
|
||||
self.dotfiles_dir = Path.home() / ".dotfiles"
|
||||
self.config_dir = self.dotfiles_dir / "config"
|
||||
self.variables = {}
|
||||
self.manifest = {}
|
||||
|
||||
# Detect system info
|
||||
self.system_os = "macos" if platform.system() == "Darwin" else "linux"
|
||||
self.system_arch = self._get_system_arch()
|
||||
self.system_platform = f"{self.system_os}-{self.system_arch}"
|
||||
|
||||
# Load manifest
|
||||
self._load_manifest()
|
||||
|
||||
def _get_system_arch(self) -> str:
|
||||
arch = platform.machine().lower()
|
||||
if arch in ["x86_64", "amd64"]:
|
||||
return "amd64"
|
||||
elif arch in ["aarch64", "arm64"]:
|
||||
return "arm64"
|
||||
else:
|
||||
return arch
|
||||
|
||||
def _load_manifest(self):
|
||||
"""Load the YAML manifest file"""
|
||||
try:
|
||||
with open(self.manifest_path, 'r') as f:
|
||||
self.manifest = yaml.safe_load(f)
|
||||
except FileNotFoundError:
|
||||
self.error(f"Manifest file not found: {self.manifest_path}")
|
||||
except yaml.YAMLError as e:
|
||||
self.error(f"Error parsing manifest: {e}")
|
||||
|
||||
def _substitute_variables(self, text: str) -> str:
|
||||
"""Substitute variables in text"""
|
||||
if not isinstance(text, str):
|
||||
return text
|
||||
|
||||
# Substitute environment variables and custom variables
|
||||
for var, value in self.variables.items():
|
||||
text = text.replace(f"${var}", str(value))
|
||||
text = text.replace(f"${{{var}}}", str(value))
|
||||
|
||||
# Substitute common environment variables
|
||||
text = text.replace("$USER", os.getenv("USER", ""))
|
||||
text = text.replace("$HOME", str(Path.home()))
|
||||
|
||||
return text
|
||||
|
||||
def info(self, message: str):
|
||||
"""Print info message"""
|
||||
print(f"[INFO] {message}")
|
||||
|
||||
def warn(self, message: str):
|
||||
"""Print warning message"""
|
||||
print(f"[WARN] {message}")
|
||||
|
||||
def error(self, message: str):
|
||||
"""Print error message and exit"""
|
||||
print(f"[ERROR] {message}")
|
||||
sys.exit(1)
|
||||
|
||||
def run_command(self, command: str, check: bool = True, shell: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a shell command"""
|
||||
self.info(f"Running: {command}")
|
||||
try:
|
||||
result = subprocess.run(command, shell=shell, check=check,
|
||||
capture_output=True, text=True)
|
||||
if result.stdout:
|
||||
print(result.stdout)
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.error(f"Command failed: {command}\nError: {e.stderr}")
|
||||
|
||||
def _detect_package_manager(self, os_type: str) -> str:
|
||||
"""Detect available package manager"""
|
||||
if os_type == "macos":
|
||||
if shutil.which("brew"):
|
||||
return "brew"
|
||||
else:
|
||||
self.info("Installing Homebrew...")
|
||||
self.run_command('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"')
|
||||
return "brew"
|
||||
else: # linux
|
||||
if shutil.which("apt"):
|
||||
return "apt"
|
||||
elif shutil.which("yum"):
|
||||
return "yum"
|
||||
elif shutil.which("pacman"):
|
||||
return "pacman"
|
||||
else:
|
||||
self.error("No supported package manager found")
|
||||
|
||||
def _install_package(self, package_manager: str, package_type: str, package_name: str):
|
||||
"""Install a single package"""
|
||||
if package_manager == "brew":
|
||||
if package_type == "cask":
|
||||
self.run_command(f"brew install --cask {package_name}")
|
||||
else:
|
||||
self.run_command(f"brew install {package_name}")
|
||||
elif package_manager == "apt":
|
||||
self.run_command(f"sudo apt-get update && sudo apt-get install -y {package_name}")
|
||||
elif package_manager == "yum":
|
||||
self.run_command(f"sudo yum install -y {package_name}")
|
||||
elif package_manager == "pacman":
|
||||
self.run_command(f"sudo pacman -S --noconfirm {package_name}")
|
||||
|
||||
def _get_github_release_url(self, repo: str, version: str, asset_pattern: str, platform_map: Dict) -> str:
|
||||
"""Get GitHub release download URL"""
|
||||
if self.system_platform not in platform_map:
|
||||
self.error(f"Platform {self.system_platform} not supported for {repo}")
|
||||
|
||||
platform_info = platform_map[self.system_platform]
|
||||
asset_name = asset_pattern.replace("{{os}}", platform_info["os"]).replace("{{arch}}", platform_info["arch"])
|
||||
|
||||
return f"https://github.com/{repo}/releases/download/v{version}/{asset_name}"
|
||||
|
||||
def _install_binary(self, binary_name: str, binary_config: Dict):
|
||||
"""Install a binary from the binaries section"""
|
||||
self.info(f"Installing binary: {binary_name}")
|
||||
|
||||
# Install dependencies first
|
||||
if "dependencies" in binary_config:
|
||||
pm = self._detect_package_manager(self.system_os)
|
||||
for dep in binary_config["dependencies"]:
|
||||
self._install_package(pm, "package", dep)
|
||||
|
||||
# Get download URL
|
||||
if binary_config["source"].startswith("github:"):
|
||||
repo = binary_config["source"].replace("github:", "")
|
||||
download_url = self._get_github_release_url(
|
||||
repo,
|
||||
binary_config["version"],
|
||||
binary_config["asset-pattern"],
|
||||
binary_config["platform-map"]
|
||||
)
|
||||
else:
|
||||
self.error(f"Unsupported binary source: {binary_config['source']}")
|
||||
|
||||
# Substitute variables in install script
|
||||
install_script = binary_config["install-script"]
|
||||
install_script = install_script.replace("{{downloadUrl}}", download_url)
|
||||
install_script = install_script.replace("{{version}}", binary_config["version"])
|
||||
if self.system_platform in binary_config["platform-map"]:
|
||||
platform_info = binary_config["platform-map"][self.system_platform]
|
||||
install_script = install_script.replace("{{os}}", platform_info["os"])
|
||||
install_script = install_script.replace("{{arch}}", platform_info["arch"])
|
||||
|
||||
# Run install script
|
||||
self.run_command(install_script)
|
||||
|
||||
def _symlink_config(self, source_path: Path, target_path: Path, copy: bool = False, force: bool = False):
|
||||
"""Create symlink or copy for configuration file"""
|
||||
if target_path.exists() and not force:
|
||||
self.warn(f"Target already exists, skipping: {target_path}")
|
||||
return
|
||||
|
||||
if target_path.exists() and force:
|
||||
if target_path.is_dir():
|
||||
shutil.rmtree(target_path)
|
||||
else:
|
||||
target_path.unlink()
|
||||
|
||||
# Create parent directories
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if copy:
|
||||
if source_path.is_dir():
|
||||
shutil.copytree(source_path, target_path)
|
||||
else:
|
||||
shutil.copy2(source_path, target_path)
|
||||
self.info(f"Copied: {source_path} -> {target_path}")
|
||||
else:
|
||||
target_path.symlink_to(source_path)
|
||||
self.info(f"Linked: {source_path} -> {target_path}")
|
||||
|
||||
def _link_config_directory(self, config_name: str, environment: str, copy: bool = False, force: bool = False):
|
||||
"""Link all files from a config directory"""
|
||||
# Try environment-specific config first, then shared
|
||||
env_config_dir = self.config_dir / environment / config_name
|
||||
shared_config_dir = self.config_dir / "shared" / config_name
|
||||
|
||||
source_dir = env_config_dir if env_config_dir.exists() else shared_config_dir
|
||||
|
||||
if not source_dir.exists():
|
||||
self.warn(f"Config directory not found: {config_name}")
|
||||
return
|
||||
|
||||
self.info(f"Linking config: {config_name} from {source_dir}")
|
||||
|
||||
# Walk through all files and directories
|
||||
for item in source_dir.rglob("*"):
|
||||
if item.is_file():
|
||||
# Calculate relative path from source_dir
|
||||
rel_path = item.relative_to(source_dir)
|
||||
target_path = Path.home() / rel_path
|
||||
self._symlink_config(item, target_path, copy, force)
|
||||
|
||||
def _set_hostname(self, hostname: str):
|
||||
"""Set system hostname"""
|
||||
hostname = self._substitute_variables(hostname)
|
||||
self.info(f"Setting hostname to: {hostname}")
|
||||
|
||||
if self.system_os == "macos":
|
||||
self.run_command(f"sudo scutil --set ComputerName '{hostname}'")
|
||||
self.run_command(f"sudo scutil --set HostName '{hostname}'")
|
||||
self.run_command(f"sudo scutil --set LocalHostName '{hostname}'")
|
||||
else:
|
||||
self.run_command(f"sudo hostnamectl set-hostname '{hostname}'")
|
||||
|
||||
def _set_shell(self, shell: str):
|
||||
"""Set default shell"""
|
||||
shell_path = shutil.which(shell)
|
||||
if not shell_path:
|
||||
self.error(f"Shell not found: {shell}")
|
||||
|
||||
self.info(f"Setting shell to: {shell_path}")
|
||||
|
||||
# Add shell to /etc/shells if not present
|
||||
try:
|
||||
with open("/etc/shells", "r") as f:
|
||||
shells = f.read()
|
||||
if shell_path not in shells:
|
||||
self.run_command(f"echo '{shell_path}' | sudo tee -a /etc/shells")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# Change user shell
|
||||
self.run_command(f"chsh -s {shell_path}")
|
||||
|
||||
def _set_locale(self, locale: str):
|
||||
"""Set system locale"""
|
||||
if self.system_os == "linux":
|
||||
self.info(f"Setting locale to: {locale}")
|
||||
self.run_command(f"sudo locale-gen {locale}")
|
||||
self.run_command(f"sudo update-locale LANG={locale}")
|
||||
|
||||
def _generate_ssh_keys(self, ssh_configs: List[Dict]):
|
||||
"""Generate SSH keys"""
|
||||
ssh_dir = Path.home() / ".ssh"
|
||||
ssh_dir.mkdir(mode=0o700, exist_ok=True)
|
||||
|
||||
for config in ssh_configs:
|
||||
key_type = config["type"]
|
||||
comment = self._substitute_variables(config.get("comment", ""))
|
||||
filename = config.get("filename", f"id_{key_type}")
|
||||
|
||||
key_path = ssh_dir / filename
|
||||
|
||||
if key_path.exists():
|
||||
self.warn(f"SSH key already exists: {key_path}")
|
||||
continue
|
||||
|
||||
self.info(f"Generating SSH key: {key_path}")
|
||||
cmd = f'ssh-keygen -t {key_type} -f "{key_path}" -N "" -C "{comment}"'
|
||||
self.run_command(cmd)
|
||||
|
||||
def setup_environment(self, environment_name: str):
|
||||
"""Setup complete environment"""
|
||||
if environment_name not in self.manifest.get("environments", {}):
|
||||
self.error(f"Environment not found: {environment_name}")
|
||||
|
||||
env_config = self.manifest["environments"][environment_name]
|
||||
|
||||
# Check required variables
|
||||
if "requires" in env_config:
|
||||
for req_var in env_config["requires"]:
|
||||
if req_var not in self.variables:
|
||||
self.error(f"Required variable not set: {req_var}")
|
||||
|
||||
self.info(f"Setting up environment: {environment_name}")
|
||||
|
||||
# Set hostname
|
||||
if "hostname" in env_config:
|
||||
self._set_hostname(env_config["hostname"])
|
||||
|
||||
# Set shell
|
||||
if "shell" in env_config:
|
||||
self._set_shell(env_config["shell"])
|
||||
|
||||
# Set locale
|
||||
if "locale" in env_config:
|
||||
self._set_locale(env_config["locale"])
|
||||
|
||||
# Install packages
|
||||
self.install_packages(environment_name)
|
||||
|
||||
# Link configurations
|
||||
self.link_configs(environment_name)
|
||||
|
||||
# Generate SSH keys
|
||||
if "ssh_keygen" in env_config:
|
||||
self._generate_ssh_keys(env_config["ssh_keygen"])
|
||||
|
||||
# Run custom commands
|
||||
if "runcmd" in env_config:
|
||||
for command in env_config["runcmd"]:
|
||||
command = self._substitute_variables(command)
|
||||
self.run_command(command)
|
||||
|
||||
self.info(f"Environment setup complete: {environment_name}")
|
||||
|
||||
def install_packages(self, environment_name: str, package_name: str = None):
|
||||
"""Install packages for environment"""
|
||||
if environment_name not in self.manifest.get("environments", {}):
|
||||
self.error(f"Environment not found: {environment_name}")
|
||||
|
||||
env_config = self.manifest["environments"][environment_name]
|
||||
|
||||
if "packages" not in env_config:
|
||||
self.info("No packages to install")
|
||||
return
|
||||
|
||||
# Detect package manager
|
||||
pm = env_config.get("package-manager")
|
||||
if not pm:
|
||||
pm = self._detect_package_manager(env_config.get("os", self.system_os))
|
||||
|
||||
packages = env_config["packages"]
|
||||
|
||||
# Install specific package if requested
|
||||
if package_name:
|
||||
self._install_single_package(packages, package_name, pm, environment_name)
|
||||
return
|
||||
|
||||
# Install all packages
|
||||
for package_type, package_list in packages.items():
|
||||
for package in package_list:
|
||||
if isinstance(package, str):
|
||||
package_spec = {"name": package}
|
||||
else:
|
||||
package_spec = package
|
||||
|
||||
self._install_package_spec(package_spec, package_type, pm, environment_name)
|
||||
|
||||
def _install_single_package(self, packages: Dict, package_name: str, pm: str, environment_name: str):
|
||||
"""Install a single package by name"""
|
||||
for package_type, package_list in packages.items():
|
||||
for package in package_list:
|
||||
if isinstance(package, str):
|
||||
if package == package_name:
|
||||
self._install_package_spec({"name": package}, package_type, pm, environment_name)
|
||||
return
|
||||
else:
|
||||
if package.get("name") == package_name:
|
||||
self._install_package_spec(package, package_type, pm, environment_name)
|
||||
return
|
||||
|
||||
self.error(f"Package not found: {package_name}")
|
||||
|
||||
def _install_package_spec(self, package_spec: Dict, package_type: str, pm: str, environment_name: str):
|
||||
"""Install a package specification"""
|
||||
package_name = package_spec["name"]
|
||||
|
||||
if package_type == "binary":
|
||||
# Install from binaries section
|
||||
if package_name not in self.manifest.get("binaries", {}):
|
||||
self.error(f"Binary not found in manifest: {package_name}")
|
||||
self._install_binary(package_name, self.manifest["binaries"][package_name])
|
||||
else:
|
||||
# Install via package manager
|
||||
self._install_package(pm, package_type, package_name)
|
||||
|
||||
# Run post-install script
|
||||
if "post-install" in package_spec:
|
||||
script = self._substitute_variables(package_spec["post-install"])
|
||||
self.run_command(script)
|
||||
|
||||
# Show post-install comment
|
||||
if "post-install-comment" in package_spec:
|
||||
comment = self._substitute_variables(package_spec["post-install-comment"])
|
||||
self.info(f"POST-INSTALL: {comment}")
|
||||
|
||||
# Link config if not disabled
|
||||
if package_spec.get("link", True):
|
||||
self._link_config_directory(package_name, environment_name)
|
||||
|
||||
# Run post-link script
|
||||
if "post-link" in package_spec:
|
||||
script = self._substitute_variables(package_spec["post-link"])
|
||||
self.run_command(script)
|
||||
|
||||
# Show post-link comment
|
||||
if "post-link-comment" in package_spec:
|
||||
comment = self._substitute_variables(package_spec["post-link-comment"])
|
||||
self.info(f"POST-LINK: {comment}")
|
||||
|
||||
def link_configs(self, environment_name: str, config_name: str = None, copy: bool = False, force: bool = False):
|
||||
"""Link configuration files"""
|
||||
if environment_name not in self.manifest.get("environments", {}):
|
||||
self.error(f"Environment not found: {environment_name}")
|
||||
|
||||
env_config = self.manifest["environments"][environment_name]
|
||||
|
||||
if config_name:
|
||||
# Link specific config
|
||||
self._link_config_directory(config_name, environment_name, copy, force)
|
||||
self._run_config_post_link(config_name, env_config)
|
||||
else:
|
||||
# Link all configs
|
||||
configs = env_config.get("configs", [])
|
||||
for config in configs:
|
||||
if isinstance(config, str):
|
||||
config_spec = {"name": config}
|
||||
else:
|
||||
config_spec = config
|
||||
|
||||
config_name = config_spec["name"] if "name" in config_spec else config
|
||||
self._link_config_directory(config_name, environment_name, copy, force)
|
||||
self._run_config_post_link(config_spec, env_config)
|
||||
|
||||
def _run_config_post_link(self, config_spec, env_config):
|
||||
"""Run post-link actions for config"""
|
||||
if isinstance(config_spec, str):
|
||||
return
|
||||
|
||||
# Run post-link script
|
||||
if "post-link" in config_spec:
|
||||
script = self._substitute_variables(config_spec["post-link"])
|
||||
self.run_command(script)
|
||||
|
||||
# Show post-link comment
|
||||
if "post-link-comment" in config_spec:
|
||||
comment = self._substitute_variables(config_spec["post-link-comment"])
|
||||
self.info(f"POST-LINK: {comment}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Dotfiles Manager")
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
||||
# Setup command
|
||||
setup_parser = subparsers.add_parser("setup", help="Setup complete environment")
|
||||
setup_parser.add_argument("environment", help="Environment name")
|
||||
setup_parser.add_argument("--set", action="append", default=[],
|
||||
help="Set variable (format: VAR=value)")
|
||||
|
||||
# Link command
|
||||
link_parser = subparsers.add_parser("link", help="Link configurations")
|
||||
link_parser.add_argument("environment", help="Environment name")
|
||||
link_parser.add_argument("--copy", action="store_true", help="Copy instead of symlink")
|
||||
link_parser.add_argument("-f", "--force", action="store_true", help="Force overwrite")
|
||||
link_parser.add_argument("-p", "--package", help="Link specific package")
|
||||
link_parser.add_argument("--set", action="append", default=[],
|
||||
help="Set variable (format: VAR=value)")
|
||||
|
||||
# Install command
|
||||
install_parser = subparsers.add_parser("install", help="Install packages")
|
||||
install_parser.add_argument("environment", help="Environment name")
|
||||
install_parser.add_argument("-p", "--package", help="Install specific package")
|
||||
install_parser.add_argument("--set", action="append", default=[],
|
||||
help="Set variable (format: VAR=value)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
# Initialize manager
|
||||
manager = DotfilesManager()
|
||||
|
||||
# Parse variables
|
||||
for var_setting in args.set:
|
||||
if "=" not in var_setting:
|
||||
manager.error(f"Invalid variable format: {var_setting}")
|
||||
key, value = var_setting.split("=", 1)
|
||||
manager.variables[key] = value
|
||||
|
||||
# Add TARGET_HOSTNAME if not set
|
||||
if "TARGET_HOSTNAME" not in manager.variables:
|
||||
manager.variables["TARGET_HOSTNAME"] = manager.variables.get("TARGET_HOSTNAME", "localhost")
|
||||
|
||||
# Execute command
|
||||
try:
|
||||
if args.command == "setup":
|
||||
manager.setup_environment(args.environment)
|
||||
elif args.command == "link":
|
||||
manager.link_configs(args.environment, args.package, args.copy, args.force)
|
||||
elif args.command == "install":
|
||||
manager.install_packages(args.environment, args.package)
|
||||
except KeyboardInterrupt:
|
||||
manager.info("Operation cancelled by user")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
manager.error(f"Unexpected error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
61
notes/tmp.md
Normal file
61
notes/tmp.md
Normal file
@ -0,0 +1,61 @@
|
||||
- We have the manifest.
|
||||
- Define empty actions array.
|
||||
- Loop over the manifest, looking for:
|
||||
- check required variables
|
||||
- hostname, push action
|
||||
- package manager, push action
|
||||
- push action: update package manager
|
||||
- for standard:
|
||||
- create array with formatted package items
|
||||
- Install step: map over with proper handler
|
||||
- Post-install post-install-cmment steps: for_each over executing script
|
||||
- for each over executing the post-install script
|
||||
- with the same context, execute post-install-comment and store output
|
||||
|
||||
- Post-install-comment:
|
||||
- group standard packages and push action
|
||||
- install casks, push action
|
||||
- install binaries, for each one: push action
|
||||
|
||||
|
||||
Steps:
|
||||
- Check required variables
|
||||
- check VAR1: YES
|
||||
- check VAR2: YES
|
||||
- check VAR3: YES
|
||||
- Detect os
|
||||
- Detected
|
||||
- Validated with the one specified on the manifest
|
||||
- Set hostname
|
||||
- Changing hostname to HOSTNAME
|
||||
- Detect pm
|
||||
- Detected
|
||||
- Validated with the one specified on the manifest
|
||||
- Update and upgrade packages with pm
|
||||
- Updated
|
||||
- Install packages
|
||||
- [bulk-standard]
|
||||
- ...apt
|
||||
- completed
|
||||
- [bulk-casks]
|
||||
- ..blabla
|
||||
- completed
|
||||
- []
|
||||
- Install standard packages
|
||||
- Bulk all packages: install apt pkg1 pkg2 pkg3
|
||||
- completed
|
||||
- Post-Install standard packages
|
||||
- [pkg1]
|
||||
- echo this and that
|
||||
- completed
|
||||
- [pkg2]
|
||||
- completed
|
||||
- Install gui packages (casks)
|
||||
- Post-Install gui packages
|
||||
- Install binaries
|
||||
- [binary 1]
|
||||
- completed
|
||||
- [binary 2]
|
||||
- completed
|
||||
- Post-Install binary packages
|
||||
|
||||
5
pyproject.toml
Normal file
5
pyproject.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <new-hostname>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEW_HOSTNAME=$1
|
||||
OLD_HOSTNAME=$(hostname)
|
||||
|
||||
echo ""
|
||||
echo "Changing hostname to '$NEW_HOSTNAME'..."
|
||||
|
||||
sudo hostnamectl set-hostname "$NEW_HOSTNAME"
|
||||
|
||||
sudo sed -i "s/$OLD_HOSTNAME/$NEW_HOSTNAME/g" /etc/hosts
|
||||
|
||||
echo ""
|
||||
echo "Hostname has been changed to: $(hostname)"
|
||||
@ -1,75 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Removing old Docker versions..."
|
||||
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
|
||||
sudo apt-get remove -y $pkg || true
|
||||
done
|
||||
|
||||
# Detect OS
|
||||
if grep -q "Ubuntu" /etc/os-release 2>/dev/null; then
|
||||
DOCKER_OS="ubuntu"
|
||||
# Source the os-release file to get variables
|
||||
. /etc/os-release
|
||||
# Use UBUNTU_CODENAME with fallback to VERSION_CODENAME
|
||||
CODENAME=${UBUNTU_CODENAME:-$VERSION_CODENAME}
|
||||
elif [ -f /etc/debian_version ]; then
|
||||
DOCKER_OS="debian"
|
||||
# Source the os-release file to get VERSION_CODENAME
|
||||
. /etc/os-release
|
||||
CODENAME=$VERSION_CODENAME
|
||||
else
|
||||
echo "Error: Unsupported OS"
|
||||
exit 1
|
||||
fi
|
||||
echo "Detected OS: ${DOCKER_OS}, Codename: ${CODENAME}"
|
||||
|
||||
echo "Updating package list and installing dependencies..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
|
||||
|
||||
echo "Setting up Docker repository..."
|
||||
# Add Docker's official GPG key
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
sudo curl -fsSL https://download.docker.com/linux/${DOCKER_OS}/gpg -o /etc/apt/keyrings/docker.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
# Add the repository to Apt sources
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${DOCKER_OS} \
|
||||
${CODENAME} stable" | \
|
||||
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
|
||||
echo "Installing Docker..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
docker-ce \
|
||||
docker-ce-cli \
|
||||
containerd.io \
|
||||
docker-buildx-plugin \
|
||||
docker-compose-plugin
|
||||
|
||||
# Verify installation
|
||||
echo "Verifying Docker installation..."
|
||||
sudo docker --version
|
||||
sudo containerd --version
|
||||
|
||||
echo "Configuring Docker permissions..."
|
||||
sudo groupadd docker 2>/dev/null || true
|
||||
sudo usermod -aG docker ${SUDO_USER:-$USER}
|
||||
|
||||
# Check if we're running in a systemd environment
|
||||
echo "Checking for systemd..."
|
||||
if pidof systemd > /dev/null && [ -d /run/systemd/system ]; then
|
||||
echo "systemd detected - enabling and starting Docker services using systemctl..."
|
||||
sudo systemctl enable --now docker.service
|
||||
sudo systemctl enable --now containerd.service
|
||||
else
|
||||
echo "systemd not detected (likely in a container) - starting Docker daemon directly..."
|
||||
# For containers or non-systemd environments, we can just start the Docker daemon directly
|
||||
sudo dockerd > /dev/null 2>&1 &
|
||||
echo "Docker daemon started in background."
|
||||
fi
|
||||
|
||||
echo "Docker setup completed. Please log out and log back in for group changes to take effect."
|
||||
@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Check if Zsh is already installed
|
||||
if command -v zsh &> /dev/null; then
|
||||
echo "Zsh is already installed. Skipping installation."
|
||||
else
|
||||
echo "Updating package list..."
|
||||
sudo apt-get update
|
||||
|
||||
echo "Installing Zsh..."
|
||||
sudo apt-get install -y zsh
|
||||
|
||||
if ! command -v zsh &> /dev/null; then
|
||||
echo "Error: Zsh installation failed."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Changing default shell to Zsh for user $(whoami)..."
|
||||
zsh_path=$(command -v zsh)
|
||||
sudo chsh -s "$zsh_path" "$(whoami)"
|
||||
|
||||
echo "Zsh installation and setup complete. Please log out and log back in for changes to take effect."
|
||||
@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
BACKUP_FILE="$HOME/.dotfiles/config/macos/homebrew/Brewfile"
|
||||
|
||||
echo "Backing up Homebrew installations..."
|
||||
mkdir -p "$(dirname "$BACKUP_FILE")"
|
||||
|
||||
brew bundle dump --file="$BACKUP_FILE" --force
|
||||
|
||||
echo "Backup saved to $BACKUP_FILE"
|
||||
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
BACKUP_FILE="$HOME/.dotfiles/config/envs/macos/homebrew/Brewfile"
|
||||
|
||||
if [[ ! -f "$BACKUP_FILE" ]]; then
|
||||
echo "Backup file not found: $BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Restoring Homebrew installations..."
|
||||
brew bundle --file="$BACKUP_FILE"
|
||||
|
||||
echo "Homebrew restoration complete."
|
||||
@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <new-hostname>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEW_HOSTNAME="$1"
|
||||
|
||||
echo ""
|
||||
echo "Changing hostname to '$NEW_HOSTNAME'..."
|
||||
|
||||
|
||||
# !!! IMPORTANT !!!
|
||||
# double check what are we changing as hostname should end up with *.local
|
||||
sudo scutil --set HostName "$NEW_HOSTNAME"
|
||||
sudo scutil --set ComputerName "$NEW_HOSTNAME"
|
||||
sudo scutil --set LocalHostName "$NEW_HOSTNAME"
|
||||
|
||||
echo ""
|
||||
echo "Hostname has been updated:"
|
||||
echo "HostName: $(scutil --get HostName)"
|
||||
echo "ComputerName: $(scutil --get ComputerName)"
|
||||
echo "LocalHostName: $(scutil --get LocalHostName)"
|
||||
@ -1,196 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Close any open System Preferences panes, to prevent them from overriding
|
||||
# settings we’re about to change
|
||||
osascript -e 'tell application "System Preferences" to quit'
|
||||
|
||||
# Ask for the administrator password upfront
|
||||
sudo -v
|
||||
|
||||
# Keep-alive: update existing `sudo` time stamp until `.macos` has finished
|
||||
while true; do
|
||||
sudo -n true
|
||||
sleep 60
|
||||
kill -0 "$$" || exit
|
||||
done 2>/dev/null &
|
||||
|
||||
# Save to disk (not to iCloud) by default
|
||||
defaults write NSGlobalDomain NSDocumentSaveNewDocumentsToCloud -bool false
|
||||
|
||||
# Disable the “Are you sure you want to open this application?” dialog
|
||||
defaults write com.apple.LaunchServices LSQuarantine -bool false
|
||||
|
||||
# Disable Typing features
|
||||
defaults write NSGlobalDomain NSAutomaticCapitalizationEnabled -bool false
|
||||
defaults write NSGlobalDomain NSAutomaticDashSubstitutionEnabled -bool false
|
||||
defaults write NSGlobalDomain NSAutomaticPeriodSubstitutionEnabled -bool false
|
||||
defaults write NSGlobalDomain NSAutomaticQuoteSubstitutionEnabled -bool false
|
||||
defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false
|
||||
|
||||
# Disable press-and-hold for keys in favor of key repeat
|
||||
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false
|
||||
defaults write NSGlobalDomain KeyRepeat -int 2
|
||||
defaults write NSGlobalDomain InitialKeyRepeat -int 15
|
||||
|
||||
defaults write NSGlobalDomain AppleLanguages -array "en" "es" "bg"
|
||||
defaults write NSGlobalDomain AppleLocale -string "en_US@rg=eszzzz"
|
||||
|
||||
## Finder
|
||||
|
||||
# Screenshots/captures
|
||||
defaults write com.apple.screencapture location -string "${HOME}/Pictures/Screenshots"
|
||||
defaults write com.apple.screencapture style -string "display"
|
||||
defaults write com.apple.screencapture target -string "file"
|
||||
defaults write com.apple.screencapture video -int 1
|
||||
|
||||
# Finder
|
||||
# Interface elements
|
||||
defaults write com.apple.finder ShowPathbar -bool true
|
||||
defaults write com.apple.finder ShowStatusBar -bool true
|
||||
defaults write com.apple.finder ShowSidebar -bool true
|
||||
defaults write com.apple.finder ShowRecentTags -bool false
|
||||
|
||||
# View and sorting
|
||||
defaults write com.apple.finder FXPreferredViewStyle -string "Nlsv"
|
||||
defaults write com.apple.finder FXPreferredSearchViewStyle -string "Nlsv"
|
||||
defaults write com.apple.finder _FXSortFoldersFirst -bool true
|
||||
defaults write com.apple.finder FXPreferredGroupBy -string "None"
|
||||
defaults write com.apple.finder FXDefaultSearchScope -string "SCcf"
|
||||
|
||||
# Behavior
|
||||
defaults write com.apple.finder NewWindowTarget -string "PfHm"
|
||||
defaults write com.apple.finder FXOpenFoldersInTabs -bool true
|
||||
defaults write com.apple.finder FXRemoveOldTrashItems -bool false
|
||||
defaults write com.apple.finder FXShowAllExtensions -bool true
|
||||
defaults write com.apple.finder FXEnableExtensionChangeWarning -bool true
|
||||
defaults write com.apple.finder FXRemoveICloudDriveWarning -bool true
|
||||
defaults write com.apple.finder FXWarnBeforeEmptyingTrash -bool true
|
||||
|
||||
# Desktop icons (none)
|
||||
defaults write com.apple.finder ShowHardDrivesOnDesktop -bool false
|
||||
defaults write com.apple.finder ShowExternalHardDrivesOnDesktop -bool false
|
||||
defaults write com.apple.finder ShowRemovableMediaOnDesktop -bool false
|
||||
defaults write com.apple.finder ShowConnectedServersOnDesktop -bool false
|
||||
|
||||
# Tags
|
||||
defaults write com.apple.finder FavoriteTagNames -array
|
||||
|
||||
# iCloud
|
||||
defaults write com.apple.finder FXICloudDriveEnabled -bool false
|
||||
|
||||
# Finder: show all filename extensions
|
||||
defaults write NSGlobalDomain AppleShowAllExtensions -bool true
|
||||
|
||||
# Avoid creating .DS_Store files on network or USB volumes
|
||||
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true
|
||||
defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true
|
||||
|
||||
## Dock
|
||||
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Reset preferences
|
||||
# rm -f ~/Library/Preferences/com.apple.dock.plist
|
||||
|
||||
# Reset Dock layout
|
||||
defaults write com.apple.dock persistent-apps -array
|
||||
defaults write com.apple.dock persistent-others -array
|
||||
|
||||
# Basic Dock preferences
|
||||
defaults write com.apple.dock autohide -bool true
|
||||
defaults write com.apple.dock autohide-delay -float 0
|
||||
defaults write com.apple.dock autohide-time-modifier -float 0.4
|
||||
defaults write com.apple.dock enterMissionControlByTopWindowDrag -bool false
|
||||
defaults write com.apple.dock expose-group-apps -bool true
|
||||
defaults write com.apple.dock mineffect -string "scale"
|
||||
defaults write com.apple.dock minimize-to-application -bool false
|
||||
defaults write com.apple.dock orientation -string "bottom"
|
||||
defaults write com.apple.dock show-process-indicators -bool false
|
||||
defaults write com.apple.dock show-recents -bool false
|
||||
defaults write com.apple.dock showAppExposeGestureEnabled -bool true
|
||||
defaults write com.apple.dock showDesktopGestureEnabled -bool false
|
||||
defaults write com.apple.dock showLaunchpadGestureEnabled -bool false
|
||||
defaults write com.apple.dock tilesize -int 38
|
||||
|
||||
# Add Brave Browser
|
||||
defaults write com.apple.dock persistent-apps -array-add \
|
||||
"<dict>
|
||||
<key>tile-data</key>
|
||||
<dict>
|
||||
<key>file-data</key>
|
||||
<dict>
|
||||
<key>_CFURLString</key>
|
||||
<string>/Applications/Brave Browser.app</string>
|
||||
<key>_CFURLStringType</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>tile-type</key>
|
||||
<string>file-tile</string>
|
||||
</dict>"
|
||||
|
||||
# Add Ghostty
|
||||
defaults write com.apple.dock persistent-apps -array-add \
|
||||
"<dict>
|
||||
<key>tile-data</key>
|
||||
<dict>
|
||||
<key>file-data</key>
|
||||
<dict>
|
||||
<key>_CFURLString</key>
|
||||
<string>/Applications/Ghostty.app</string>
|
||||
<key>_CFURLStringType</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>tile-type</key>
|
||||
<string>file-tile</string>
|
||||
</dict>"
|
||||
|
||||
# Add Screenshots directory (display as folder, show as grid)
|
||||
defaults write com.apple.dock persistent-others -array-add \
|
||||
"<dict>
|
||||
<key>tile-data</key>
|
||||
<dict>
|
||||
<key>displayas</key>
|
||||
<integer>1</integer>
|
||||
<key>showas</key>
|
||||
<integer>2</integer>
|
||||
<key>file-data</key>
|
||||
<dict>
|
||||
<key>_CFURLString</key>
|
||||
<string>/Users/tomas/Pictures/Screenshots</string>
|
||||
<key>_CFURLStringType</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>tile-type</key>
|
||||
<string>directory-tile</string>
|
||||
</dict>"
|
||||
|
||||
# Add Downloads directory (display as folder, show as grid)
|
||||
defaults write com.apple.dock persistent-others -array-add \
|
||||
"<dict>
|
||||
<key>tile-data</key>
|
||||
<dict>
|
||||
<key>displayas</key>
|
||||
<integer>1</integer>
|
||||
<key>showas</key>
|
||||
<integer>2</integer>
|
||||
<key>file-data</key>
|
||||
<dict>
|
||||
<key>_CFURLString</key>
|
||||
<string>/Users/tomas/Downloads</string>
|
||||
<key>_CFURLStringType</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>tile-type</key>
|
||||
<string>directory-tile</string>
|
||||
</dict>"
|
||||
|
||||
# Apply changes
|
||||
killall Finder &>/dev/null
|
||||
killall Dock &>/dev/null
|
||||
echo "Setup complete."
|
||||
@ -1,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 --comment <comment> [--filename <filename>]"
|
||||
echo " --comment <comment> The comment for the SSH key."
|
||||
echo " --filename <filename> (optional) The filename suffix for the SSH key. Defaults to 'id_ed25519'."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Default values
|
||||
COMMENT=""
|
||||
FILENAME="id_ed25519"
|
||||
|
||||
# Parse named arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--filename)
|
||||
FILENAME="id_ed25519_$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required arguments
|
||||
if [ -z "$COMMENT" ]; then
|
||||
echo "Error: --comment is required."
|
||||
usage
|
||||
fi
|
||||
|
||||
SSH_DIR="$HOME/.ssh"
|
||||
KEY_PATH="$SSH_DIR/$FILENAME"
|
||||
|
||||
# Ensure SSH directory exists
|
||||
mkdir -p "$SSH_DIR"
|
||||
chmod 700 "$SSH_DIR"
|
||||
|
||||
# Generate SSH key
|
||||
if [ -f "$KEY_PATH" ]; then
|
||||
echo "Skipping: Key file $KEY_PATH already exists."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ssh-keygen -t ed25519 -C "$COMMENT" -f "$KEY_PATH" -N ""
|
||||
|
||||
echo "SSH key created at: $KEY_PATH"
|
||||
echo "Public key:"
|
||||
cat "$KEY_PATH.pub"
|
||||
5
secrets.yaml
Normal file
5
secrets.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
env:
|
||||
DOTFILES_GIT_REMOTE: "git@gitea.tomastm.com:tomas.mirchev/dotfiles.git"
|
||||
DOCKER_REGISTRY: "registry.tomastm.com"
|
||||
DOCKER_REGISTRY_USERNAME: "tomas"
|
||||
DOCKER_REGISTRY_PASSWORD: "Tomas12345!"
|
||||
@ -1,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "This script should not be run as root. Please run it as a normal user."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <new-hostname>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEW_HOSTNAME="$1"
|
||||
|
||||
if ! sudo -v; then
|
||||
echo "Sudo access required. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir ~/projects
|
||||
# /bin/bash ./scripts/linux-setup_sudoers.sh
|
||||
|
||||
/bin/bash ./scripts/linux-change_hostname.sh "$NEW_HOSTNAME"
|
||||
|
||||
echo ""
|
||||
echo "Installing docker..."
|
||||
/bin/bash ./scripts/linux-setup_docker.sh
|
||||
|
||||
echo ""
|
||||
echo "Creating internal SSH keys..."
|
||||
/bin/bash ./scripts/setup_ssh_keys.sh --comment "${USER}@${NEW_HOSTNAME}"
|
||||
|
||||
echo ""
|
||||
echo "Installing packages..."
|
||||
python3 ./manage.py install linux-vm
|
||||
|
||||
echo ""
|
||||
echo "Linking config..."
|
||||
python3 ./manage.py link linux-vm --force
|
||||
|
||||
echo ""
|
||||
echo "Updating git remote origin url..."
|
||||
git remote set-url origin git@gitea.tomastm.com:tomas.mirchev/dotfiles.git
|
||||
|
||||
echo ""
|
||||
echo "Setup complete. Follow the next steps to finalize your environment:"
|
||||
|
||||
echo ""
|
||||
echo "Update your git repository SSH key. Your public key is:"
|
||||
echo " $(cat ~/.ssh/id_ed25519.pub)"
|
||||
|
||||
echo ""
|
||||
echo "Start you development with \`dev -i <image> <container_name>\`"
|
||||
echo "Example: \`dev -i node mybox\`"
|
||||
echo "You will need to authenticate first: \`docker login registry.tomastm.com\`"
|
||||
@ -1,68 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "This script should not be run as root. Please run it as a normal user."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <new-hostname>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEW_HOSTNAME="$1"
|
||||
|
||||
if ! sudo -v; then
|
||||
echo "Sudo access required. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v brew &>/dev/null; then
|
||||
echo ""
|
||||
echo "Installing Homebrew..."
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null || /usr/local/bin/brew shellenv)"
|
||||
else
|
||||
echo "Homebrew already installed."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Installing brew packages..."
|
||||
/bin/bash ./scripts/macos-brew_restore.sh
|
||||
|
||||
echo ""
|
||||
echo "Linking configs..."
|
||||
python3 ./manage.py link macos --force
|
||||
|
||||
echo ""
|
||||
echo "Changing hostname..."
|
||||
/bin/bash ./scripts/macos-change_hostname.sh "$NEW_HOSTNAME"
|
||||
|
||||
echo ""
|
||||
echo "Creating internal SSH keys..."
|
||||
/bin/bash ./scripts/setup_ssh_keys.sh --comment "${USER}@${NEW_HOSTNAME}" --filename "internal"
|
||||
/bin/bash ./scripts/setup_ssh_keys.sh --comment "${USER}@${NEW_HOSTNAME}" --filename "git"
|
||||
|
||||
echo ""
|
||||
echo "Setup complete. Follow the next steps to finalize your environment:"
|
||||
|
||||
echo ""
|
||||
echo "Set up UTM and configure DNS settings."
|
||||
|
||||
echo ""
|
||||
echo "Once your VMs and DNS are set up, add your SSH key to the remote server:"
|
||||
echo " ssh-copy-id -i ~/.ssh/id_25519_internal $USER@<server-ip>"
|
||||
|
||||
echo ""
|
||||
echo "Add your SSH key for Git access. Your public key is:"
|
||||
echo " $(cat ~/.ssh/id_ed25519_git.pub)"
|
||||
|
||||
echo ""
|
||||
echo "Update your Git remote to use SSH instead of HTTPS:"
|
||||
echo " git remote set-url origin git@gitea.tomastm.com:tomas.mirchev/dotfiles.git"
|
||||
|
||||
echo ""
|
||||
echo "Install WindowTagger. You can find the repository here:"
|
||||
echo " https://gitea.tomastm.com/tomas.mirchev/window-tagger"
|
||||
117
src/console_logger.py
Normal file
117
src/console_logger.py
Normal file
@ -0,0 +1,117 @@
|
||||
class ConsoleLogger:
|
||||
# Color constants
|
||||
BLUE = "\033[34m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
RED = "\033[31m"
|
||||
CYAN = "\033[36m"
|
||||
GRAY = "\033[90m"
|
||||
DARK_GRAY = "\033[2;37m"
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
# Box drawing characters
|
||||
BOX_VERTICAL = "│"
|
||||
BOX_HORIZONTAL = "─"
|
||||
BOX_TOP_LEFT = "┌"
|
||||
BOX_TOP_RIGHT = "┐"
|
||||
BOX_BOTTOM_LEFT = "└"
|
||||
BOX_BOTTOM_RIGHT = "┘"
|
||||
|
||||
def __init__(self):
|
||||
self.step_counter = 0
|
||||
self.start_time = None
|
||||
|
||||
def info(self, message: str):
|
||||
print(f"{self.CYAN}[INFO]{self.RESET} {message}")
|
||||
|
||||
def warn(self, message: str):
|
||||
print(f"{self.YELLOW}[WARN]{self.RESET} {message}")
|
||||
|
||||
def error(self, message: str):
|
||||
print(f"{self.RED}[ERROR]{self.RESET} {message}")
|
||||
|
||||
def success(self, message: str):
|
||||
print(f"{self.GREEN}[SUCCESS]{self.RESET} {message}")
|
||||
|
||||
def step_start(self, current: int, total: int, description: str):
|
||||
"""Start a new step with Docker-style formatting"""
|
||||
print(f"\n{self.BOLD}{self.BLUE}Step {current}/{total}:{self.RESET} {self.BOLD}{description}{self.RESET}")
|
||||
print(f"{self.BLUE}{self.BOX_HORIZONTAL * 4}{self.RESET} {self.GRAY}Starting...{self.RESET}")
|
||||
self.start_time = time.time()
|
||||
|
||||
def step_command(self, command: str):
|
||||
"""Show the command being executed"""
|
||||
print(f"{self.BLUE}{self.BOX_VERTICAL} {self.RESET}{self.GRAY}$ {command}{self.RESET}")
|
||||
|
||||
def step_output(self, line: str):
|
||||
"""Show command output with indentation"""
|
||||
if line.strip():
|
||||
print(f"{self.BLUE}{self.BOX_VERTICAL} {self.RESET}{self.DARK_GRAY} {line.rstrip()}{self.RESET}")
|
||||
|
||||
def step_complete(self, message: str = "Completed successfully"):
|
||||
"""Mark step as completed"""
|
||||
elapsed = time.time() - self.start_time if self.start_time else 0
|
||||
print(f"{self.BLUE}{self.BOX_VERTICAL} {self.RESET}{self.GREEN}✓ {message} ({elapsed:.1f}s){self.RESET}")
|
||||
|
||||
def step_skip(self, message: str):
|
||||
"""Mark step as skipped"""
|
||||
elapsed = time.time() - self.start_time if self.start_time else 0
|
||||
print(
|
||||
f"{self.BLUE}{self.BOX_VERTICAL} {self.RESET}{self.YELLOW}⚠ Skipped: {message} ({elapsed:.1f}s){self.RESET}"
|
||||
)
|
||||
|
||||
def step_fail(self, message: str):
|
||||
"""Mark step as failed"""
|
||||
elapsed = time.time() - self.start_time if self.start_time else 0
|
||||
print(
|
||||
f"{self.BLUE}{self.BOX_VERTICAL} {self.RESET}{self.RED}✗ Failed: {message} ({elapsed:.1f}s){self.RESET}"
|
||||
)
|
||||
|
||||
def section_header(self, title: str, subtitle: str = ""):
|
||||
"""Print a section header"""
|
||||
width = 70
|
||||
print(f"\n{self.BOLD}{self.BLUE}{'=' * width}{self.RESET}")
|
||||
if subtitle:
|
||||
print(f"{self.BOLD}{self.BLUE}🚀 {title.upper()} - {subtitle}{self.RESET}")
|
||||
else:
|
||||
print(f"{self.BOLD}{self.BLUE}🚀 {title.upper()}{self.RESET}")
|
||||
print(f"{self.BOLD}{self.BLUE}{'=' * width}{self.RESET}")
|
||||
|
||||
def section_summary(self, title: str):
|
||||
"""Print a section summary header"""
|
||||
width = 70
|
||||
print(f"\n{self.BOLD}{self.GREEN}{'=' * width}{self.RESET}")
|
||||
print(f"{self.BOLD}{self.GREEN}📊 {title.upper()}{self.RESET}")
|
||||
print(f"{self.BOLD}{self.GREEN}{'=' * width}{self.RESET}")
|
||||
|
||||
def plan_header(self, title: str, count: int):
|
||||
"""Print planning phase header"""
|
||||
width = 70
|
||||
print(f"\n{self.BOLD}{self.CYAN}{'=' * width}{self.RESET}")
|
||||
print(f"{self.BOLD}{self.CYAN}📋 {title.upper()} ({count} actions){self.RESET}")
|
||||
print(f"{self.BOLD}{self.CYAN}{'=' * width}{self.RESET}")
|
||||
|
||||
def plan_category(self, category: str):
|
||||
"""Print plan category"""
|
||||
print(f"\n{self.BOLD}{self.CYAN}{category.upper()}{self.RESET}")
|
||||
print(f"{self.CYAN}{'─' * 20}{self.RESET}")
|
||||
|
||||
def plan_item(self, number: int, description: str, os_filter: str = None, critical: bool = False):
|
||||
"""Print a plan item"""
|
||||
# OS compatibility indicator
|
||||
os_indicator = ""
|
||||
if os_filter:
|
||||
os_indicator = f" {self.GRAY}({os_filter}){self.RESET}"
|
||||
|
||||
# Error handling indicator
|
||||
error_indicator = f" {self.RED}(critical){self.RESET}" if critical else ""
|
||||
|
||||
print(f" {number:2d}. {description}{os_indicator}{error_indicator}")
|
||||
|
||||
def plan_legend(self):
|
||||
"""Print plan legend"""
|
||||
print(
|
||||
f"\n{self.GRAY}Legend: {self.RED}(critical){self.GRAY} = stops on failure, {self.GRAY}(os){self.GRAY} = OS-specific{self.RESET}"
|
||||
)
|
||||
812
src/dotfiles_manager.py
Normal file
812
src/dotfiles_manager.py
Normal file
@ -0,0 +1,812 @@
|
||||
class DotfilesManager:
|
||||
def __init__(
|
||||
self,
|
||||
environment: str,
|
||||
manifest_path: str = "manifest.yaml",
|
||||
secrets_path: str = "secrets.yaml",
|
||||
):
|
||||
self.dotfiles_dir = Path.cwd()
|
||||
self.manifest_path = self.dotfiles_dir / manifest_path
|
||||
self.secrets_path = self.dotfiles_dir / secrets_path
|
||||
self.config_dir = self.dotfiles_dir / "config"
|
||||
|
||||
# Initialize console logger
|
||||
self.console = ConsoleLogger()
|
||||
|
||||
# Load configuration
|
||||
self.variables = self._load_secrets()
|
||||
self.manifest = self._load_manifest()
|
||||
|
||||
# System info
|
||||
self.system_os = self._get_system_os()
|
||||
self.system_arch = self._get_system_arch()
|
||||
self.system_platform = f"{self.system_os}-{self.system_arch}"
|
||||
|
||||
# Validate environment
|
||||
if environment not in self.manifest.get("environments", {}):
|
||||
self.console.error(f"Environment not found: {environment}")
|
||||
sys.exit(1)
|
||||
|
||||
self.environment = environment
|
||||
self.env_config = self.manifest["environments"][environment]
|
||||
self.pm = PackageManager(self)
|
||||
|
||||
# Execution state
|
||||
self.actions: List[Action] = []
|
||||
self.post_install_comments: List[str] = []
|
||||
|
||||
def info(self, message: str):
|
||||
self.console.info(message)
|
||||
|
||||
def warn(self, message: str):
|
||||
self.console.warn(message)
|
||||
|
||||
def error(self, message: str):
|
||||
self.console.error(message)
|
||||
sys.exit(1)
|
||||
|
||||
def success(self, message: str):
|
||||
self.console.success(message)
|
||||
|
||||
def _load_secrets(self) -> Dict[str, str]:
|
||||
variables = {}
|
||||
try:
|
||||
if self.secrets_path.exists():
|
||||
with open(self.secrets_path, "r") as f:
|
||||
yaml_data = yaml.safe_load(f)
|
||||
if yaml_data and "env" in yaml_data:
|
||||
variables.update(yaml_data["env"])
|
||||
except yaml.YAMLError as e:
|
||||
self.error(f"Error parsing secrets file: {e}")
|
||||
except Exception as e:
|
||||
self.error(f"Error reading secrets file: {e}")
|
||||
return variables
|
||||
|
||||
def add_variables(self, vars: List[str]):
|
||||
for var_setting in vars:
|
||||
if "=" not in var_setting:
|
||||
self.error(f"Invalid variable format: {var_setting}")
|
||||
key, value = var_setting.split("=", 1)
|
||||
self.variables[key] = value
|
||||
|
||||
def _load_manifest(self) -> Dict[str, Any]:
|
||||
try:
|
||||
with open(self.manifest_path, "r") as f:
|
||||
return yaml.safe_load(f)
|
||||
except FileNotFoundError:
|
||||
self.error(f"Manifest file not found: {self.manifest_path}")
|
||||
except yaml.YAMLError as e:
|
||||
self.error(f"Error parsing manifest: {e}")
|
||||
|
||||
def _get_system_os(self) -> str:
|
||||
os_mapping = {"Darwin": "macos", "Linux": "linux"}
|
||||
detected_os = platform.system()
|
||||
if detected_os not in os_mapping:
|
||||
self.error(f"Unsupported operating system: {detected_os}")
|
||||
return os_mapping[detected_os]
|
||||
|
||||
def _get_system_arch(self) -> str:
|
||||
arch_mapping = {"x86_64": "amd64", "aarch64": "arm64", "arm64": "arm64"}
|
||||
detected_arch = platform.machine().lower()
|
||||
if detected_arch not in arch_mapping:
|
||||
self.error(f"Unsupported system architecture: {detected_arch}")
|
||||
return arch_mapping[detected_arch]
|
||||
|
||||
def _substitute_variables(self, text: str) -> str:
|
||||
"""Substitute variables in text"""
|
||||
if not isinstance(text, str):
|
||||
return text
|
||||
|
||||
# Substitute custom variables
|
||||
for var, value in self.variables.items():
|
||||
text = text.replace(f"${var}", str(value))
|
||||
text = text.replace(f"${{{var}}}", str(value))
|
||||
|
||||
# Substitute common environment variables
|
||||
text = text.replace("$USER", os.getenv("USER", ""))
|
||||
text = text.replace("$HOME", str(Path.home()))
|
||||
|
||||
return text
|
||||
|
||||
def run_command(self, command: str, check: bool = True, shell: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run command with Docker-style output formatting"""
|
||||
self.console.step_command(command)
|
||||
|
||||
try:
|
||||
# Use Popen for real-time output
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
shell=shell,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1,
|
||||
)
|
||||
|
||||
# Stream output in real-time
|
||||
output_lines = []
|
||||
for line in process.stdout:
|
||||
line = line.rstrip()
|
||||
if line: # Only show non-empty lines
|
||||
self.console.step_output(line)
|
||||
output_lines.append(line)
|
||||
|
||||
process.wait()
|
||||
|
||||
if check and process.returncode != 0:
|
||||
raise subprocess.CalledProcessError(process.returncode, command, output="\n".join(output_lines))
|
||||
|
||||
# Create a mock CompletedProcess for compatibility
|
||||
result = subprocess.CompletedProcess(command, process.returncode, stdout="\n".join(output_lines), stderr="")
|
||||
return result
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(f"Command failed: {command}\nExit code: {e.returncode}")
|
||||
|
||||
# =============================================================================
|
||||
# ACTION PLANNING PHASE
|
||||
# =============================================================================
|
||||
|
||||
def plan_actions(self, command_filter: Optional[str] = None) -> List[Action]:
|
||||
"""Plan all actions based on environment configuration"""
|
||||
actions = []
|
||||
|
||||
# Check required variables first
|
||||
actions.extend(self._plan_variable_checks())
|
||||
|
||||
# System setup actions
|
||||
actions.extend(self._plan_hostname_actions())
|
||||
|
||||
# Package manager setup
|
||||
actions.extend(self._plan_package_manager_actions())
|
||||
|
||||
# Package installation
|
||||
actions.extend(self._plan_package_installation_actions())
|
||||
|
||||
# System configuration
|
||||
actions.extend(self._plan_system_config_actions())
|
||||
|
||||
# SSH key generation
|
||||
actions.extend(self._plan_ssh_actions())
|
||||
|
||||
# Config linking
|
||||
actions.extend(self._plan_config_actions())
|
||||
|
||||
# Custom commands
|
||||
actions.extend(self._plan_custom_command_actions())
|
||||
|
||||
# Filter actions based on command
|
||||
if command_filter:
|
||||
actions = self._filter_actions(actions, command_filter)
|
||||
|
||||
return actions
|
||||
|
||||
def _plan_variable_checks(self) -> List[Action]:
|
||||
"""Plan variable requirement checks"""
|
||||
actions = []
|
||||
|
||||
if "requires" in self.env_config:
|
||||
for req_var in self.env_config["requires"]:
|
||||
actions.append(
|
||||
Action(
|
||||
type="check-variable",
|
||||
description=f"Check required variable: {req_var}",
|
||||
data={"variable": req_var},
|
||||
skip_on_error=False,
|
||||
)
|
||||
)
|
||||
|
||||
return actions
|
||||
|
||||
def _plan_hostname_actions(self) -> List[Action]:
|
||||
"""Plan hostname setting actions"""
|
||||
actions = []
|
||||
|
||||
if "hostname" in self.env_config:
|
||||
hostname = self._substitute_variables(self.env_config["hostname"])
|
||||
actions.append(
|
||||
Action(
|
||||
type="set-hostname",
|
||||
description=f"Set system hostname to: {hostname}",
|
||||
data={"hostname": hostname},
|
||||
skip_on_error=False,
|
||||
os_filter=None, # Both macos and linux support hostname setting
|
||||
)
|
||||
)
|
||||
|
||||
return actions
|
||||
|
||||
def _plan_package_manager_actions(self) -> List[Action]:
|
||||
"""Plan package manager setup actions"""
|
||||
actions = []
|
||||
|
||||
if "packages" in self.env_config:
|
||||
specified_pm = self.env_config.get("package-manager")
|
||||
pm = self.pm.get_package_manager(specified_pm)
|
||||
|
||||
# Install brew if needed
|
||||
if pm == "brew" and not shutil.which("brew"):
|
||||
actions.append(
|
||||
Action(
|
||||
type="install-brew",
|
||||
description="Install Homebrew package manager",
|
||||
data={},
|
||||
skip_on_error=False,
|
||||
os_filter="macos",
|
||||
)
|
||||
)
|
||||
|
||||
# Update package manager
|
||||
actions.append(
|
||||
Action(
|
||||
type="pm-update",
|
||||
description=f"Update {pm} package repositories",
|
||||
data={"pm": pm},
|
||||
skip_on_error=False,
|
||||
)
|
||||
)
|
||||
|
||||
return actions
|
||||
|
||||
def _plan_package_installation_actions(self) -> List[Action]:
|
||||
"""Plan package installation actions"""
|
||||
actions = []
|
||||
|
||||
if "packages" not in self.env_config:
|
||||
return actions
|
||||
|
||||
packages_config = self.env_config["packages"]
|
||||
specified_pm = self.env_config.get("package-manager")
|
||||
pm = self.pm.get_package_manager(specified_pm)
|
||||
|
||||
# Collect all packages by type
|
||||
all_packages = {"standard": set(), "cask": set(), "binary": []}
|
||||
|
||||
# Process standard packages
|
||||
if "standard" in packages_config:
|
||||
for pkg in packages_config["standard"]:
|
||||
if isinstance(pkg, str):
|
||||
all_packages["standard"].add(pkg)
|
||||
else:
|
||||
all_packages["standard"].add(pkg["name"])
|
||||
|
||||
# Process binary packages and their dependencies
|
||||
if "binary" in packages_config:
|
||||
for pkg in packages_config["binary"]:
|
||||
if isinstance(pkg, str):
|
||||
pkg_spec = {"name": pkg}
|
||||
else:
|
||||
pkg_spec = pkg.copy()
|
||||
|
||||
# Merge with binary config from manifest
|
||||
if pkg_spec["name"] in self.manifest.get("binaries", {}):
|
||||
binary_config = self.manifest["binaries"][pkg_spec["name"]]
|
||||
pkg_spec.update(binary_config)
|
||||
|
||||
# Add dependencies to standard packages
|
||||
if "dependencies" in pkg_spec:
|
||||
all_packages["standard"].update(pkg_spec["dependencies"])
|
||||
|
||||
all_packages["binary"].append(pkg_spec)
|
||||
|
||||
# Process cask packages
|
||||
if "cask" in packages_config:
|
||||
for pkg in packages_config["cask"]:
|
||||
if isinstance(pkg, str):
|
||||
all_packages["cask"].add(pkg)
|
||||
else:
|
||||
all_packages["cask"].add(pkg["name"])
|
||||
|
||||
# Create installation actions in order: standard -> cask -> binary
|
||||
if all_packages["standard"]:
|
||||
actions.append(
|
||||
Action(
|
||||
type="install-packages",
|
||||
description=f"Install {len(all_packages['standard'])} standard packages via {pm}",
|
||||
data={
|
||||
"pm": pm,
|
||||
"packages": list(all_packages["standard"]),
|
||||
"package_type": "standard",
|
||||
},
|
||||
skip_on_error=False,
|
||||
)
|
||||
)
|
||||
|
||||
if all_packages["cask"]:
|
||||
actions.append(
|
||||
Action(
|
||||
type="install-packages",
|
||||
description=f"Install {len(all_packages['cask'])} cask packages via {pm}",
|
||||
data={"pm": pm, "packages": list(all_packages["cask"]), "package_type": "cask"},
|
||||
skip_on_error=False,
|
||||
)
|
||||
)
|
||||
|
||||
# Process individual binary packages
|
||||
for pkg_spec in all_packages["binary"]:
|
||||
actions.append(
|
||||
Action(
|
||||
type="install-binary",
|
||||
description=f"Install binary: {pkg_spec['name']}",
|
||||
data={"package": pkg_spec},
|
||||
skip_on_error=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Add post-install actions for packages
|
||||
if "standard" in packages_config:
|
||||
for pkg in packages_config["standard"]:
|
||||
if isinstance(pkg, dict):
|
||||
actions.extend(self._plan_package_post_actions(pkg))
|
||||
|
||||
if "binary" in packages_config:
|
||||
for pkg in packages_config["binary"]:
|
||||
if isinstance(pkg, dict):
|
||||
actions.extend(self._plan_package_post_actions(pkg))
|
||||
|
||||
if "cask" in packages_config:
|
||||
for pkg in packages_config["cask"]:
|
||||
if isinstance(pkg, dict):
|
||||
actions.extend(self._plan_package_post_actions(pkg))
|
||||
|
||||
return actions
|
||||
|
||||
def _plan_package_post_actions(self, pkg_spec: Dict[str, Any]) -> List[Action]:
|
||||
"""Plan post-install actions for a package"""
|
||||
actions = []
|
||||
|
||||
if "post-install" in pkg_spec:
|
||||
actions.append(
|
||||
Action(
|
||||
type="run-command",
|
||||
description=f"Run post-install script for {pkg_spec['name']}",
|
||||
data={"command": pkg_spec["post-install"]},
|
||||
skip_on_error=True,
|
||||
)
|
||||
)
|
||||
|
||||
if "post-install-comment" in pkg_spec:
|
||||
actions.append(
|
||||
Action(
|
||||
type="store-comment",
|
||||
description=f"Store post-install comment for {pkg_spec['name']}",
|
||||
data={"comment": pkg_spec["post-install-comment"]},
|
||||
skip_on_error=True,
|
||||
)
|
||||
)
|
||||
|
||||
return actions
|
||||
|
||||
def _plan_system_config_actions(self) -> List[Action]:
|
||||
"""Plan system configuration actions"""
|
||||
actions = []
|
||||
|
||||
if "locale" in self.env_config and self.system_os == "linux":
|
||||
locale = self.env_config["locale"]
|
||||
actions.append(
|
||||
Action(
|
||||
type="set-locale",
|
||||
description=f"Set system locale to: {locale}",
|
||||
data={"locale": locale},
|
||||
skip_on_error=True,
|
||||
os_filter="linux",
|
||||
)
|
||||
)
|
||||
|
||||
if "shell" in self.env_config and self.system_os == "linux":
|
||||
shell = self.env_config["shell"]
|
||||
actions.append(
|
||||
Action(
|
||||
type="set-shell",
|
||||
description=f"Set default shell to: {shell}",
|
||||
data={"shell": shell},
|
||||
skip_on_error=True,
|
||||
os_filter="linux",
|
||||
)
|
||||
)
|
||||
|
||||
return actions
|
||||
|
||||
def _plan_ssh_actions(self) -> List[Action]:
|
||||
"""Plan SSH key generation actions"""
|
||||
actions = []
|
||||
|
||||
if "ssh_keygen" in self.env_config:
|
||||
for ssh_config in self.env_config["ssh_keygen"]:
|
||||
key_type = ssh_config["type"]
|
||||
filename = ssh_config.get("filename", f"id_{key_type}")
|
||||
actions.append(
|
||||
Action(
|
||||
type="generate-ssh-key",
|
||||
description=f"Generate SSH key: {filename}",
|
||||
data=ssh_config,
|
||||
skip_on_error=True,
|
||||
)
|
||||
)
|
||||
|
||||
return actions
|
||||
|
||||
def _plan_config_actions(self) -> List[Action]:
|
||||
"""Plan configuration linking actions"""
|
||||
actions = []
|
||||
|
||||
# Get all configs to link (from packages and explicit configs)
|
||||
configs_to_link = set()
|
||||
|
||||
# Add configs from packages (unless link: false)
|
||||
if "packages" in self.env_config:
|
||||
for package_type, packages in self.env_config["packages"].items():
|
||||
for pkg in packages:
|
||||
if isinstance(pkg, str):
|
||||
pkg_name = pkg
|
||||
should_link = True
|
||||
else:
|
||||
pkg_name = pkg["name"]
|
||||
should_link = pkg.get("link", True)
|
||||
|
||||
if should_link:
|
||||
configs_to_link.add(pkg_name)
|
||||
|
||||
# Add explicit configs
|
||||
if "configs" in self.env_config:
|
||||
for config in self.env_config["configs"]:
|
||||
if isinstance(config, str):
|
||||
configs_to_link.add(config)
|
||||
else:
|
||||
configs_to_link.add(config["name"])
|
||||
|
||||
# Create link actions
|
||||
for config_name in configs_to_link:
|
||||
actions.append(
|
||||
Action(
|
||||
type="link-config",
|
||||
description=f"Link configuration: {config_name}",
|
||||
data={"config_name": config_name},
|
||||
skip_on_error=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Add post-link actions for explicit configs
|
||||
if "configs" in self.env_config:
|
||||
for config in self.env_config["configs"]:
|
||||
if isinstance(config, dict):
|
||||
actions.extend(self._plan_config_post_actions(config))
|
||||
|
||||
return actions
|
||||
|
||||
def _plan_config_post_actions(self, config_spec: Dict[str, Any]) -> List[Action]:
|
||||
"""Plan post-link actions for a config"""
|
||||
actions = []
|
||||
|
||||
if "post-link" in config_spec:
|
||||
actions.append(
|
||||
Action(
|
||||
type="run-command",
|
||||
description=f"Run post-link script for {config_spec['name']}",
|
||||
data={"command": config_spec["post-link"]},
|
||||
skip_on_error=True,
|
||||
)
|
||||
)
|
||||
|
||||
if "post-link-comment" in config_spec:
|
||||
actions.append(
|
||||
Action(
|
||||
type="store-comment",
|
||||
description=f"Store post-link comment for {config_spec['name']}",
|
||||
data={"comment": config_spec["post-link-comment"]},
|
||||
skip_on_error=True,
|
||||
)
|
||||
)
|
||||
|
||||
return actions
|
||||
|
||||
def _plan_custom_command_actions(self) -> List[Action]:
|
||||
"""Plan custom command actions"""
|
||||
actions = []
|
||||
|
||||
if "runcmd" in self.env_config:
|
||||
for i, command in enumerate(self.env_config["runcmd"]):
|
||||
actions.append(
|
||||
Action(
|
||||
type="run-command",
|
||||
description=f"Run custom command {i+1}",
|
||||
data={"command": command},
|
||||
skip_on_error=True,
|
||||
)
|
||||
)
|
||||
|
||||
return actions
|
||||
|
||||
def _filter_actions(self, actions: List[Action], command_filter: str) -> List[Action]:
|
||||
"""Filter actions based on command type and OS compatibility"""
|
||||
# First filter by OS compatibility
|
||||
filtered_actions = []
|
||||
for action in actions:
|
||||
if action.os_filter is None or action.os_filter == self.system_os:
|
||||
filtered_actions.append(action)
|
||||
else:
|
||||
self.info(f"Skipping {action.description} (not compatible with {self.system_os})")
|
||||
|
||||
# Then filter by command type
|
||||
if command_filter == "install":
|
||||
install_types = {
|
||||
"check-variable",
|
||||
"install-brew",
|
||||
"pm-update",
|
||||
"install-packages",
|
||||
"install-binary",
|
||||
}
|
||||
return [a for a in filtered_actions if a.type in install_types]
|
||||
elif command_filter == "link":
|
||||
link_types = {"check-variable", "link-config"}
|
||||
return [a for a in filtered_actions if a.type in link_types]
|
||||
|
||||
return filtered_actions
|
||||
|
||||
# =============================================================================
|
||||
# ACTION EXECUTION PHASE
|
||||
# =============================================================================
|
||||
|
||||
def execute_actions(self, actions: List[Action], dry_run: bool = False):
|
||||
"""Execute all planned actions"""
|
||||
if dry_run:
|
||||
self._print_plan(actions)
|
||||
return
|
||||
|
||||
# Filter out OS-incompatible actions that weren't filtered in planning
|
||||
compatible_actions = [a for a in actions if a.os_filter is None or a.os_filter == self.system_os]
|
||||
|
||||
if len(compatible_actions) != len(actions):
|
||||
skipped_count = len(actions) - len(compatible_actions)
|
||||
self.info(f"Skipped {skipped_count} OS-incompatible actions")
|
||||
|
||||
self.console.section_header(f"EXECUTING {len(compatible_actions)} ACTIONS", f"Environment: {self.environment}")
|
||||
|
||||
for i, action in enumerate(compatible_actions, 1):
|
||||
self.console.step_start(i, len(compatible_actions), action.description)
|
||||
|
||||
try:
|
||||
self._execute_action(action)
|
||||
action.status = "completed"
|
||||
self.console.step_complete()
|
||||
except Exception as e:
|
||||
action.error = str(e)
|
||||
if action.skip_on_error:
|
||||
action.status = "skipped"
|
||||
self.console.step_skip(str(e))
|
||||
else:
|
||||
action.status = "failed"
|
||||
self.console.step_fail(str(e))
|
||||
print(f"\n{self.console.RED}💥 Critical action failed, stopping execution{self.console.RESET}")
|
||||
break
|
||||
|
||||
# Show final summary
|
||||
self._print_execution_summary(compatible_actions)
|
||||
|
||||
def _execute_action(self, action: Action):
|
||||
"""Execute a single action"""
|
||||
executors = {
|
||||
"check-variable": self._execute_check_variable,
|
||||
"set-hostname": self._execute_set_hostname,
|
||||
"install-brew": self._execute_install_brew,
|
||||
"pm-update": self._execute_pm_update,
|
||||
"install-packages": self._execute_install_packages,
|
||||
"install-binary": self._execute_install_binary,
|
||||
"set-locale": self._execute_set_locale,
|
||||
"set-shell": self._execute_set_shell,
|
||||
"generate-ssh-key": self._execute_generate_ssh_key,
|
||||
"link-config": self._execute_link_config,
|
||||
"run-command": self._execute_run_command,
|
||||
"store-comment": self._execute_store_comment,
|
||||
}
|
||||
|
||||
executor = executors.get(action.type)
|
||||
if not executor:
|
||||
raise RuntimeError(f"Unknown action type: {action.type}")
|
||||
|
||||
executor(action.data)
|
||||
|
||||
def _execute_check_variable(self, data: Dict[str, Any]):
|
||||
"""Execute variable check"""
|
||||
variable = data["variable"]
|
||||
if variable not in self.variables:
|
||||
raise RuntimeError(f"Required variable not set: {variable}")
|
||||
|
||||
def _execute_set_hostname(self, data: Dict[str, Any]):
|
||||
"""Execute hostname setting"""
|
||||
hostname = data["hostname"]
|
||||
if self.system_os == "macos":
|
||||
self.run_command(f"sudo scutil --set ComputerName '{hostname}'")
|
||||
self.run_command(f"sudo scutil --set HostName '{hostname}'")
|
||||
self.run_command(f"sudo scutil --set LocalHostName '{hostname}'")
|
||||
else:
|
||||
self.run_command(f"sudo hostnamectl set-hostname '{hostname}'")
|
||||
|
||||
def _execute_install_brew(self, data: Dict[str, Any]):
|
||||
"""Execute Homebrew installation"""
|
||||
self.run_command(
|
||||
'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
||||
)
|
||||
|
||||
def _execute_pm_update(self, data: Dict[str, Any]):
|
||||
"""Execute package manager update"""
|
||||
pm = data["pm"]
|
||||
cmd = self.pm.get_update_command(pm)
|
||||
self.run_command(cmd)
|
||||
|
||||
def _execute_install_packages(self, data: Dict[str, Any]):
|
||||
"""Execute package installation"""
|
||||
pm = data["pm"]
|
||||
packages = data["packages"]
|
||||
package_type = data["package_type"]
|
||||
|
||||
if not packages:
|
||||
return
|
||||
|
||||
cmd = self.pm.get_install_command(pm, packages, package_type)
|
||||
self.run_command(cmd)
|
||||
|
||||
def _execute_install_binary(self, data: Dict[str, Any]):
|
||||
"""Execute binary installation"""
|
||||
package = data["package"]
|
||||
# This would need implementation based on your binary installation logic
|
||||
# For now, just a placeholder
|
||||
self.info(f"Binary installation for {package['name']} would happen here")
|
||||
|
||||
def _execute_set_locale(self, data: Dict[str, Any]):
|
||||
"""Execute locale setting"""
|
||||
locale = data["locale"]
|
||||
self.run_command(f"sudo locale-gen {locale}")
|
||||
self.run_command(f"sudo update-locale LANG={locale}")
|
||||
|
||||
def _execute_set_shell(self, data: Dict[str, Any]):
|
||||
"""Execute shell setting"""
|
||||
shell = data["shell"]
|
||||
shell_path = shutil.which(shell)
|
||||
if not shell_path:
|
||||
raise RuntimeError(f"Shell not found: {shell}")
|
||||
|
||||
# Add shell to /etc/shells if not present
|
||||
try:
|
||||
with open("/etc/shells", "r") as f:
|
||||
shells = f.read()
|
||||
if shell_path not in shells:
|
||||
self.run_command(f"echo '{shell_path}' | sudo tee -a /etc/shells")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
self.run_command(f"chsh -s {shell_path}")
|
||||
|
||||
def _execute_generate_ssh_key(self, data: Dict[str, Any]):
|
||||
"""Execute SSH key generation"""
|
||||
ssh_dir = Path.home() / ".ssh"
|
||||
ssh_dir.mkdir(mode=0o700, exist_ok=True)
|
||||
|
||||
key_type = data["type"]
|
||||
comment = self._substitute_variables(data.get("comment", ""))
|
||||
filename = data.get("filename", f"id_{key_type}")
|
||||
|
||||
key_path = ssh_dir / filename
|
||||
|
||||
if key_path.exists():
|
||||
self.warn(f"SSH key already exists: {key_path}")
|
||||
return
|
||||
|
||||
cmd = f'ssh-keygen -t {key_type} -f "{key_path}" -N "" -C "{comment}"'
|
||||
self.run_command(cmd)
|
||||
|
||||
def _execute_link_config(self, data: Dict[str, Any]):
|
||||
"""Execute configuration linking"""
|
||||
config_name = data["config_name"]
|
||||
# This would need implementation based on your config linking logic
|
||||
# For now, just a placeholder
|
||||
self.info(f"Config linking for {config_name} would happen here")
|
||||
|
||||
def _execute_run_command(self, data: Dict[str, Any]):
|
||||
"""Execute custom command"""
|
||||
command = self._substitute_variables(data["command"])
|
||||
self.run_command(command)
|
||||
|
||||
def _execute_store_comment(self, data: Dict[str, Any]):
|
||||
"""Execute comment storage"""
|
||||
comment = self._substitute_variables(data["comment"])
|
||||
self.post_install_comments.append(comment)
|
||||
|
||||
def _print_plan(self, actions: List[Action]):
|
||||
"""Print execution plan"""
|
||||
self.console.plan_header(f"EXECUTION PLAN FOR {self.environment}", len(actions))
|
||||
|
||||
# Group actions by type for better readability
|
||||
grouped_actions = {}
|
||||
for action in actions:
|
||||
action_category = action.type.split("-")[0] # "install", "set", "link", etc.
|
||||
if action_category not in grouped_actions:
|
||||
grouped_actions[action_category] = []
|
||||
grouped_actions[action_category].append(action)
|
||||
|
||||
for category, category_actions in grouped_actions.items():
|
||||
self.console.plan_category(category)
|
||||
for i, action in enumerate(category_actions, 1):
|
||||
# Check if action will be skipped due to OS compatibility
|
||||
will_skip = action.os_filter and action.os_filter != self.system_os
|
||||
|
||||
if will_skip:
|
||||
self.console.plan_item(
|
||||
i,
|
||||
f"{action.description} (will be skipped - {action.os_filter} only)",
|
||||
action.os_filter,
|
||||
not action.skip_on_error,
|
||||
)
|
||||
else:
|
||||
self.console.plan_item(i, action.description, action.os_filter, not action.skip_on_error)
|
||||
|
||||
self.console.plan_legend()
|
||||
|
||||
def _print_execution_summary(self, actions: List[Action]):
|
||||
"""Print execution summary"""
|
||||
completed = len([a for a in actions if a.status == "completed"])
|
||||
failed = len([a for a in actions if a.status == "failed"])
|
||||
skipped = len([a for a in actions if a.status == "skipped"])
|
||||
|
||||
self.console.section_summary("EXECUTION SUMMARY")
|
||||
|
||||
print(f"Total actions: {self.console.BOLD}{len(actions)}{self.console.RESET}")
|
||||
print(f"Completed: {self.console.GREEN}{completed}{self.console.RESET}")
|
||||
if failed > 0:
|
||||
print(f"Failed: {self.console.RED}{failed}{self.console.RESET}")
|
||||
if skipped > 0:
|
||||
print(f"Skipped: {self.console.YELLOW}{skipped}{self.console.RESET}")
|
||||
|
||||
if self.post_install_comments:
|
||||
print(f"\n{self.console.BOLD}📝 POST-INSTALL NOTES{self.console.RESET}")
|
||||
print(f"{self.console.CYAN}{'─' * 25}{self.console.RESET}")
|
||||
for i, comment in enumerate(self.post_install_comments, 1):
|
||||
print(f"{i}. {comment}")
|
||||
|
||||
if failed > 0:
|
||||
print(f"\n{self.console.BOLD}❌ FAILED ACTIONS{self.console.RESET}")
|
||||
print(f"{self.console.RED}{'─' * 20}{self.console.RESET}")
|
||||
for action in actions:
|
||||
if action.status == "failed":
|
||||
print(f"{self.console.RED}✗{self.console.RESET} {action.description}")
|
||||
print(f" {self.console.GRAY}Error: {action.error}{self.console.RESET}")
|
||||
|
||||
# Final status
|
||||
if failed == 0:
|
||||
print(f"\n{self.console.GREEN}🎉 All actions completed successfully!{self.console.RESET}")
|
||||
else:
|
||||
print(f"\n{self.console.RED}💥 {failed} action(s) failed. Check the errors above.{self.console.RESET}")
|
||||
|
||||
# =============================================================================
|
||||
# PUBLIC INTERFACE
|
||||
# =============================================================================
|
||||
|
||||
def setup_environment(self, dry_run: bool = False):
|
||||
"""Setup complete environment"""
|
||||
actions = self.plan_actions()
|
||||
self.execute_actions(actions, dry_run)
|
||||
|
||||
def install_packages(self, package_name: Optional[str] = None, dry_run: bool = False):
|
||||
"""Install packages"""
|
||||
actions = self.plan_actions("install")
|
||||
if package_name:
|
||||
# Filter to specific package
|
||||
actions = [a for a in actions if package_name in str(a.data)]
|
||||
self.execute_actions(actions, dry_run)
|
||||
|
||||
def link_configs(
|
||||
self,
|
||||
config_name: Optional[str] = None,
|
||||
copy: bool = False,
|
||||
force: bool = False,
|
||||
dry_run: bool = False,
|
||||
):
|
||||
"""Link configurations"""
|
||||
actions = self.plan_actions("link")
|
||||
if config_name:
|
||||
# Filter to specific config
|
||||
actions = [a for a in actions if config_name in str(a.data)]
|
||||
|
||||
# TODO: Handle copy and force flags in action data
|
||||
self.execute_actions(actions, dry_run)
|
||||
|
||||
63
src/package_manager.py
Normal file
63
src/package_manager.py
Normal file
@ -0,0 +1,63 @@
|
||||
class PackageManager:
|
||||
SUPPORTED_MANAGERS = {
|
||||
"brew": {
|
||||
"update_cmd": "brew update",
|
||||
"install_cmd": "brew install {packages}",
|
||||
"install_cask_cmd": "brew install --cask {packages}",
|
||||
},
|
||||
"apt": {
|
||||
"update_cmd": "sudo apt-get update",
|
||||
"install_cmd": "sudo apt-get install -y {packages}",
|
||||
},
|
||||
"dnf": {
|
||||
"update_cmd": "sudo dnf check-update || true",
|
||||
"install_cmd": "sudo dnf install -y {packages}",
|
||||
},
|
||||
"pacman": {
|
||||
"update_cmd": "sudo pacman -Sy",
|
||||
"install_cmd": "sudo pacman -S --noconfirm {packages}",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.system_os = "macos" if platform.system() == "Darwin" else "linux"
|
||||
self.detected_pm = self._detect_package_manager()
|
||||
|
||||
def _detect_package_manager(self) -> str:
|
||||
if self.system_os == "macos":
|
||||
return "brew"
|
||||
else:
|
||||
if shutil.which("apt"):
|
||||
return "apt"
|
||||
elif shutil.which("dnf"):
|
||||
return "dnf"
|
||||
elif shutil.which("pacman"):
|
||||
return "pacman"
|
||||
else:
|
||||
raise ValueError("No supported package manager found (apt, dnf, pacman)")
|
||||
|
||||
def get_package_manager(self, specified_pm: Optional[str] = None) -> str:
|
||||
if specified_pm:
|
||||
if specified_pm != self.detected_pm:
|
||||
raise ValueError(
|
||||
f"Inconsistent package manager: detected ({self.detected_pm}) != specified ({specified_pm})"
|
||||
)
|
||||
return specified_pm
|
||||
return self.detected_pm
|
||||
|
||||
def get_update_command(self, pm: str) -> str:
|
||||
if pm not in self.SUPPORTED_MANAGERS:
|
||||
raise ValueError(f"Unsupported package manager: {pm}")
|
||||
return self.SUPPORTED_MANAGERS[pm]["update_cmd"]
|
||||
|
||||
def get_install_command(self, pm: str, packages: List[str], package_type: str = "standard") -> str:
|
||||
if pm not in self.SUPPORTED_MANAGERS:
|
||||
raise ValueError(f"Unsupported package manager: {pm}")
|
||||
|
||||
pm_config = self.SUPPORTED_MANAGERS[pm]
|
||||
|
||||
if package_type == "cask" and pm == "brew":
|
||||
return pm_config["install_cask_cmd"].format(packages=" ".join(packages))
|
||||
else:
|
||||
return pm_config["install_cmd"].format(packages=" ".join(packages))
|
||||
54
test.py
Normal file
54
test.py
Normal file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
# Use Popen for real-time output
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
shell=shell,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1,
|
||||
)
|
||||
|
||||
# Stream output in real-time
|
||||
output_lines = []
|
||||
for line in process.stdout:
|
||||
line = line.rstrip()
|
||||
if line: # Only show non-empty lines
|
||||
self.console.step_output(line)
|
||||
output_lines.append(line)
|
||||
|
||||
process.wait()
|
||||
|
||||
if check and process.returncode != 0:
|
||||
raise subprocess.CalledProcessError(process.returncode, command, output="\n".join(output_lines))
|
||||
|
||||
# Create a mock CompletedProcess for compatibility
|
||||
result = subprocess.CompletedProcess(command, process.returncode, stdout="\n".join(output_lines), stderr="")
|
||||
return result
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(f"Command failed: {command}\nExit code: {e.returncode}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user