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
|
**/.DS_Store
|
||||||
|
.venv/
|
||||||
.dotfiles_env
|
.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