Compare commits

..

No commits in common. "main" and "v2" have entirely different histories.
main ... v2

64 changed files with 2188 additions and 3096 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
**/.DS_Store
.venv/
.dotfiles_env

6
.gitmodules vendored
View File

@ -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
View File

@ -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.

View File

@ -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)
}
}
);
}

View File

@ -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"
}

View File

@ -1 +0,0 @@
/home/tomas/.local/share/kwin/scripts

View File

@ -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"

View File

@ -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'

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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" }
}
]
}

View File

@ -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" }
}
]
}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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
}
}
]
}

View File

@ -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.

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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 containers 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[@] "$@"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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/

View File

@ -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

View File

@ -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}"'

View File

@ -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

View File

@ -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
View 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
View File

@ -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()

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
[tool.black]
line-length = 120
[tool.isort]
profile = "black"

View File

@ -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)"

View File

@ -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."

View File

@ -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."

View File

@ -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"

View 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."

View File

@ -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)"

View File

@ -1,196 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Close any open System Preferences panes, to prevent them from overriding
# settings were 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."

View File

@ -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
View 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!"

View File

@ -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\`"

View File

@ -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
View 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
View 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
View 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
View 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()